Skip to content

Bulk AG Overlap for bf16 on gfx950 - #713

Open
alextmagro wants to merge 1 commit into
devfrom
hipkittens_fused_bulkag
Open

Bulk AG Overlap for bf16 on gfx950#713
alextmagro wants to merge 1 commit into
devfrom
hipkittens_fused_bulkag

Conversation

@alextmagro

@alextmagro alextmagro commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Enables fused wgrad AG + dgrad GEMM for BF16 + gfx950.

Adds additional test coverage and layer tests, and makes fused/bulk_fused the default overlap scheme for gfx950, with fallback to no overlap at the moment for unaligned shapes, dtypes, and reduce-scatter.

1.282x Geomean speedup for wgrad AG + dgrad GEMM step

_assert_numerics_passed(result)
fused = _reported_names(result.stdout, "UB FUSED NAMES: ")
assert fused is not None, f"harness printed no fused name set\n{result.stdout.decode()}"
assert "qkv_dgrad" in fused, fused

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assertion can't fail if the fused bulk dgrad path stops being taken.

_ub_fused_names is populated in add_ub() purely from the method config (method == "fused" and the region wasn't disabled). It says nothing about whether fused_bulk_ag_eligible() returned True for the call, or whether fused_overlap_bulk_ag ever ran. So if a shape-math regression in _fused_gemm_dims / _fused_gemm_shape_ok silently downgraded ub_bulk_dgrad to False, this test would still pass: numerics are correct on the fallback path, and qkv_dgrad is still in fused.

The gap is visible in the pair of tests: test_fused_layer_declines_ineligible_k (line 608) asserts the identical condition — "qkv_dgrad" in fused and not in disabled — while expecting the opposite outcome. The two tests can't distinguish "kernel ran" from "kernel declined", which is the property this PR is adding.

Consider having run_layer_with_overlap.py report the post-gate decision (e.g. print the effective ub_bulk_dgrad per layer after fused_bulk_ag_eligible) and assert on that instead, so the positive test fails when the path is not taken.

Comment thread tests/pytorch/distributed/test_comm_gemm_overlap.py
Comment on lines +615 to +616
"""A Userbuffers region the fused backend cannot serve declines at setup."
"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stray " at the end of the first line — the docstring text ends with ... declines at setup." and then closes on the next line. It parses fine, but the quote looks like a typo.

Suggested change
"""A Userbuffers region the fused backend cannot serve declines at setup."
"""
"""A Userbuffers region the fused backend cannot serve declines at setup."""

Comment on lines +482 to +483
dist_print("UB FUSED NAMES: " + " ".join(sorted(te.module.base._ub_fused_names)))
dist_print("UB DISABLED NAMES: " + " ".join(sorted(te.module.base._ub_disabled_names)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two dist_print calls have no src= argument, so every rank prints its own copy of the name lists. The consumer (_reported_names in test_comm_gemm_overlap.py) just takes the first substring match, so the duplicates are harmless but noisy — and if ranks ever disagreed, the test would silently key off whichever line landed first. Since the registered UB names are identical on all ranks, src=0 gives the same information with one line.

if not IS_HIP_EXTENSION:
return True
if not _ub_is_fused(name):
return False

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This early-out has the opposite polarity from its near-identical sibling fused_ag_gemm_eligible, which returns True for a non-fused name ("not our backend, don't veto") while this one returns False. The difference is deliberate and load-bearing — the caller uses this to enable the bulk path, so a non-fused region must decline — but the two functions sit next to each other with the same shape, and a future reader "fixing the inconsistency" would silently turn bulk dgrad on for every ROCm UB region. Worth a one-line comment stating that this predicate is opt-in whereas the other is opt-out.

Comment thread transformer_engine/pytorch/module/base.py
"fused bulk AG reached with a non-bf16 operand");
NVTE_CHECK(ubuf.numel() != 0, "fused bulk AG reached without a gather destination");

const size_t m = A.size(1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m here comes from the GEMM operands, but the gather geometry is derived from chunk (the Userbuffers region), and nothing checks that chunk.size(0) * _tp_size equals the GEMM's row count. In fused_overlap_ag this can't drift because the ubuf is the A operand; in the bulk variant the gathered tensor and the dgrad GEMM are completely unrelated tensors that only happen to share a row count today (both are the layer input's sequence dim). The Python gate enforces the alignment rules but never compares the two.

If they ever disagree — a UB region registered with a different shape than the layer actually runs at, which the Python-side eligibility check doesn't catch — the gather writes a differently-sized region than the kernel grid was sized for, silently. An NVTE_CHECK that the ubuf rows match the GEMM rows would both document the coupling and turn a silent corruption into an error.

Comment thread transformer_engine/common/gemm/kittens/cdna4/fused_ag_gemm.cpp
@github-actions

Copy link
Copy Markdown

Reviewed the full diff of this PR against the base tip (16 files, +443/-63): the hipKittens bulk AG+GEMM kernel path (BULK template parameter, persistent_bulk_ag_bf16_gemm, run_bulk_nn), the fused_overlap_bulk_ag C++ entry point and its gemm.cpp dispatch, the Python eligibility gating in base.py, the ub_bulk_dgrad default flip in transformer.py, and the distributed test additions.

Overall the approach looks sound — the BULK specialization cleanly reuses the existing persistent GEMM body while redirecting the gather destination and compiling out the per-tile spin-wait that a bulk overlap doesn't need, and the pybind change correctly fixes reset_fused_ag_gemm_cache having been registered only under #ifndef USE_ROCM, which would have made destroy_ub() raise on ROCm.

Left 7 inline comments. The substantive ones are the new tests not actually distinguishing "fused kernel ran" from "fused kernel declined" (the reported name list reflects setup config, not the runtime gate), an unchecked shape coupling between the Userbuffers region and the dgrad GEMM in hk_bulk_ag_gemm, and a dtype-check relaxation on the non-bulk path that may be unintentional. The rest are nits.

Copyright headers: OK.

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