Skip to content

Amd/hip graph support - #6

Open
zhihuidu-amd wants to merge 4 commits into
mainfrom
amd/hip-graph-support
Open

Amd/hip graph support#6
zhihuidu-amd wants to merge 4 commits into
mainfrom
amd/hip-graph-support

Conversation

@zhihuidu-amd

Copy link
Copy Markdown

PR Description: Opt A + HG (hipGraph capture + multi-stream)

Use for: zhihuidu-amd PR#1, AMD-Ecosystem new PR, google-deepmind PR#1556

Title: feat(hip): HIP graph capture + multi-stream parallelism for AMD ROCm (Opt A+HG)


Summary

Enables correct and efficient HIP graph capture for AMD ROCm in mujoco_warp, and adds multi-stream parallelism in fwd_position. Makes the standard upstream graph capture pattern work on AMD MI300X/MI325X (ROCm 7.x) with zero user API changes.

Measured on AMD MI325X (gfx942, ROCm 7.2), Unitree G1, 16,384 environments:

Configuration sps vs AMD baseline vs NVIDIA H200 (114,905 sps)
AMD baseline (no opts, upstream) 72,998 100% 64%
NVIDIA H200 reference 114,905 157% 100%
This PR: Opt A + HG 165,642 227% 144%

AMD MI325X with this PR surpasses H200 by 44% on Unitree G1 locomotion.


Root Cause of the Previous 3× Regression

On HIP/ROCm, hipMallocAsync calls inside ScopedCapture become memAlloc graph nodes that re-execute every replay. Before this fix: 24 memAlloc + 24 memFree nodes → replay 7.1ms (0.33× slower than eager 2.4ms). After: 0 memAlloc nodes → 1.4ms (1.7× faster).


Changes

io.py — Pre-allocate scratch buffers + dedicated streams

Pre-allocates solver context, tendon scratch, and step_size_cost buffers using hipMalloc (stable addresses, no graph nodes) before capture. Also pre-creates dedicated streams:

d._stream_collision = wp.Stream(device=dev)   # for collision detection
d._stream_secondary = wp.Stream(device=dev)   # for mass-matrix kinematics

forward.py — Multi-stream fork-join in fwd_position

Collision detection and mass-matrix kinematics run concurrently after fwd_kinematics, joined via GPU events (capturable as graph dependency edges — unlike synchronize_stream which raises RuntimeError during HIP capture):

with wp.ScopedStream(d._stream_collision):   # Fork A: collision
    collision_driver.collision(m, d)
with wp.ScopedStream(d._stream_secondary):   # Fork B: kinematics
    smooth.crb(m, d); smooth.factor_m(m, d)
# Join via GPU events — capturable in both CUDA and HIP graphs
d._stream_collision.record_event(d._event_collision)
d._stream_secondary.record_event(d._event_secondary)
wp.get_device().stream.wait_event(d._event_collision)
wp.get_device().stream.wait_event(d._event_secondary)

Falls back to sequential when sleep is enabled or streams unavailable. Single-stream path used during graph capture for minimal graph overhead.

solver.py + smooth.py — Eliminate 24 memAlloc nodes

Cache _solver_ctx, _ten_Jdot, _ten_bias_coef using hipMalloc on first call. Graph: 282 nodes → 234 nodes, 0 memAlloc.

__init__.pymjw.hip_graph_capture() context manager

# Handles mempool + warmup automatically. Works identically on NVIDIA CUDA and AMD ROCm.
with mjw.hip_graph_capture(model, data) as cap:
    mjw.step(model, data)

for _ in range(1000):
    wp.capture_launch(cap.graph)  # 1.7× faster on AMD ROCm

Benchmark Results

Step() throughput (physics only)

Environments Eager (sps) Graph (sps) Speedup
256 109,977 186,546 1.70×
1,024 431,504 661,987 1.53×
4,096 1,252,303 1,596,248 1.27×

Graph node composition

Nodes memAlloc Replay time
Before this PR 282 24 7.1 ms (0.33× eager)
After this PR 234 0 1.4 ms (1.7× eager)

End-to-end training throughput (16,384 envs, Unitree G1)

Configuration sps vs H200
Upstream AMD baseline 72,998 64%
This PR (Opt A + HG) 165,642 144%
This PR + solver PR (Opt B+C) 166,451 145%
H200 reference (Prabhu Kuttiyam, AMD) 114,905 100%

Validation (11/11 checks pass, AMD MI325X ROCm 7.2)

  • put_data pre-creates _stream_collision + _stream_secondary
  • Zero memAlloc nodes in graph (was 24) ✅
  • Graph step no NaN ✅
  • Graph matches eager step (max_diff = 5.94e-08) ✅
  • Graph replay 1.7× faster than eager ✅

Dependencies

Companion PR to NVIDIA/warp #1702 for three HIP platform bug fixes required by this PR:

  • hipMalloc(0) returns NULL on ROCm → 1-byte minimum for zero-size arrays
  • zero_() on zero-size array raises hipErrorInvalidValue → size guard
  • Mempool auto-enable conflicts with PyTorch ROCm → WARP_DISABLE_MEMPOOLS_FOR_TORCH opt-out

Related

Makes wp.ScopedCapture() + mjw.step() + wp.capture_launch() work correctly
on AMD MI300X/MI325X (ROCm 7.x) without any environment variables.

Three changes enable zero-overhead graph replay:

1. solver.py: cache solver context on Data._solver_ctx on first call.
   On HIP/ROCm, temporarily disable memory pool during allocation so buffers
   use hipMalloc (stable addresses) not hipMallocAsync.  hipMallocAsync
   pointers appear as memAlloc nodes inside a hipGraph and re-execute on every
   replay, adding ~5ms overhead per step.  On CUDA this code path has no effect.

2. smooth.py: same fix for tendon scratch buffers (ten_Jdot, ten_bias_coef).
   Both are wp.zeros() calls that fire inside graph capture on HIP, adding
   two more memAlloc/memFree node pairs.  Cached on Data with stable hipMalloc.

3. forward.py + io.py: optional multi-stream parallelism in fwd_position.
   put_data() pre-creates two dedicated streams (stream_collision,
   stream_secondary).  fwd_position() uses them to run collision detection
   and mass-matrix kinematics concurrently via fork-join.
   The join uses stream.record_event / stream.wait_event (GPU-side events,
   capturable as graph dependency edges) rather than synchronize_stream
   (CPU-blocking, raises RuntimeError inside graph capture).
   Falls back to sequential execution when sleep is enabled or streams
   are unavailable.

Measured on AMD MI325X (gfx942, ROCm 7.2), humanoid model, 256 worlds:
  Before: graph replay 7.1 ms/step (0.33x slower than eager 2.4 ms)
  After:  graph replay 1.4 ms/step (1.7x faster than eager 2.4 ms)

Usage (unchanged from upstream convention):
  with wp.ScopedCapture() as cap:
    mjw.step(model, data)
  wp.capture_launch(cap.graph)  # 1.7x faster on AMD ROCm
Provides a mempool-aware wrapper around wp.ScopedCapture() that works
correctly with all Warp versions on AMD ROCm:

    with mjw.hip_graph_capture() as cap:
        mjw.step(model, data)
    wp.capture_launch(cap.graph)  # ~1.7x faster on AMD ROCm

On HIP/ROCm: auto-enables mempool before ScopedCapture (required for
hipGraph capture) and restores pool state after. Some Warp versions
disable mempool globally in put_data() for ROCm 7.2 stability; this
helper ensures capture always has a compatible memory state.

On CUDA: behaves identically to wp.ScopedCapture() (no-op wrapper).

Note: wp.ScopedCapture() also works directly if the caller manages
mempool state manually.
Takes model+data, runs warmup_steps (default 3) eager steps before
capture to trigger all lazy wp.zeros/wp.empty calls (solver context,
tendon scratch, RK4 buffers, collision structures, etc.).

After warmup these are cached on Data and do not fire during capture,
ensuring the graph has zero memAlloc nodes and replays at full speed.

Also enables mempool on HIP before capture and restores state after.

Updated signature: mjw.hip_graph_capture(model, data, device=None, warmup_steps=3)
…m graph

Multi-stream fork-join benefits eager execution but adds event-record/wait
nodes to the captured graph. On ROCm 7.2, these event nodes add overhead
during replay. During ScopedCapture (wp.get_device().is_capturing=True),
use the sequential single-stream path to produce a minimal graph with only
kernel nodes. Multi-stream continues to benefit eager execution.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant