The Turning Point: Investigating GPU Utilization in EAGLE-3 Training
A Single Grep That Uncovered a Design Constraint
In the middle of training a custom EAGLE-3 draft model for the Kimi-K2.5 verifier, a seemingly trivial observation by the user triggered a chain of investigation that would reshape the entire training configuration. The user noticed that the four RTX PRO 6000 Blackwell GPUs were drawing only 250W each out of a 600W budget — a clear sign of underutilization. What followed was a deep dive into the speculators library's data pipeline, culminating in a single bash command that served as the critical evidence-gathering step in the reasoning chain.
The Message
The target message, <msg id=4257>, is deceptively simple — a single bash command executed over SSH on the remote training machine:
ssh -o ConnectTimeout=10 root@10.1.230.174 'grep -rn "def.*train.*step\|batch_size\|for.*batch" ~/ml-env/lib/python3.12/site-packages/speculators/train/trainer.py 2>/dev/null | head -15'
The output returned four lines from the speculators trainer code:
164: for batch in train_loader:
169: for k, v in batch.items()
209: for batch in val_loader:
214: for k, v in batch.items()
On its face, this is just a grep hitting four lines in a Python file. But in the context of the ongoing investigation, this output was the fulcrum on which the entire training strategy pivoted.
The Context: Why This Message Was Written
The story begins two messages earlier, at <msg id=4251>, where the user asked: "Also was seeing only 250/600W GPU power use, can we bump batch or sth to get better util?" The assistant had just restarted training with --ttt-steps 5 (increasing from 3 to 5 test-time training steps for deeper speculative predictions), and the initial monitoring showed 100% GPU utilization but only 250W power draw — a classic sign that the GPU cores were active but the workload per step was too small to saturate the memory bandwidth and tensor cores.
The assistant's response at <msg id=4252> was to immediately kill the training and begin investigating. The reasoning was sound: the drafter model has only ~1.2 billion trainable parameters, and with batch_size=1 and sequence packing to 4096 tokens, each training step was processing a minuscule amount of work. The assistant hypothesized that increasing the batch size would pack more work into each step, driving up power consumption and overall throughput.
This led to a multi-step investigation. At <msg id=4253>, the assistant checked the speculators data loading code to understand what batch_size and max_seq_len parameters were supported. At <msg id=4254>, the assistant read the training script's DataLoader configuration, confirming it used batch_size=1 with a collate function that packs multiple samples into a single sequence of max_len tokens. At <msg id=4255>, the assistant read the specific line in 04_train.py showing batch_size=1 with the comment "Packing handles effective batch size."
Then came the critical question: would the speculators Trainer actually handle a batch size greater than 1? The DataLoader could be configured to yield batches of [batch_size, max_seq_len, ...] tensors, but if the trainer or model expected [1, max_seq_len, ...], increasing batch_size would crash. The assistant began investigating this at <msg id=4256> with a grep of the trainer code, and the target message <msg id=4257> is the result of that grep.
What the Grep Revealed — And What It Didn't
The grep output shows the trainer's training loop structure. Line 164 (for batch in train_loader:) and line 169 (for k, v in batch.items()) reveal that the trainer simply iterates over whatever the DataLoader yields and passes the items directly to the model. There is no explicit batch_size handling, no reshaping, no splitting logic. Critically, the grep found no match for batch_size in the trainer code at all.
This was a double-edged discovery. On one hand, the absence of batch_size handling meant the trainer was agnostic to batch size — it would accept whatever the DataLoader produced. On the other hand, it meant there was no mechanism to split a multi-batch into individual packed sequences, suggesting the model itself might need to handle the batch dimension.
The assistant didn't stop here. The grep output was immediately followed by deeper investigation. At <msg id=4258>, the assistant read the full training loop code (lines 155-200 of the trainer), confirming that the batch dict was passed straight to model(...). At <msg id=4259>, the assistant checked the model's forward method signature. At <msg id=4260>, the assistant read the actual forward method, discovering the critical constraint: the model expected hidden_states with shape [1, total_seq_len, 3 * hidden_size] — batch dimension fixed at 1.
The Discovery and the Pivot
The conclusion, delivered at <msg id=4261>, was definitive: "The forward method expects batch dimension = 1 (shape [1, total_seq_len, ...]). It uses flex_attention with BlockMask that's built for B=None (single batch). Increasing batch_size in the DataLoader would give [batch_size, total_seq_len, ...] tensors which would break this."
This was the moment of truth. The initial assumption — that increasing batch_size in the DataLoader would be a straightforward way to improve GPU utilization — was incorrect. The model architecture itself constrained the batch dimension to 1. The assistant pivoted to the alternative: increasing --max-seq-len from 4096 to 8192, packing more training samples into each sequence and thereby increasing the computational work per step. With 96 GB of VRAM available and only 28.5 GB used, there was ample headroom.
Assumptions, Mistakes, and Lessons
The investigation revealed several assumptions, some correct and some not:
Correct assumption: The GPUs were underutilized despite showing 100% utilization. The assistant correctly identified that 100% GPU-Util with only 250W power draw indicated small per-step workloads rather than idle cores.
Incorrect assumption: That increasing batch_size in the DataLoader would be a viable path. The assistant initially believed that the trainer and model could handle multi-batch inputs, and was preparing to add a --batch-size argument to the training script (as seen at <msg id=4254>). The grep and subsequent code reading disproved this.
Correct assumption: That the speculators library's code structure could be investigated remotely via SSH and grep. The assistant's systematic approach — checking data loading, then the trainer loop, then the model's forward method — was methodical and efficient.
Implicit assumption: That the model's flex_attention implementation with BlockMask was fundamentally single-batch. This turned out to be correct, and understanding this constraint was essential to choosing the right optimization path.
Input and Output Knowledge
To understand this message, the reader needs knowledge of: PyTorch's DataLoader and batch dimension conventions; the concept of sequence packing (collating multiple variable-length samples into a single fixed-length sequence); the EAGLE-3 architecture and its use of flex_attention with block-diagonal masks; and the speculators library's structure (trainer → model → forward).
The message created critical output knowledge: evidence that the speculators trainer does not explicitly handle batch size, paving the way for the deeper investigation that revealed the batch-dimension constraint. This knowledge directly informed the decision to increase max_seq_len instead of batch_size, which in turn led to better GPU utilization (as confirmed in subsequent monitoring).
The Thinking Process
The reasoning visible in this message and its surrounding context shows a systematic debugging approach: observe a symptom (low power draw), form a hypothesis (increase batch size), gather evidence (grep the trainer code), interpret the results (no batch_size handling found), and follow the evidence trail deeper (check the model's forward method). The assistant did not jump to conclusions — each step built on the previous one, and the grep at <msg id=4257> was the crucial evidence-gathering step that ruled out one path and pointed toward another.
This single bash command, in retrospect, was the turning point. Without it, the assistant might have wasted time modifying the training script to support multi-batch, only to discover at runtime that the model couldn't handle it. Instead, the discovery was made in seconds, and the training was restarted with a better configuration within minutes.