Skip to content

Initial integration of a4w4 GEMM#663

Draft
Micky774 wants to merge 1 commit into
devfrom
zain/aiter/a4w4-integration
Draft

Initial integration of a4w4 GEMM#663
Micky774 wants to merge 1 commit into
devfrom
zain/aiter/a4w4-integration

Conversation

@Micky774

@Micky774 Micky774 commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Description

Companion and integration point to ROCm/aiter#4124

Fixes # (issue)

Type of change

  • Documentation change (change only to the documentation, either a fix or a new content)
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Infra/Build change
  • Code refactoring

Changes

Please list the changes introduced in this PR:

  • Change A
  • Change B

Checklist:

  • I have read and followed the contributing guidelines
  • The functionality is complete
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

@Micky774 Micky774 added the ci-level 3 CI test level 3 label Jul 7, 2026
@Micky774 Micky774 changed the title Initial integration of a4w4 GEMM and QoLA version bump Initial integration of a4w4 GEMM Jul 7, 2026
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

Claude Walkthrough

Intent. Initial integration of the AITER a4w4 (FP4 × FP4) GEMM into TE, wired through a new torch-free C++ backend instead of the aiter Python package's torch-linked ops. Companion to ROCm/aiter#4124. The GEMM itself now runs through libtransformer_engine on ROCm/gfx950; the aiter package is still used only for the weight/scale shuffle_weight helper.

Key changes.

  • New torch-free wrapper library aiter_gemm at transformer_engine/common/aiter_gemm/ — thin executor for CK-blockscale and ASM (f4gemm) a4w4 kernels (aiter_gemm.hpp:36, src/aiter_gemm_a4w4.cpp:60).
  • New NVTE C API in transformer_engine/common/include/transformer_engine/aiter_gemm.h and transformer_engine/common/gemm/aiter_a4w4_gemm.cpp — includes stub symbols when USE_AITER_GEMM is off so non-gfx950 builds still link.
  • Build wiring: new CMake option USE_AITER_GEMM (default ON), gated to gfx950 in transformer_engine/common/CMakeLists.txt:587-598; new env var NVTE_AITER_GEMM in setup.py:99-105.
  • QoLA-driven checkout/build of the AITER kernel .sos (te_libgemm_a4w4_blockscale.so, te_libgemm_a4w4_asm.so) via aiter_gemm/CMakeLists.txt + qola_manifest.toml; QoLA submodule bumped and pinned AITER commit updated to 773c8f6….
  • PyTorch bindings: new tex.gemm_a4w4_blockscale / tex.gemm_a4w4_asm (pytorch/csrc/extensions/gemm.cpp:828-899, extensions.h:173-180, pybind.cpp:185-191).
  • _fp4_gemm_core in pytorch/cpp_extensions/gemm.py:174 now dispatches through the new tex entry points instead of aiter.gemm_a4w4_asm / aiter.ops.gemm_op_a4w4.gemm_a4w4_blockscale.

Walkthrough.

common/aiter_gemm/ (new): Self-contained sub-project that produces libaiter_gemm.so, wrapping AITER's torch-free a4w4 kernels. The public header (aiter_gemm.hpp) intentionally holds no AITER/QoLA includes — it exposes only a TensorDesc POD and two hipError_t-returning entry points, so libtransformer_engine.so never sees AITER's headers. The .cpp translates TensorDescaiter_tensor_t and forwards to QOLA_NS(gemm_a4w4_blockscale) / QOLA_NS(gemm_a4w4_asm), catching exceptions and returning hipErrorUnknown on failure. Kernel selection and weight-shuffling are not done here; the caller passes an already-resolved kernel_name and pre-shuffled tensors.

aiter_gemm/CMakeLists.txt (new): Restricts arches to gfx950 (a4w4 asm blobs ship only for that), parses qola_manifest.toml for the pinned AITER commit, then either (a) points at a pre-built tree via AITER_GEMM_PATH, or (b) invokes python -m qola.cli checkout + qola.cli build to fetch/build the two kernel .sos. NVTE_AITER_SOURCE_DIR lets developers point at a local AITER checkout and skip the QoLA checkout step. The resulting shared libs are linked in via -l:te_libgemm_a4w4_blockscale.so -l:te_libgemm_a4w4_asm.so with $ORIGIN rpath, and installed alongside libtransformer_engine.so.

