graph_trainer: auto-size GPU_MAX_HW_QUEUES on ROCm - #4032
Conversation
|
Unknown label
|
|
|
| lanes = ["compute", "all_reduce"] # compute + loss/grad all_reduce (distributed) | ||
| if dp_shard_active: | ||
| if fsdp_ag_rs_overlap: | ||
| lanes += ["dense_all_gather", "reduce_scatter"] # split onto own PGs |
There was a problem hiding this comment.
why do we need two lanes for "dense_all_gather", "reduce_scatter"?
There was a problem hiding this comment.
When enable_fsdp_ag_rs_overlap is on, reassign_collective_pgs_pass puts dense all-gather and reduce-scatter on separate process groups, so each gets its own stream
| lanes.append("fsdp_comm") # dense AG+RS unsplit -> share one stream | ||
| if is_moe and ep > 1: | ||
| lanes.append("expert_all_gather") # eFSDP mesh AG (its own PG, structural) | ||
| lanes.append("expert_all_to_all") # expert-parallel dispatch (distinct EP PG) |
There was a problem hiding this comment.
"expert_all_gather" and "expert_all_to_all" never overlap. so they can share one lane.
Estimate the compiled step's independent GPU stream count from the training config and set GPU_MAX_HW_QUEUES to the next power of two before HIP init. Ignore if the env var is already set.
bb4406b to
a2fd5c3
Compare
Move GPU_MAX_HW_QUEUES setup out of train.py and run it before super().__init__() so it stays before HIP init while keeping the logic scoped to graph_trainer.
|
@rishisinhanj is this only needed for graph trainer or also needed for the main torchtitan trainer? |
@sanketpurandare This PR was intended only for Graphtrainer, as we have only validated the improvement for graph trainer and tested with GT workloads specifically so I've added it as a hook in the graph trainer trainer file. I think it is also valid to add to the regular torchtitan, but that is a bigger change. If you think it is worthwhile, I can change where it is being called as the underlying logic would remain identical. |
|
@rishisinhanj can you check the failing CI tests? |
|
@sanketpurandare We have observed that failure in our PR to reenable the ROCm CI in the past. I have linked an issue here with our findings: #4064 -> it enters a deadlock due to an SPMD race It's unrelated to this PR and any changes it makes so shouldn't affect merge status. |
| return 1 << (n - 1).bit_length() | ||
|
|
||
|
|
||
| def maybe_set_gpu_max_hw_queues(config: JobConfig) -> None: |
There was a problem hiding this comment.
if it's for rocm only, at least the function name and file name should reflect that? o/w it could be confusing to cuda users what this is doing.
sanketpurandare
left a comment
There was a problem hiding this comment.
We had some internal discussion about this, we understand that the abstraction that rocm has requires that we need to know the number of unique streams so that the hw queues can be set accordingly. The part that we are unhappy is that we are introducing hw dependent if-else style code in torchtitan. I would recommend instead adding a section in README on how to set the env variable GPU_MAX_HW_QUEUES based on parallelism configs. Does that sound okay?
SherlockNoMad
left a comment
There was a problem hiding this comment.
we don't have Rocm CI for graph trainer, and ROCM CI is failing for main for a long time...
Thanks for the review! Moving the whole logic to README is messy, as it is not as concise and direct as it is run by the code. In many places of the pytorch ecosystem, we have hip/rocm specific code. I think it is fine as long as the function names clearly indicate it is for AMD platforms (as Tianyu commented in this PR). |
Summary
On ROCm, HIP maps logical streams to hardware queues via
stream_id % Q. WhenQis too small, independent streams alias onto the same queue and lose overlap.Adds
hw_queues.maybe_set_gpu_max_hw_queues(): estimates independent GPU stream count from the graph_trainer config, setsGPU_MAX_HW_QUEUESto the next power of two, and runs inGraphTrainer.__init__before HIP init.GPU_MAX_HW_QUEUESoverrideStream model
EP=2 DeepSeek-V3 with FSDP AG/RS overlap → 5 lanes →
Q=8.Results
EP=2 2-node DeepSeek-V3 (MI355X, gfx950, ROCm 7.2). Throughput (tok/s/GPU):
fec3e19(2026-07-14)1ac4653(2026-07-29, current)On current main the win is net-neutral, but the setter lands on the best
Qand protects againstQ=16overshoot.Why this is still worth merging
Q >= #streamsis a hard ceiling: ifQis too small, colliding streams share a hardware queue and cannot run concurrently, regardless of scheduler quality. Today upstream scheduling already keeps dense FSDP collectives off the compute queue atQ=4, so the lever is not the binding constraint — but that can change as overlap improves or configs add streams.This hook is cheap (config-only, before HIP init, no profiling run) and ensures the queue count is sized correctly ahead of time. It also prevents accidental overshoot to
Q=16, which regressed throughput even on the current build.Test plan
PYTHONPATH=. python3 -m torchtitan.experiments.graph_trainer.tests.test_hw_queuesGPU_MAX_HW_QUEUES, run graph_trainer, confirm auto-set log lineGPU_MAX_HW_QUEUES=4→ no auto-set