Summary
TrainerRank.forward_micro_batches admits a micro-batch after a memory-planner check, but the planner's activation estimate does not depend on recompute_granularity. With ART_MEGATRON_RECOMPUTE_GRANULARITY=selective (method/num_layers unset) on a 64-layer dense model (Qwen/Qwen3.8-27B, hidden 5120) the planner predicted 5.9 GB for a ~32.7k-token packed group and admitted it; the forward then hit CUDA OOM against a 107–119 GB budget on both TP 2 and TP 4:
art.trainer_rank.TrainerRankMemoryError: forward_micro_batches: CUDA OOM occurred despite the planner estimate.
packed_tokens=32710 logical_tokens=38443 predicted_peak_gb=5.894 usable_limit_gb=107.192.
Use smaller top-level items, reduce output requests, or call dp_rank_forward with already-DP-local smaller inputs.
(TP 4 rank: predicted_peak_gb=5.894 usable_limit_gb=119.289, same failure.) The OOM surfaces inside _decoder_hidden → transformer_block.forward → _forward_mlp → art.megatron.lora.forward on the very first forward of the run, so no learned memory profile exists yet and the static estimate is the only guard.
Why the estimate is off
_estimate_required_memory_bytes_from_values (src/art/trainer_rank/_impl.py) uses
activation_factor = max(4, min(16, num_layers // 4 + 4)) # 16 for 64 layers
static_compute = packed_tokens * hidden_size * dtype_size * activation_factor
required = (output_bytes + static_compute) * 1.10
For 32,710 × 5,120 × 2 B × 16 × 1.10 that is ≈5.9 GB, which matches the observed footprint under full recompute (each layer retains roughly one hidden tensor). Under selective every layer retains its attention/MLP inputs, the MLP intermediate (ffn_hidden ≫ hidden) and norm outputs, i.e. tens of hidden-tensor equivalents per layer, so the true peak is one to two orders of magnitude larger and sharding by TP does not close the gap. The estimator has no term for the recompute mode (there is no reference to recompute_granularity anywhere in trainer_rank/_impl.py), and the provider defaults to full for every model (megatron/provider.py), so the constant is implicitly calibrated for full recompute only.
Reproduction
Qwen/Qwen3.8-27B, rank-1 LoRA, Trainer(..., cluster="H200:2", num_processes_per_node=2)
- env:
ART_MEGATRON_CONTEXT_PARALLEL_SIZE=1 ART_MEGATRON_TENSOR_MODEL_PARALLEL_SIZE=2 ART_MEGATRON_RECOMPUTE_GRANULARITY=selective ART_MEGATRON_RECOMPUTE_METHOD=none ART_MEGATRON_RECOMPUTE_NUM_LAYERS=none
- one
forward_micro_batches call whose top-level item is a group of two ~16–20k-token sequences (38,443 logical tokens, 32,710 packed) with hidden_states requested → OOM on step 1. The same call with the default full recompute runs at 41 GiB peak per rank on TP 2 (22 GiB on TP 4).
Driver: caladan experiments/058-rank1-probe-bases.py probe ... --gpus 2 --tp 2 --no-recompute; ART checkout bcfe13519 (2026-09-08); the cluster ran caladan's pinned build.
Suggested fix
Any of these would have avoided the crash:
- Scale
activation_factor by the provider's effective recompute mode (full → current constant; selective → a per-layer retained-activation estimate that includes ffn_hidden_size and the attention inputs; none → larger still) so the check declines / splits instead of admitting.
- Failing that, treat an unprofiled signature under non-
full recompute as untrusted: run a probe forward on a small slice first, or cap the first admitted micro-batch until a profile exists.
- At minimum, mention the recompute mode in the
TrainerRankMemoryError hint, since "use smaller top-level items" is not actionable when the item is already a single (indivisible) group and the real remedy is switching back to full recompute.
Related: #911 (context-parallel forward returns rank-local shards; that is why these runs were on TP rather than the default CP topology).
Summary
TrainerRank.forward_micro_batchesadmits a micro-batch after a memory-planner check, but the planner's activation estimate does not depend onrecompute_granularity. WithART_MEGATRON_RECOMPUTE_GRANULARITY=selective(method/num_layers unset) on a 64-layer dense model (Qwen/Qwen3.8-27B, hidden 5120) the planner predicted 5.9 GB for a ~32.7k-token packed group and admitted it; the forward then hit CUDA OOM against a 107–119 GB budget on both TP 2 and TP 4:(TP 4 rank:
predicted_peak_gb=5.894 usable_limit_gb=119.289, same failure.) The OOM surfaces inside_decoder_hidden→transformer_block.forward→_forward_mlp→art.megatron.lora.forwardon the very first forward of the run, so no learned memory profile exists yet and the static estimate is the only guard.Why the estimate is off
_estimate_required_memory_bytes_from_values(src/art/trainer_rank/_impl.py) usesFor 32,710 × 5,120 × 2 B × 16 × 1.10 that is ≈5.9 GB, which matches the observed footprint under full recompute (each layer retains roughly one hidden tensor). Under
selectiveevery layer retains its attention/MLP inputs, the MLP intermediate (ffn_hidden≫ hidden) and norm outputs, i.e. tens of hidden-tensor equivalents per layer, so the true peak is one to two orders of magnitude larger and sharding by TP does not close the gap. The estimator has no term for the recompute mode (there is no reference torecompute_granularityanywhere intrainer_rank/_impl.py), and the provider defaults tofullfor every model (megatron/provider.py), so the constant is implicitly calibrated for full recompute only.Reproduction
Qwen/Qwen3.8-27B, rank-1 LoRA,Trainer(..., cluster="H200:2", num_processes_per_node=2)ART_MEGATRON_CONTEXT_PARALLEL_SIZE=1 ART_MEGATRON_TENSOR_MODEL_PARALLEL_SIZE=2 ART_MEGATRON_RECOMPUTE_GRANULARITY=selective ART_MEGATRON_RECOMPUTE_METHOD=none ART_MEGATRON_RECOMPUTE_NUM_LAYERS=noneforward_micro_batchescall whose top-level item is a group of two ~16–20k-token sequences (38,443 logical tokens, 32,710 packed) withhidden_statesrequested → OOM on step 1. The same call with the default full recompute runs at 41 GiB peak per rank on TP 2 (22 GiB on TP 4).Driver: caladan
experiments/058-rank1-probe-bases.py probe ... --gpus 2 --tp 2 --no-recompute; ART checkoutbcfe13519(2026-09-08); the cluster ran caladan's pinned build.Suggested fix
Any of these would have avoided the crash:
activation_factorby the provider's effective recompute mode (full → current constant; selective → a per-layer retained-activation estimate that includesffn_hidden_sizeand the attention inputs; none → larger still) so the check declines / splits instead of admitting.fullrecompute as untrusted: run a probe forward on a small slice first, or cap the first admitted micro-batch until a profile exists.TrainerRankMemoryErrorhint, since "use smaller top-level items" is not actionable when the item is already a single (indivisible) group and the real remedy is switching back to full recompute.Related: #911 (context-parallel forward returns rank-local shards; that is why these runs were on TP rather than the default CP topology).