common/gemm/aiter_a4w4_gemm.cpp (new) + include/transformer_engine/aiter_gemm.h (new): NVTE-style C API layer (nvte_aiter_gemm_a4w4_blockscale / nvte_aiter_gemm_a4w4_asm) that converts NVTEAiterGemmTensoraiter_gemm::TensorDesc. Under #ifdef USE_AITER_GEMM it delegates to the wrapper lib; when the backend is disabled, the same symbols are defined as stubs returning -1, so downstream link steps and framework bindings work unchanged on non-gfx950.

common/CMakeLists.txt: Adds the USE_AITER_GEMM option, always compiles gemm/aiter_a4w4_gemm.cpp into libtransformer_engine.so, but only descends into aiter_gemm/ (and links aiter_gemm + defines USE_AITER_GEMM) when gfx950 is in CMAKE_HIP_ARCHITECTURES. A default-ON USE_AITER_GEMM therefore does not break gfx942/gfx1250 builds — those get a warning and the stubs.

setup.py: Translates NVTE_AITER_GEMM (default 1) into -DUSE_AITER_GEMM=ON/OFF on the CMake invocation.

pytorch/csrc/extensions/*: Two new pybind entry points that pack at::Tensors into NVTEAiterGemmTensor (fixing dtype tags per operand — FP4x2 inputs, E8M0 scales, out dtype inferred from at::kBFloat16 / at::kHalf), release the GIL via NVTE_SCOPED_GIL_RELEASE, and NVTE_CHECK on non-zero return. bias on the ASM path is an std::optional<at::Tensor> and only forwarded when defined.

pytorch/cpp_extensions/gemm.py: _fp4_gemm_core no longer imports aiter.gemm_a4w4_asm or aiter.ops.gemm_op_a4w4.gemm_a4w4_blockscale; both branches now call the new tex.gemm_a4w4_* executors. Semantics preserved: the CK path still uses log2_k_split as split-K, the ASM path still passes bpreshuffle=True, alpha=1.0, beta=0.0. The one call-site tweak is that the ASM path used to return a fresh tensor from AITER; the new path writes into the pre-allocated out_hp, and the function now returns out_hp directly.

3rdparty/QoLA + ck_fused_attn/qola_manifest.toml: Submodule bumped to b9ee3749; the CK-fused-attn manifest's aiter_commit moved to 773c8f6… to stay in sync with the new a4w4 manifest.

Testing. No new tests in this PR. The existing FP4 GEMM code paths in _fp4_gemm_core are exercised via the existing PyTorch tests, but this PR does not add a dedicated a4w4 executor test. On non-gfx950 arches the stub path returns non-zero, which will trip NVTE_CHECK at call-time rather than at link-time.

Notes for reviewers.

  • gfx950-only by design: the CMake logic silently drops the backend on other arches; the stubs mean the Python entry points exist but will raise on invocation. Worth double-checking that no non-gfx950 CI job now hits _fp4_gemm_core.
  • The a4w4 .sos are consumed via -l:te_libgemm_a4w4_*.so with $ORIGIN rpath and installed to transformer_engine/lib. Wheels/redistributables will need those two extra .sos alongside libtransformer_engine.so.
  • Depends on torch-free a4w4 GEMM + C++ library build aiter#4124 landing (torch-free a4w4 entry points + QOLA_NS macros); the pinned commit 773c8f6… presumably contains that work.
  • Python-side kernel selection is now the source of truth for kernel_name — the C wrapper does not do any heuristic; an empty string is forwarded and interpreted by AITER as "use default".
  • The ASM path's return-value semantics changed subtly (write-into-out vs. return-new); if any caller besides _fp4_gemm_core relied on the old aiter return-tensor behavior, it will now see a stale/wrong result. Grep shows only _fp4_gemm_core as the caller.

Generated by Claude. To request a code review, comment /claude review.

NVTEAiterGemmTensor bias_t{};
NVTEAiterGemmTensor *bias_ptr = nullptr;
if (bias.has_value() && bias->defined()) {
bias_t = make_aiter_gemm_tensor(*bias, kNVTEAiterGemmFP32);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The bias dtype is hard-coded to kNVTEAiterGemmFP32 regardless of the actual at::Tensor scalar type. Today mxfp4_gemm only ever calls this with bias=None, so this branch is unreachable — but if a future caller passes a bf16/fp16 bias, AITER's ASM kernel will reinterpret the byte pattern as fp32 and produce silently wrong results.

Two low-cost hedges:

  • NVTE_CHECK(bias->scalar_type() == at::kFloat, "AITER a4w4 GEMM bias must be fp32");
  • or derive the dtype from bias->scalar_type() the same way aiter_gemm_out_dtype does for the output tensor.

Either is fine; leaving the current line silently mis-typing is the concerning shape.

Comment on lines +48 to +52
for (int i = 0; i < d.ndim; ++i) {
t.shape[i] = d.shape[i];
t.strides[i] = d.strides[i];
numel *= static_cast<size_t>(d.shape[i]);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

d.shape / d.strides are fixed-size int64_t[8] in TensorDesc, but this loop trusts the caller-supplied d.ndim without clamping. If d.ndim > 8 (unlikely for a4w4 GEMM but possible via the public C API), we'll OOB-read past the descriptor and write past t.shape / t.strides in aiter_tensor_t. The NVTE C API wrapper (aiter_a4w4_gemm.cpp::to_desc) already clamps to 8; matching that here would make the wrapper library equally safe against a raw C API caller:

Suggested change
for (int i = 0; i < d.ndim; ++i) {
t.shape[i] = d.shape[i];
t.strides[i] = d.strides[i];
numel *= static_cast<size_t>(d.shape[i]);
}
size_t numel = (d.ndim > 0) ? 1 : 0;
const int loop_ndim = d.ndim < 8 ? d.ndim : 8;
for (int i = 0; i < loop_ndim; ++i) {
t.shape[i] = d.shape[i];
t.strides[i] = d.strides[i];
numel *= static_cast<size_t>(d.shape[i]);
}

Comment on lines 201 to 205
if use_ck:
result = gemm_a4w4_blockscale(
tex.gemm_a4w4_blockscale(
A_fp4, B_shuffled, A_scales_uint8, B_scales_uint8, out_hp,
splitK=log2_k_split,
split_k=log2_k_split, kernel_name=kernel_name,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Behavior change worth being deliberate about: the pre-PR call site invoked gemm_a4w4_blockscale(..., splitK=log2_k_split) without kernel_name, so on the CK branch AITER's own heuristic picked the kernel. This PR now forwards kernel_name (the tuned-CSV lookup). Per the wrapper header docstring, "non-empty must exist in the compiled registry" — so if the tuned CSV holds a stale/renamed CK kernel name that used to be silently ignored, it will now fail loudly at runtime.

If that's the intended fix (tuned CSV wasn't actually reaching CK before), fine — please confirm. Otherwise consider a fall-through (empty kernel_name on miss).

Also nit: the local is still called log2_k_split but the value flowing in is a linear splitK from _select_kernel_fp4 (cfg.get("splitK", 0)). The CK function takes split_k (linear); the ASM function takes log2_k_split (log2). Passing the same variable to both only works if _select_kernel_fp4's output is already backend-appropriate — worth a comment either way to make future misuse harder.

Comment on lines +89 to +103
execute_process(
COMMAND ${CMAKE_COMMAND} -E env "PYTHONPATH=${__QOLA_DIR}:$ENV{PYTHONPATH}"
${Python_EXECUTABLE} -m qola.cli build
--manifest ${__QOLA_MANIFEST}
--aiter-root ${__AITER_SOURCE_DIR}
--output-dir ${__QOLA_BUILD_DIR}
--arch "${GPU_ARCHS_STR}"
--skip-checkout
RESULT_VARIABLE QOLA_BUILD_RESULT)
if(NOT QOLA_BUILD_RESULT EQUAL 0)
message(FATAL_ERROR "[aiter_gemm] QoLA build failed.")
endif()
set(__AITER_GEMM_LIB_PATH "${__QOLA_BUILD_DIR}/lib")
set(__QOLA_INCLUDE_DIR "${__QOLA_BUILD_DIR}/include")
endif()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The QoLA build runs via execute_process at CMake configure time. CMAKE_CONFIGURE_DEPENDS on qola_manifest.toml (line 44) covers manifest edits, but a modified AITER source tree (e.g. NVTE_AITER_SOURCE_DIR pointing at a local checkout that the developer is iterating on) will not trigger a rebuild — only re-running cmake .. will. The aiter_gemm shared library also has no add_dependencies link back to the QoLA output, so .sos that get replaced under ${__QOLA_BUILD_DIR}/lib do not cause relinking.

For a first integration this is likely acceptable, but a follow-up switching to ExternalProject_Add (or a custom target with DEPENDS/BYPRODUCTS on the te_libgemm_a4w4_*.sos) would make iterative development on gfx950 far less error-prone. Worth at least a TODO comment.

Comment on lines +78 to +92
#else // !USE_AITER_GEMM

extern "C" int nvte_aiter_gemm_a4w4_blockscale(const NVTEAiterGemmTensor *, const NVTEAiterGemmTensor *,
const NVTEAiterGemmTensor *, const NVTEAiterGemmTensor *,
const NVTEAiterGemmTensor *, int, const char *, void *) {
return -1;
}

extern "C" int nvte_aiter_gemm_a4w4_asm(const NVTEAiterGemmTensor *, const NVTEAiterGemmTensor *,
const NVTEAiterGemmTensor *, const NVTEAiterGemmTensor *,
const NVTEAiterGemmTensor *, const NVTEAiterGemmTensor *,
const char *, float, float, int, int, void *) {
return -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.

The "framework layer degrades gracefully" comment at the top of the file (line 11) is a bit optimistic given how these stubs are consumed on the pytorch side: gemm.cpp unconditionally calls NVTE_CHECK(rc == 0, "nvte_aiter_gemm_a4w4_blockscale failed (rc=", rc, ")") and throws. A user building on gfx942 who accidentally exercises the a4w4 path will see a rather cryptic rc=-1 error rather than a "a4w4 GEMM not built for this arch" message.

Two options that would materially improve the experience with almost zero code:

  • use a distinctive negative code (e.g. -2 = "backend not compiled in") plus a matching branch in gemm.cpp's check that produces a targeted error, or
  • expose a build-state query (extern "C" int nvte_aiter_gemm_a4w4_available();) so the pytorch layer can guard the entry points and give the framework a hasattr-like probe from Python.

Not a blocker; landing this as-is is fine so long as the intent is understood.

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

Claude review

Reviewed the initial a4w4 (FP4) GEMM integration + QoLA/AITER version bump. Diff scope: 14 files, +652/-10, entirely ROCm-side (all C++ additions guarded by #ifdef USE_ROCM or if(USE_ROCM); CUDA execution path is untouched). Upstream compatibility is clean.

Verdict: looks close to landable. No blockers; five inline notes on latent correctness/UX rough edges:

  • gemm.cpp:888 — bias tensor dtype is hard-coded to kNVTEAiterGemmFP32 regardless of the actual at::Tensor scalar type. Dead code today (bias=None in _fp4_gemm_core) but a silent-wrong-answer landmine if a future caller passes bf16/fp16 bias.
  • aiter_gemm_a4w4.cpp:48-52to_aiter_tensor trusts caller-supplied ndim without clamping to the fixed-size shape[8]. The NVTE C API wrapper already clamps; matching that here closes an OOB-read hole.
  • gemm.py:201-205 — CK branch now forwards kernel_name (pre-PR it didn't). Behavior change worth confirming: a stale entry in the tuned CSV will now fail loudly instead of silently falling back to AITER's heuristic. Also log2_k_split naming is misleading — the variable actually holds a linear splitK.
  • aiter_gemm/CMakeLists.txt:89-103 — QoLA build runs at configure-time via execute_process; iterative AITER source edits won't rebuild until CMake is re-run. Fine for initial integration; worth a TODO toward ExternalProject_Add/proper DEPENDS.
  • aiter_a4w4_gemm.cpp:78-92 — the "graceful degradation" comment doesn't match reality: stubs return -1, pytorch NVTE_CHECKs and throws a cryptic error on non-gfx950 arches. Distinguishable code + targeted message, or a build-state query, would help.

Copyright headers: OK (all modified files preserve NVIDIA headers unchanged and carry AMD end-year 2026; new ROCm-only files follow the existing AMD-only "License for AMD contributions = MIT" convention).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-level 3 CI test level 3

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants