Performance Metrics#
AngelSpec includes an opt-in metrics system to identify bottlenecks between the asynchronous inference and synchronous training stages.
Enabling#
Set debug.enable_perf_metrics: true (or override on the CLI). When disabled, no overhead is
added to the training loop.
python3 -m angelspec.train_entry --config configs/default.yaml debug.enable_perf_metrics=true
Metrics#
All metrics are logged under the perf/ namespace (tied to train/step).
Training (per optimizer step)
Metric |
Unit |
Description |
|---|---|---|
|
s |
Wall-clock time of a training step (data fetch + compute + optimizer) |
|
s |
Time in the data iterator (Ray queue get + Mooncake fetch + collation + H2D) |
|
s |
GPU time for forward + backward + optimizer, via CUDA events |
|
samples/s |
|
Inference (aggregated between steps)
Metric |
Unit |
Description |
|---|---|---|
|
samples/s |
System inference capacity across engines |
|
s |
Average time of a single engine generate call |
Pipeline health
Metric |
Unit |
Description |
|---|---|---|
|
s |
Time the loop waited for enough samples to dispatch. High → inference-bound |
Progress bar#
With metrics enabled, the progress bar adds fields:
Training: 10%|== | loss=0.350, acc=0.830, thru=12.1, I=18.5, T=13.2, wait=0.1s, pool=24, epoch=1/10
Key |
Meaning |
|---|---|
|
Realized end-to-end pipeline throughput (samples/s); in steady state ≈ |
|
Inference capacity — how fast inference could produce |
|
Training capacity — how fast training could consume |
|
Dispatch wait time |
|
Current sample-pool size |
Finding the bottleneck#
if dispatch_wait >> 0: bottleneck = inference # training is starved
elif pool grows over time: bottleneck = training # inference outpaces training
Within training:
if data_time >> compute_time: bottleneck = Mooncake transfer / data loading
elif compute_time >> data_time: bottleneck = GPU compute
Interpreting capacities:
I > T— training is the bottleneck; the pool tends to grow.T > I— inference is the bottleneck;dispatch_waitis high.I ≈ T— balanced;thruapproaches both.
Implementation notes#
Compute time uses CUDA events (torch.cuda.Event(enable_timing=True)) rather than
torch.cuda.synchronize(), so no extra synchronization is introduced — timings are extracted at
an existing sync point. Data-loading time uses wall-clock (time.time()) because that work is
CPU/network-bound and the GPU stream may be idle.