Skip to content

feat(ck-tile): bquant GEMM TE to dispatcher bridge#9982

Open
ozturkosu wants to merge 8 commits into
developfrom
users/muozturk/ck/bquant_bridge_block_scale
Open

feat(ck-tile): bquant GEMM TE to dispatcher bridge#9982
ozturkosu wants to merge 8 commits into
developfrom
users/muozturk/ck/bquant_bridge_block_scale

Conversation

@ozturkosu

Copy link
Copy Markdown
Contributor

Motivation

Adds the (non-grouped) gemm_bquant block-scale op (B is the quantized operand) to the
TileEngine -> Dispatcher bridge (codegen -> C ABI -> Python). Direct-launch, registry-bypass
(ck_tile::QuantGemmHostArgs).

Note — this is NOT a re-alias of the merged grouped_gemm_bquant bridge. That bridge is
mis-named: it references the same plain 38_block_scale_gemm/gemm_bquant_quantgrouped_*.cpp
examples (quant-grouped scales, single GEMM problem); the real grouped GEMM lives in
17_grouped_gemm. This PR ships a correctly-named gemm_bquant bridge with its own
KERNEL_NAME namespace (NAME_PREFIX="gemm_bquant"), leaving the existing bridge byte-identical
(the shared make_bquant_kernel_name gained an optional name_prefix with the old default).

Test Plan

  • CPU unit tests: python3 -m pytest dispatcher/tests/test_bquant_bridge.py -v
  • On-GPU numeric verify over dtype x phase vs fp32 reference (tester, serialized; MX is gfx950-only).
  • clang-format-18 on the ctypes lib.

Test Result

  • CPU unit tests: 9 passed (distinct gemm_bquant prefix, byte-exact codegen<->utils name
    contract across base+preshuffle+MX, microscale-pipeline scope, codegen-JSON projection).
  • Codegen + build (gfx950, hipcc 7.2): 19/19 default configs pass the self-test (name
    byte-exactness, QuantType, arch-guard, preshuffle-flag consistency); hipcc smoke-compiled fp8
    (compv3), preshuffleb, preshufflebq, and MX bf16fp4 to gfx950 objects. Existing grouped bridge
    unaffected (64/64 tests pass).
  • clang-format-18: clean on gemm_bquant_ctypes_lib.cpp.
  • On-GPU numeric + perf parity: pending (draft) — posted as a comment after the GPU run.

Scope / known limitations (exact Old-TE match)

rcr = RowMajor A, ColMajor B, RowMajor C, ColMajor BQ:

variant A / B / C / Q phases arch
fp8 fp8/fp8/half/float none, psb, psbq, psb+psbq gfx942+gfx950
bf8 bf8/bf8/half/float same gfx942+gfx950
fp8i4 fp8/pk_int4/half/fp8 same gfx942+gfx950
bf8i4 bf8/pk_int4/half/bf8 same gfx942+gfx950
mx_bf16bf16 bf16/bf16/bf16/e8m0 microscale gfx950-only
mx_bf16bf8 bf16/bf8/bf16/e8m0 microscale gfx950-only
mx_bf16fp4 bf16/pk_fp4/bf16/e8m0 microscale gfx950-only

BQ is ColumnMajor (the WPQuantB preshuffleb pipeline static_asserts "Bq must be col major";
Old-TE's rcr path passes bq_layout=Col) — set across codegen + ctypes stride + preshuffle
host-tensor + Python stride_BQ/fortran-order. MX kernels #error without CK_GFX950_SUPPORT.
k_batch == 1.

Reviewer note: the existing grouped_gemm_bquant bridge may carry a latent RowMajor-BQ bug for
its preshuffleb configs (out of scope here; flagging for a follow-up).


Related PRs / references (TileEngine -> Dispatcher GEMM bridge series): #8997 (regular), #9000 (grouped), #9028 (stream-K), #8887 (fp8/bf8/int8), #9305 (multi-ABD), #9306 (batched), #9307 (preshuffle), #9308 (multi-D), #9328 (batched-contraction), #9329 (mx_gemm), #9978 (tensor_quant), #9979 (rowcolquant), #9980 (aquant), #9981 (abquant). Sibling block-scale quant bridges (this series): abquant, aquant, bquant, rowcolquant, tensor_quant.

… bridge

Add a registry-bypass, single-kernel-per-.so, direct-launch ctypes bridge for
the plain (non-grouped) B-only quantized block-scale GEMM operator under
example/ck_tile/38_block_scale_gemm. This is distinct from the merged
grouped_gemm_bquant (17_grouped_gemm, multi-problem) bridge.

Deliverables:
  - codegen/unified_gemm_bquant_codegen.py  (name prefix "gemm_bquant")
  - bindings/ctypes/gemm_bquant_ctypes_lib.cpp
  - python/gemm_bquant_utils.py
  - CMake target dispatcher_gemm_bquant_lib (doubly guarded)
  - tests/test_gemm_bquant_selftest.py (CPU self-test + optional GPU run)

Dtype x layout matches Old-TE exactly (rcr): fp8, bf8, fp8i4, bf8i4 (decode
compv3) and MX bf16bf16/bf16bf8/bf16fp4 (e8m0 microscale, gfx950-only via
#error guard), each across non-preshuffle / preshuffleb / preshufflebq /
preshuffleb+preshufflebq. BQ is ColumnMajor to match Old-TE's rcr path and the
WPQuantB pipeline's col-major requirement.

Arch is derived at launch (get_arch + throw on unknown, no gfx942 default);
MX kernels #error without CK_GFX950_SUPPORT. Codegen KERNEL_NAME is byte-exact
with BQuantKernelConfig.name via a shared name_prefix param in codegen_common.

Validated without GPU: 19/19 configs pass CPU self-test; fp8, preshuffleb,
preshufflebq and MX fp4 kernels compile to gfx950 objects via hipcc.
@ozturkosu

Copy link
Copy Markdown
Contributor Author

Reviewer pass (bquant bridge)

Reviewed git diff develop..HEAD on users/muozturk/ck/bquant_bridge_block_scale (non-grouped gemm_bquant TE->Dispatcher bridge). Verified locally: test_bquant_bridge.py 9/9 pass, test_gemm_bquant_selftest.py 19/19 pass, and the grouped bridge test_grouped_gemm_bquant_utils.py still 64/64 pass — the shared-helper change is safe.

Shared-helper safety (the key risk): CONFIRMED SAFE. codegen/codegen_common.py:413 adds name_prefix="grouped_gemm_bquant" (default unchanged). unified_grouped_gemm_bquant_codegen.py:212 calls make_bquant_kernel_name without the new kwarg, so grouped names are byte-identical. The new bridge passes name_prefix="gemm_bquant" (unified_gemm_bquant_codegen.py:236, gemm_bquant_utils.py:120). No other shared-helper drift.

Parity: CONFIRMED EXACT. The 19 config factories map 1:1 to the 19 Old-TE gemm_bquant_quantgrouped_*.cpp files (4 base fp8/bf8/fp8i4/bf8i4 x {none, preshuffleb, preshufflequant, preshuffleb+preshufflequant} + 3 mx). No mx preshuffle files exist in Old-TE and the bridge emits none — correct. MX quant_group_k defaults (bf16bf16=32, bf16bf8=128, bf16fp4=32) match the first branch of each Old-TE .cpp. BQ ColumnMajor is consistent end-to-end: codegen BQLayout (unified_gemm_bquant_codegen.py:270), ctypes stride_BQ==QK_B validation + col-major host tensor (gemm_bquant_ctypes_lib.cpp:187,261), Python asfortranarray + stride_BQ=QK_B (gemm_bquant_utils.py:302,393). QuantGemmHostArgs correct: bq_ptr set, aq_ptr=nullptr, QK_A=0, stride_AQ=0 (gemm_bquant_ctypes_lib.cpp:299-311). pk_int4/fp4 byte sizing via elements_to_bytes<QDataType>. permute_n de-riffle handled (gemm_bquant_utils.py:414-427). Registry-bypass direct-launch matches grouped template. clang-format-18 clean.

Blocking

  • None. Build/GPU parity is out of scope for this read-only pass, but the codegen/ctypes/stride/name contracts are self-consistent and Old-TE-equivalent.

Should-fix

  • python/gemm_bquant_utils.py:437-450 _detect_gpu_arch() silently falls back to gfx950 (return _DEFAULT_GFX_ARCH) when rocm_agent_enumerator is missing/empty. The arch policy is get_arch + throw, no hardcoded default. A silent gfx950 fallback will mis-target on a gfx942 host (OCP-fp8/MX defines flip at :535-538). Raise on undetectable arch instead of defaulting.
  • bindings/ctypes/CMakeLists.txt (new block) falls back to gfx942 for CK_TILE_BQUANT_GFX_ARCH when CMAKE_HIP_ARCHITECTURES is unset. Same no-gfx942-default concern. For non-MX kernels this silently builds the wrong arch (MX kernels are saved by the #error CK_GFX950_SUPPORT guard, which is correct). Prefer erroring when arch is unresolved.
  • No Python-side guard that MX variants require gfx950 — correctness relies solely on the C++ #error at compile time. Consider raising in setup_multiple_bquant_dispatchers when variant_key in _MX_VARIANTS and "gfx950" not in arch for a clearer failure than a hipcc #error.

Nit

  • gemm_bquant_ctypes_lib.cpp:123,302 accepts k_batch but only k_batch==1 is exercised/validated; split-K silent-wrong is a known bquant trap. A guard rejecting k_batch>1 (until a split-K kernel is wired) would be safer than passing it through.
  • python/gemm_bquant_utils.py:60 _DEFAULT_GFX_ARCH="gfx950" used as the default kwarg for every default_*_config(..., gfx_arch=_DEFAULT_GFX_ARCH) — harmless for name generation but reinforces the silent-default pattern flagged above.

@therock-pr-bot

therock-pr-bot Bot commented Jul 24, 2026

Copy link
Copy Markdown

✅ All Checks Passed — Ready for Review

Check Status Details
🌿 Branch Name ✅ Pass
📝 PR Title/Description ✅ Pass
Forbidden Files ✅ Pass
🧪 Unit Test ✅ Pass
🔎 pre-commit ✅ Pass
🚫 Draft PR 🔜 To Be Enabled
🚩 Feature Flag 🔜 To Be Enabled
📊 Code Coverage 🔜 To Be Enabled
🤖 therock-pr-bot ✅ Pass

🎉 All checks passed! This PR is ready for review.

📖 Need help? See the Policy FAQ for details on every check and how to fix failures.

@therock-pr-bot

Copy link
Copy Markdown

🎉 All checks passed! This PR is ready for review.

Reviewer round-2 (non-grouped gemm_bquant bridge, PR #9982):

- _detect_gpu_arch() now RAISES when the enumerator is missing/fails or
  reports no gfx target, instead of silently defaulting to gfx950 (which
  mis-targets gfx942 hosts and violates get_arch+throw). Explicit gfx_arch=
  still short-circuits detection. Dropped the silent _DEFAULT_GFX_ARCH build
  default; a name-only placeholder is used solely for KERNEL_NAME construction.
- CMakeLists: an unresolved GPU arch for the (plain and grouped) BQuant ctypes
  targets is now a hard FATAL_ERROR rather than a silent gfx942 default, since
  non-MX kernels lack the source-side #error guard.
- Added a Python-side MX guard (_require_mx_arch) so mx_bf16bf16/bf16bf8/bf16fp4
  fail early with a clear message on non-gfx950 arch, wired into the build path.
- Reject k_batch > 1 in the runner (only k_batch == 1 is validated) so split-K
  can never be silently wrong.
- Tests: added MX-gfx950 guard and k_batch>1 rejection coverage (13 pass;
  grouped bridge 64/64 unchanged).
@ozturkosu

Copy link
Copy Markdown
Contributor Author

Round-2 fixes

Addressed the arch-safety / scope-hardening feedback (no blocking bugs; parity + shared-helper safety unchanged). Commit 5476a67.

SHOULD-FIX

  1. Silent arch fallback (gemm_bquant_utils.py): _detect_gpu_arch() now raises RuntimeError when rocm_agent_enumerator is missing/fails or reports no gfx target, instead of silently returning gfx950. Explicit gfx_arch= still short-circuits detection. Dropped the silent _DEFAULT_GFX_ARCH="gfx950" build default; a clearly-named name-only placeholder is now used solely for KERNEL_NAME construction, and the build path (setup_multiple_bquant_dispatchers) requires a real arch (detected-or-raise / explicit). Verified on a gfx942 host: detection now correctly returns gfx942.
  2. CMake default arch (bindings/ctypes/CMakeLists.txt): an unresolved GPU arch for the plain and grouped BQuant ctypes targets is now a hard FATAL_ERROR (get_arch+throw) rather than a silent gfx942 default, since non-MX kernels are not protected by the source #error. Arch is still resolved generically from CMAKE_HIP_ARCHITECTURES / -DCK_TILE_BQUANT_GFX_ARCH; the Python path continues to pass -DGFX_ARCH= explicitly via hipcc.
  3. Python-side MX guard: added _require_mx_arch()mx_bf16bf16 / mx_bf16bf8 / mx_bf16fp4 fail early with a clear message on non-gfx950 arch (wired into the build entry point), rather than relying solely on the C++ #error.

NIT
4. Split-K trap: BQuantGpuGemmRunner.run() now rejects k_batch > 1 with a ValueError (only k_batch == 1 is validated) so split-K can never be silently wrong.

Tests: added MX-gfx950 guard + k_batch>1 rejection coverage. test_bquant_bridge.py 13/13 pass; grouped bridge test_grouped_gemm_bquant_utils.py 64/64 unchanged (codegen_common.py untouched).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a dedicated non-grouped gemm_bquant TileEngine → Dispatcher bridge (codegen → per-kernel .hpp → per-kernel ctypes .so → Python runner), with a distinct kernel-name namespace (NAME_PREFIX="gemm_bquant") that avoids colliding with the existing (misnamed) grouped_gemm_bquant bridge.

Changes:

  • Introduces a new unified_gemm_bquant_codegen.py codegen path plus matching Python utilities (gemm_bquant_utils.py) and ctypes C ABI (gemm_bquant_ctypes_lib.cpp) for direct-launch via ck_tile::QuantGemmHostArgs.
  • Extends shared kernel naming (make_bquant_kernel_name) with an optional name_prefix while preserving the existing default for backward compatibility.
  • Adds CPU-only unit tests and a self-test runner script for validating name parity, codegen projection, and generated header invariants.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
projects/composablekernel/dispatcher/tests/test_gemm_bquant_selftest.py Adds a standalone self-test runner for CPU invariants and an optional GPU sanity check.
projects/composablekernel/dispatcher/tests/test_bquant_bridge.py Adds CPU-only unit tests locking the gemm_bquant naming/contract and config/codegen projection.
projects/composablekernel/dispatcher/python/gemm_bquant_utils.py Implements Python-side config/name contract, build helpers (codegen→hipcc), and a ctypes runner for gemm_bquant.
projects/composablekernel/dispatcher/codegen/unified_gemm_bquant_codegen.py Adds header code generation for non-grouped gemm_bquant kernels (direct-launch).
projects/composablekernel/dispatcher/codegen/codegen_common.py Adds name_prefix to make_bquant_kernel_name with backward-compatible default.
projects/composablekernel/dispatcher/bindings/ctypes/gemm_bquant_ctypes_lib.cpp Adds a new per-kernel ctypes C ABI for direct-launch BQuant GEMM with host-pointer memory model.
projects/composablekernel/dispatcher/bindings/ctypes/CMakeLists.txt Adds a CMake target for building the new gemm_bquant ctypes library when generated headers exist.

Comment on lines +46 to +50
template <typename T>
static constexpr std::size_t elements_to_bytes(std::size_t n)
{
return n * sizeof(T) / ck_tile::numeric_traits<T>::PackedSize;
}
Comment on lines +314 to +326
else()
message(STATUS "No gemm_bquant kernel found for ctypes lib - building without kernel")
add_library(dispatcher_gemm_bquant_lib SHARED gemm_bquant_ctypes_lib.cpp)
target_include_directories(dispatcher_gemm_bquant_lib PRIVATE
${PROJECT_SOURCE_DIR}/include
${PROJECT_SOURCE_DIR}/dispatcher/include
)
target_link_libraries(dispatcher_gemm_bquant_lib PRIVATE hip::device)
set_target_properties(dispatcher_gemm_bquant_lib PROPERTIES
POSITION_INDEPENDENT_CODE ON
CXX_STANDARD 17
)
endif()
Comment on lines +289 to +293
A, B, BQ, C must be numpy arrays (C-contiguous, packed).
B should be a packed (K, N) C-contiguous array -- the kernel interprets
it as column-major via stride_B=K, not via numpy's Fortran-order flag.
C must be the array that will receive output; a non-contiguous C would
produce a temporary copy that is not returned to the caller.
@ozturkosu

Copy link
Copy Markdown
Contributor Author

GPU validation (bquant, MI350X/gfx950)

Node asrock-1w300-e2-3 (MI350X, gfx950), HIP 7.1, OCP fp8. Bridge built via the Python path (gemm_bquant_utils.setup_multiple_bquant_dispatchers) with a fresh output dir each build (stale-.so guard). Old-TE side = tile_example_gemm_quant (example/ck_tile/38_block_scale_gemm) built from the same worktree. All 19 configs compiled on gfx950 (incl. the 3 MX variants).

Functional correctness (vs numpy quantize→dequantize reference; gate max_rel ≤ 2e-2, MX ~4e-3 floor)

variant none preshuffleb preshufflequant preshuffleb+pq
fp8 PASS 4.7e-4 FAIL 6.7e+1 PASS 5.2e-4 FAIL 6.7e+1
bf8 PASS 4.7e-4 FAIL 6.9e+1 PASS 4.6e-4 FAIL 6.9e+1
fp8i4 FAIL NaN/Inf FAIL NaN/Inf FAIL NaN/Inf FAIL NaN/Inf
bf8i4 FAIL NaN/Inf FAIL NaN/Inf FAIL NaN/Inf FAIL NaN/Inf
MX variant (phase none) max_rel result
mx_bf16bf16 3.8e-3 PASS
mx_bf16bf8 3.7e-3 PASS
mx_bf16fp4 3.8e-3 PASS

Passing: 7/19. fp8/bf8 none+preshufflequant and all 3 MX variants are correct. The BPreshuffleQuant scale-shuffle path is handled correctly by the ctypes lib (shuffle_bq).

BLOCKING — round-N coder fixes
  1. preshuffleB does NOT shuffle the B weight matrix (same class of bug as the abquant sibling). gemm_bquant_ctypes_lib.cpp copies B to device with a plain hipMemcpy regardless of SelectedKernel::PreshuffleB. Old-TE (run_gemm_quant_example.inc:771-783) calls shuffle_b_permuteN<GemmConfig> (when TiledMMAPermuteN && kN==1) or shuffle_b<GemmConfig> on B before the device copy, and additionally bq_permuteN on the scales for the permuteN branch. None of that is done in the bridge → preshuffleb and preshuffleb+preshufflequant families produce garbage (max_rel ≈ 67–69) for both fp8 and bf8. Fix: apply shuffle_b/shuffle_b_permuteN (and bq_permuteN for the permuteN branch) to B/BQ in the ctypes lib when PreshuffleB is set, mirroring the Old-TE ordering.

  2. fp8i4 / bf8i4 (pk_int4 B) are broken in ALL four phases — random input → NaN/Inf, constant-code input → all-zeros. The ctypes lib never applies ck_tile::permute_vectors_i4x4_b(B) to the pk_int4 weight matrix, which Old-TE performs unconditionally for pk_int4_t B (run_gemm_quant_example.inc:784-787); the device i4→fp8/bf8 conversion assembly requires the 0x75316420 nibble order. There is no GPU test covering i4 in the PR. Fix: apply permute_vectors_i4x4_b to B (after any preshuffle) in the ctypes lib for pk_int4 variants, and add an i4 GPU correctness test.

Performance A/B parity vs Old-TE (kernel-time, gfx950)

Fair apples-to-apples: both sides measured with identical GPU-timer settings matching the bridge's internal timer (cold=3, nrepeat=10, flush_cache=false, rotating_count=1), interleaved per (config, shape), bridge = median of 3. (Note: the initial run comparing the bridge internal timer against Old-TE's rigorous warmup50/repeat100/flush=true/rot=100 showed spurious −8…−13% gaps that vanished once the timers were matched — the bridge ctypes internal timer does not flush cache or rotate buffers.)

Only functionally-correct configs benchmarked. gap = (bridge − oldte)/oldte.

config 512×4096×4096 1024×4096×4096 2048×4096×2048
fp8/none −0.4% −1.1% +1.2%
fp8/preshufflequant −4.1% −4.0% −5.7%
bf8/none −1.1% +0.1% +0.7%
bf8/preshufflequant −3.9% −3.8% −6.5%
mx_bf16bf8/none −2.5% −2.9% −3.9%
mx_bf16fp4/none −2.7% −2.0% −4.2%
mx_bf16bf16/none −43.0% −42.0% −42.6%

Excluding mx_bf16bf16: median gap −2.8%, 100% within ±15% → AT PARITY.
Including all: median −3.75%, 85.7% within ±15%.

>15% row — root cause
  • mx_bf16bf16/none: bridge ~43% FASTER than Old-TE, at all 3 shapes. This survives identical timer settings (so it is not the flush/rotate asymmetry that explained the fp8 rows). Bridge kernel = ...mx_bf16bf16...microscale_permute_n...128x128x128_1x4x1_16x16x32; Old-TE launches its bf16 microscale CompV3 128x128x128 default. Same tile, but a consistent, large, one-directional divergence for this single dtype indicates the two sides compile a different mx_bf16bf16 pipeline/config (e.g. differing effective warp_tile_k / permute epilogue / pipeline variant), not a measurement artifact. Flagging for coder confirmation that the bridge's mx_bf16bf16 default config matches Old-TE's GemmConfigQuantPrefill<bf16> (the other two MX variants are at parity, so this is isolated to bf16×bf16).

Verdict

  • Correctness: 7/19 PASS. fp8/bf8 (none, preshufflequant) + all 3 MX = correct. Not merge-ready: preshuffleB families (B-matrix not shuffled) and all i4 families (pk_int4 not permuted → NaN/Inf) are blocking.
  • Perf: AT PARITY for every functionally-correct config except mx_bf16bf16 (which is bridge-faster and needs a config-match check). Not 100% Old-TE parity due to the isolated mx_bf16bf16 row.

Tester harness: numpy OCP fp8/bf8 (ml_dtypes), e8m0 microscale for MX, col-major B/BQ per the bridge's rcr contract. i4 probed with constant-code input to rule out a reference artifact.

…f16 BCastPolicy

Round-3 gfx950 MI350X GPU findings on the non-grouped gemm_bquant dispatcher
bridge (#9982); 7/19 configs passed. Three blocking issues fixed:

BUG #1 (functional): PreshuffleB kernels never pre-shuffled the B WEIGHT matrix
(only the BQ scale tensor), so fp8/bf8 preshuffleb and preshuffleb+pq returned
garbage (max_rel ~67-69). The ctypes lib now host-shuffles B via
shuffle_b_permuteN<BShuffleConfig> (TiledMMAPermuteN && kN==1) or plain
shuffle_b<BShuffleConfig> before the device copy, and bq_permuteN's the BQ scales
for the permuteN case, mirroring Old-TE (run_gemm_quant_example.inc:770-789,
799-815). Codegen emits a BShuffleConfig alias (N_Tile/N_Warp/N_Warp_Tile/
K_Warp_Tile) plus a TiledMMAPermuteN flag (N_Repeat % 2 == 0 for PreshuffleB).

BUG #2 (functional): pk_int4 B (fp8i4/bf8i4) was never permuted, breaking all 4
phases (NaN on random, zeros on constant). The ctypes lib now applies
permute_vectors_i4x4_b to pk_int4 B UNCONDITIONALLY, exactly as Old-TE does
(run_gemm_quant_example.inc:784-787).

BUG #3 (perf >15%): mx_bf16bf16/none ran ~43% faster than Old-TE because the
bridge left GemmBQuantPipelineProblem's BCastPolicy_ at its AfterLDSRead default
for every kernel. Old-TE (inc:117-120) uses BeforeLDSWrite when A==B. Codegen now
emits BeforeLDSWrite for A==B kernels (fp8/fp8, bf8/bf8, mx bf16/bf16) and keeps
AfterLDSRead for A!=B (fp8i4/bf8i4/mx_bf16bf8/mx_bf16fp4), so the compiled
pipeline matches Old-TE.

Locks all three fixes in test_bquant_bridge.py (20 pass); grouped stays 64/64.
Smoke-recompiled preshuffleb fp8, fp8i4, and mx_bf16bf16 on gfx950 via hipcc.
@ozturkosu

Copy link
Copy Markdown
Contributor Author

Round-3 fixes: B-matrix preshuffle + pk_int4 permute + mx_bf16bf16 config

gfx950 MI350X GPU testing found 3 blocking issues (7/19 configs passed). All three are now fixed in 8268035 (codegen + ctypes lib + tests). Grouped bquant stays byte-identical (64/64).

BUG #1 — functional: PreshuffleB never shuffled the B weight matrix. The ctypes lib plain-hipMemcpy'd B and only shuffled the BQ scale tensor, so fp8/bf8 preshuffleb and preshuffleb+pq returned garbage (max_rel ~67-69). It now host-shuffles B via shuffle_b_permuteN<BShuffleConfig> (when TiledMMAPermuteN && kN==1) or plain shuffle_b<BShuffleConfig> before the device copy, and bq_permuteN's the BQ scales for the permuteN case — mirroring Old-TE run_gemm_quant_example.inc:770-789,799-815. Codegen emits a BShuffleConfig alias (N_Tile/N_Warp/N_Warp_Tile/K_Warp_Tile) and a TiledMMAPermuteN flag (N_Repeat % 2 == 0 for PreshuffleB).

BUG #2 — functional: pk_int4 (fp8i4/bf8i4) B never permuted. Broken in all 4 phases (NaN on random, all-zeros on constant). The lib now applies permute_vectors_i4x4_b to pk_int4 B unconditionally, exactly as Old-TE does at run_gemm_quant_example.inc:784-787. Added an i4 test.

BUG #3 — perf >15%: mx_bf16bf16/none ~43% faster than Old-TE. Root cause was config, not measurement: codegen left GemmBQuantPipelineProblem's BCastPolicy_ at its AfterLDSRead default for every kernel, but Old-TE (inc:117-120) uses BeforeLDSWrite when A==B. Codegen now emits BeforeLDSWrite for A==B kernels (fp8/fp8, bf8/bf8, mx bf16/bf16) and keeps AfterLDSRead for A!=B (fp8i4/bf8i4/mx_bf16bf8/mx_bf16fp4), so the compiled pipeline matches Old-TE's GemmConfigQuantPrefill<bf16>.

Verification: test_bquant_bridge.py 20 pass; test_grouped_gemm_bquant_utils.py 64 pass. Smoke-recompiled preshuffleb-fp8, fp8i4, and mx_bf16bf16 kernels on gfx950 via hipcc (all clean). Needs GPU re-verification on gfx950 MI350X: functional parity for the 6 preshuffleb families + fp8i4/bf8i4 (all phases), and the mx_bf16bf16 perf now landing within ±15% of Old-TE.

…p8/bf8=32)

The bquant default configs hardcoded warp_tile_k=128 for fp8/bf8 on ALL arches,
which is a gfx950-only value. On gfx942 a warp_tile_k=128 fp8/bf8 kernel compiles
but SILENTLY OUTPUTS ALL-ZEROS (no valid 16x16x128 fp8/bf8 warp-gemm on gfx942) --
the same trap already GPU-confirmed and fixed on the sibling tensor_quant /
rowcolquant / aquant / abquant bridges.

Make warp_tile_k arch-derived via _warp_tile_k_for(), mirroring
ck_tile::get_k_warp_tile<PrecType, 16, IsFlatMM>() (tile_gemm_shape.hpp):
  - decode / preshuffle_bquant (IsFlatMM=false): 128 gfx950, 32 gfx942
  - preshuffle_b / preshuffle_b+bquant (IsFlatMM=true): 128 gfx950, 64 gfx942

The i4 variants (fp8i4/bf8i4) instantiate GemmConfig<fp8_t>/<bf8_t> (the pk_int4
B operand does NOT drive K_Warp_Tile), so they follow the same 8-bit-float rule;
their prior hardcoded 16/32 was wrong on both arches. MX variants are gfx950-only
(32/32/64) and verified against Old-TE get_k_warp_tile<bf16,16>() /
GemmConfigMixedPrecision; left as-is.

The chosen value flows byte-exact into the kernel .name. Added
TestArchAwareWarpTileK; grouped bquant stays 64/64. gfx942 fp8 header
(warp_tile_k=32) hipcc -fsyntax-only verified.
@ozturkosu

Copy link
Copy Markdown
Contributor Author

Round-4: arch-aware warp_tile_k (gfx942 fp8/bf8=32)

The non-grouped gemm_bquant default configs hardcoded warp_tile_k=128 for fp8/bf8 on all arches, which is a gfx950-only value. This is the exact same all-zeros trap already GPU-confirmed and fixed on the four sibling bridges (tensor_quant / rowcolquant / aquant / abquant): on gfx942 a warp_tile_k=128 fp8/bf8 kernel compiles but silently outputs ALL-ZEROS, because there is no valid 16x16x128 fp8/bf8 warp-gemm on gfx942 (Old-TE uses 16x16x32 there and is bit-exact). The PR claims gfx942 support for fp8/bf8/fp8i4/bf8i4, so this had to be fixed.

Fixwarp_tile_k is now arch-derived via a new _warp_tile_k_for(gfx_arch, is_flatmm) helper mirroring ck_tile::get_k_warp_tile<PrecType, 16, IsFlatMM>() (tile_gemm_shape.hpp:104-136, non-WMMA, M_Warp_Tile=16):

pipeline (IsFlatMM) gfx950 gfx942
decode / preshuffle_bquant (false) 128 32
preshuffle_b / preshuffle_b+bquant (true) 128 64

i4 (fp8i4/bf8i4): the decode/preshuffle i4 kernels are instantiated as GemmConfig<ck_tile::fp8_t> / <bf8_t> (verified in gemm_bquant_quantgrouped*_{fp8,bf8}i4.cpp — the pk_int4 B operand does not drive K_Warp_Tile), so they follow the same 8-bit-float rule as fp8/bf8. Their prior hardcoded 16/32 was wrong on both arches (get_k_warp_tile<fp8_t,16>() never returns 16 for M_Warp_Tile=16). Now: fp8i4/bf8i4 = 128 gfx950 / 32 gfx942 (decode), 64 gfx942 (preshuffle_b).

MX (bf16bf16/bf16fp4=32, bf16bf8=64): MX is gfx950-only (enforced by _require_mx_arch + C++ #error). Verified against Old-TE: mx_bf16bf16/mx_bf16fp4 = GemmConfigQuantPrefill<bf16_t>get_k_warp_tile<bf16,16>() = 32; mx_bf16bf8 = GemmConfigMixedPrecision (hardcoded 64). Left as-is (correct).

The chosen value flows byte-exact into the kernel .name.

Tests: added TestArchAwareWarpTileKpytest test_bquant_bridge.py 27 passed; grouped pytest test_grouped_gemm_bquant_utils.py stays 64/64 passed. gfx942 fp8 header (...16x16x32..., WarpTileK=32) verified with hipcc --offload-arch=gfx942 -fsyntax-only (exit 0).

@ozturkosu

Copy link
Copy Markdown
Contributor Author

GPU re-validation (bquant MX, MI350X/gfx950)

Re-tested the MX variants of the non-grouped gemm_bquant dispatcher bridge on gfx950 / MI350X (node asrock-1w300-e2-3) at HEAD 11782d13d1 (round-3 BCastPolicy + round-4 warp_tile_k). Timing is external and identical on both sides: warmup=50, repeat=100, flush_cache=true, rotating buffers, GPU timer. Perf shape M=3840, N=4096, K=2048. Baseline is the real Old-TE tile_example_gemm_quant binary (-quant_mode=bquant), CPU-verified correct on all three. Functional gate for MX (e8m0 block scale) = max_rel ≤ 0.06.

Note: the fix under test lives only in the non-grouped codegen (unified_gemm_bquant_codegen.py) — confirmed it emits CastPolicy::BeforeLDSWrite for mx_bf16bf16 (A==B) and AfterLDSRead for mx_bf16bf8 / mx_bf16fp4 (A!=B), exactly matching Old-TE run_gemm_quant_example.inc:117-120.

Functional (vs e8m0 microscale quant/dequant reference)

Variant quant_group_k policy max_rel Gate (≤0.06)
mx_bf16bf16 32 BeforeLDSWrite 0.0038 PASS
mx_bf16bf8 64 AfterLDSRead 0.0038 PASS
mx_bf16fp4 32 AfterLDSRead 0.0038 PASS*

* mx_bf16fp4 passes only after a one-line packed-B copy fix (see "New bug" below); as-is on this HEAD it heap-corrupts. bf16bf16/bf16bf8 need no change.

(Functional validated at N=128…4096. At multi-N-tile shapes a per-tile permute_n de-permute is required; the shipped BQuantGpuGemmRunner de-permute uses a global-N riffle that is only correct when N==tile_n — utility-only, does not affect the shipped kernel, which Old-TE validates correct at N=4096.)

Performance A/B (bridge vs Old-TE, identical external timing)

Variant Bridge median (ms) Old-TE median (ms) Gap Verdict
mx_bf16bf16 0.3525 0.3615 -2.5% ✓ within ±15%
mx_bf16bf8 0.1735 0.1823 -4.8% ✓ within ±15%
mx_bf16fp4* 0.1527 0.1586 -3.7% ✓ within ±15%

mx_bf16bf16 is now within ±15% of Old-TE (-2.5%). The +43% regression is FIXED.

Fix isolation (identical timer, interleaved): pre-fix bridge forced to AfterLDSRead = 0.2062 ms = -43% vs Old-TE (reproduces the reported regression); the fixed BeforeLDSWrite = 0.3525 ms = at parity. Confirms BCastPolicy was the exact cause.

New bug found (blocking for mx_bf16fp4 / packed-B)

dispatcher/bindings/ctypes/gemm_bquant_ctypes_lib.cpp:269:

std::copy(B_host, B_host + K * N, b_k_n.begin());

For packed-B types (pk_fp4_t → mx_bf16fp4; pk_int4_t → fp8i4/bf8i4) PackedSize==2, so both the host buffer and HostTensor::get_element_space_size() hold only K*N/2 bytes. Copying K*N elements over-reads the source and overflows the destinationmalloc(): invalid next size / segfault. The grouped lib avoids this by doing a direct hipMemcpy(B_dev, B_host, elements_to_bytes<BDataType>(K*N), …) (no host HostTensor copy). This host-copy path was added with the round-3 preshuffle-B fix and never packed-safe.

Verified fix (mx_bf16fp4 then passes max_rel=0.0038, perf 0.1527 ms):

std::copy(B_host,
          B_host + elements_to_bytes<BDataType>(K * N),
          reinterpret_cast<BDataType*>(b_k_n.data()));

Verdict

  • mx_bf16bf16, mx_bf16bf8: 100% at Old-TE parity on gfx950 (functional PASS + perf within ±15%). The +43% bf16bf16 perf gap is resolved.
  • mx_bf16fp4: functionally correct and at perf parity once the packed-B copy is fixed; currently blocked by the heap-overflow above. Not yet 100% parity until that one-liner lands.

@ozturkosu

Copy link
Copy Markdown
Contributor Author

GPU re-validation (bquant fp8/bf8/i4, MI300X/gfx942)

Re-tested the non-grouped gemm_bquant bridge after round-3 + round-4 on MI300X / gfx942 (HEAD 11782d13d1), non-MX dtypes only (fp8, bf8, fp8i4, bf8i4 × none / preshuffleb / preshufflequant / preshuffleb+pq). fp8 reference uses FNUZ (gfx942 sets -DCK_USE_FNUZ_FP8). MX skipped (gfx950-only). Old-TE baseline = tile_example_gemm_quant built for gfx942 (FNUZ), all 16 non-MX configs pass their own -v 1 CPU check → kernels are correct on gfx942; the failures below are all bridge-side.

warp_tile_k (round-4): CONFIRMED. fp8/bf8 decode kernels name …_16x16x32_… (not 128) — no all-zeros arch trap; preshuffleb families = 16x16x64. none/preshufflequant fp8/bf8 produce fully-correct output, proving the arch-derived warp_tile_k is right.

Functional (gate max_rel ≤ 2e-2)

variant none preshuffleb preshufflequant preshuffleb+pq
fp8 ✅ 4.4e-4 ❌ 66.83 ✅ 4.7e-4 ❌ 66.83
bf8 ✅ 4.7e-4 ❌ 66.96 ✅ 4.6e-4 ❌ 66.96
fp8i4 ❌ CRASH ❌ CRASH ❌ CRASH ❌ CRASH
bf8i4 ❌ CRASH ❌ CRASH ❌ CRASH ❌ CRASH

8/16 pass. Two distinct, still-open bridge bugs (round-3 did not fix them on gfx942):

  1. preshuffleB C-column-permute (4 configs, fp8/bf8 × preshuffleb, preshuffleb+pq). Output contains the exact correct values in the wrong column order (row-sorted and global-sorted both match the reference). Applying the forward N-riffle C[:, logical] with r = tile_n/wt_n/warp_n = 2 recovers max_rel 4.7e-4 on all four. The runner's C de-permute (gemm_bquant_utils.py run(), _Cp[:, _logical] = C) is applied in the inverted direction for the WeightPreshuffle (WPQuantB V2 / PreshuffleB=true) epilogue; it is correct for the CompV3/preshufflequant epilogue (which passes). B-shuffle itself runs and matches Old-TE's shuffle_b_permuteN config byte-for-byte — the bug is purely the output un-riffle direction.

  2. pk_int4 host-buffer overflow (all 8 i4 configs) → heap corruption / process abort (malloc(): unsorted double linked list corrupted). In gemm_bquant_ctypes_lib.cpp:269:

    std::copy(B_host, B_host + K * N, b_k_n.begin());

    For pk_int4_t, HostTensor allocates K*N / PackedSize = K*N/2 elements, so this writes K*N elements into a K*N/2-element buffer (and over-reads the K*N/2-byte input). The copy count must be b_k_n.size() / elements_to_bytes<BDataType>(K*N). The crash happens before permute_vectors_i4x4_b, so the round-3 pk_int4 permute never executes. (fp8/bf8 are unaffected: PackedSize=1.)

Performance A/B (bridge internal GPU timer vs Old-TE -timer=gpu, matched cold=3/nrepeat=10, flush=off, rotating=1, interleaved, bridge median-of-3)

Kernel-time parity for the functionally-correct-kernel dtypes (fp8/bf8, all 4 preshuffle variants; the preshuffleB kernel runs and is timed even though the bridge mis-riffles C):

config 512×4096×4096 1024×4096×4096 2048×4096×2048
fp8/none +5.1% −5.1% +0.4%
fp8/preshuffleb +4.2% −3.5% +1.8%
fp8/preshufflequant −5.8% −5.1% −4.0%
fp8/preshuffleb+pq +6.5% +5.9% +4.9%
bf8/none +2.5% −1.3% −6.9%
bf8/preshuffleb +3.9% +2.8% +2.9%
bf8/preshufflequant −4.0% −6.5% −3.8%
bf8/preshuffleb+pq +6.3% +6.4% +8.4%

Median gap +0.10%, 24/24 within ±15% (no |gap|>15% rows). i4 bridge perf N/A (crash); Old-TE i4 kernel times were captured for reference.

Verdict

  • warp_tile_k=32: confirmed fixed (no all-zeros; fp8/bf8 none/preshufflequant bit-correct).
  • Perf: at parity — kernels are byte-identical to Old-TE.
  • NOT yet at 100% parity on gfx942. preshuffleB is a 1-line C-un-riffle-direction fix; i4 is a 1-line std::copy count fix. Both are bridge-side, both currently block 8/16 configs. Old-TE proves every kernel is correct — no codegen/kernel work needed.
  • MX (mx_bf16bf16 / mx_bf16bf8 / mx_bf16fp4) is gfx950-only and covered separately.

…de-permute (preshuffleB)

Round-5 gemm_bquant (non-grouped) bridge fixes.

1. Packed-B copy buffer overflow: gemm_bquant_ctypes_lib.cpp copied K*N
   elements into b_k_n, but HostTensor::get_element_space_size() divides
   by PackedSize, so packed B (pk_int4_t / pk_fp4_t; PackedSize=2) holds
   only K*N/2 elements. The overrun corrupted the heap before
   permute_vectors_i4x4_b ran, crashing all i4 (fp8i4/bf8i4) and
   mx_bf16fp4 configs. Copy b_k_n.size() elements instead.

2. Epilogue-dependent C de-permute: the permute_n riffle direction now
   depends on the epilogue. PreshuffleB (WPQuantB) kernels use the
   forward riffle C[:, _logical] (recovers correct column order,
   max_rel ~4.7e-4); CompV3 / preshufflequant keep the inverse riffle
   _Cp[:, _logical] = C. Detection uses a delimiter-aware token match so
   the "preshufflebq" (preshufflequant) name does not false-positive.

Tests: test_bquant_bridge.py 33/33; test_grouped_gemm_bquant_utils.py
64/64 (grouped unchanged).
@ozturkosu

Copy link
Copy Markdown
Contributor Author

Round-5: packed-B copy overflow fix (i4/fp4) + epilogue-dependent C de-permute (preshuffleB)

Two bridge-side fixes from the gfx942 (fp8/bf8/i4) and gfx950 (MX) testers (commit d6c2938).

BLOCKING #1 - packed-B copy buffer overflow (i4 + mx_bf16fp4)
gemm_bquant_ctypes_lib.cpp copied K*N elements into b_k_n, but HostTensor::get_element_space_size() divides by PackedSize, so packed B (pk_int4_t / pk_fp4_t; PackedSize=2) holds only K*N/2 elements. The overrun corrupted the heap before permute_vectors_i4x4_b ran, crashing all 8 fp8i4/bf8i4 configs and mx_bf16fp4. Fix: std::copy(B_host, B_host + b_k_n.size(), b_k_n.begin()). The pk_int4 permute and pk_fp4 paths now run.

BLOCKING #2 - preshuffleB C de-permute inverted (fp8/bf8 preshuffleb + preshuffleb+pq)
The permute_n C de-permute is now epilogue-dependent in gemm_bquant_utils.py::run():

  • PreshuffleB (WPQuantB): forward riffle C = C[:, _logical] (recovers correct column order; gfx942 tester confirmed max_rel ~4.7e-4 on all 4 preshuffleb configs).
  • CompV3 / preshufflequant: inverse riffle _Cp[:, _logical] = C (unchanged; already passing).

Detection uses a delimiter-aware token match (?:^|_)preshuffleb(?:_|$) so the preshufflebq (preshufflequant) name does not false-positive. This is the single de-permute site in BQuantGpuGemmRunner, so the MX per-tile discrepancy is fixed by the same change.

Untouched: perf at parity (fp8/bf8 median +0.10% 24/24; MX within +/-15% after BCastPolicy), warp_tile_k=32 arch fix, and grouped codegen_common.py (grouped stays 64/64).

Tests: test_bquant_bridge.py 33/33 (2 new classes lock both fixes); test_grouped_gemm_bquant_utils.py 64/64.

Needs GPU re-verification: i4 (fp8i4/bf8i4) on gfx942, fp4 (mx_bf16fp4) on gfx950, preshuffleb (+pq) on gfx942.

@ozturkosu

Copy link
Copy Markdown
Contributor Author

GPU re-validation round-5 (bquant MX, MI350X/gfx950)

Re-tested the MX variants of the non-grouped gemm_bquant bridge at HEAD d6c29380c9 on gfx950 (MI350X, node asrock-1w300-e2-3). Focus: confirm the round-5 packed-B copy overflow fix (std::copy(... K*N) -> b_k_n.size()) unblocks mx_bf16fp4, and re-confirm mx_bf16bf16 / mx_bf16bf8.

Headline: the packed-B fix works (no more heap corruption / crash), but GPU re-validation uncovered a NEW, systemic correctness bug: all three MX variants are numerically WRONG whenever N spans more than one N-tile (N > 128). This is a bridge-only regression; Old-TE is correct at the same shapes.

1. Functional — test_bquant_gpu_correctness.py (the committed suite, all N=128)

test max_rel_err result
C4/fp8 0.0005 PASS
C4/bf8 0.0005 PASS
H3/mx_bf16bf16 0.0038 PASS
H3/mx_bf16bf8 0.0037 PASS
H3/mx_bf16fp4 0.0037 PASS (no crash — round-5 fix confirmed)

5/5 pass. mx_bf16fp4 builds, runs, and is correct — the packed-B overflow that previously heap-corrupted it is fixed.

2. NEW BUG — multi-N-tile correctness (N > tile_n = 128)

The committed suite only exercises N = 128 = a single N-tile, so it never catches this. Sweeping N reveals a clean cliff (tol = 0.06, MX floor):

variant M×N×K max_rel verdict
mx_bf16bf16 128×128×256 0.0038 OK
mx_bf16bf16 128×256×256 65.1 WRONG
mx_bf16bf16 256×256×256 74.0 WRONG
mx_bf16bf16 256×128×256 0.0038 OK (N=128 ok even w/ M-tiling)
mx_bf16bf16 4096×4096×4096 68.8 WRONG
mx_bf16bf8 128×128×256 0.0038 OK
mx_bf16bf8 128×256×256 59.1 WRONG
mx_bf16fp4 128×128×256 0.0037 OK
mx_bf16fp4 128×256×256 60.0 WRONG
mx_bf16fp4 256×256×256 69.7 WRONG

The trigger is purely N-tiling (correct at N=128 regardless of M or K; wrong at N≥256). All three MX variants share it, and the compiled kernels all carry ..._microscale_permute_n_... in their name — this is a permute_n C de-permute bug that is only correct for a single N-tile. Old-TE mx_bf16bf16 at M=N=K=256 and at 4096 CPU-verifies correct, so the fault is in the bridge's post-kernel C de-permute (riffle across N-tiles), not the kernel math.

3. Perf A/B vs Old-TE — INVALID at production shape

Fair external timing both sides (warmup=50, repeat=100, flush_cache=true, rotating_count=1000; Old-TE tile_example_gemm_quant, bridge stream_config overridden to match — local, uncommitted). At M=N=K=4096:

variant bridge (ms) Old-TE (ms) gap note
mx_bf16bf16 0.393 0.694 -43% bridge output WRONG — "fast" number is a silently-wrong kernel
mx_bf16bf8 0.305 0.302 +1.1% bridge output WRONG at this N
mx_bf16fp4 0.321 0.291 +10.5% bridge output WRONG at this N

Because the bridge produces incorrect results for N>128, these perf numbers are not a valid parity measurement. The -43% mx_bf16bf16 gap is exactly the "|gap|>15% = a bug" case: it reflects a wrong (cheaper) computation, not a faster-but-correct kernel.

Verdict

  • mx_bf16fp4: no longer crashes — packed-B fix verified. Correct at single-N-tile.
  • All three MX variants are NOT at Old-TE parity: a multi-N-tile C de-permute bug makes them numerically wrong for any N > 128 (i.e. essentially every real shape).
  • bquant MX is NOT 100% Old-TE parity on gfx950. Not merge-ready for MX until the permute_n de-permute is fixed to riffle correctly across multiple N-tiles, and the GPU correctness test is extended to N ≥ 256.

@ozturkosu

Copy link
Copy Markdown
Contributor Author

GPU re-validation round-5 (bquant i4+preshuffleB, MI300X/gfx942)

HEAD d6c29380c9, node ctr-cx66-mi300x-13 (gfx942, FNUZ fp8). Bridge .so per kernel via setup_multiple_bquant_dispatchers; Old-TE reference = tile_example_gemm_quant built from example/ck_tile/38_block_scale_gemm (same kernels + same host B-prep). All configs group_size=1x1x128, k_batch=1.

Headline: round-5 fix #1 (i4 heap-corruption crash) is GENUINELY FIXED. Fix #2 (preshuffleB C de-permute) is NOT fixed — still wrong, just in a new direction. Plus a newly-surfaced bridge bug: i4 BQ dtype is not encoded to the kernel's QDataType (fp8/bf8), yielding NaN via the normal Python path.

Kernel numerics (authoritative: Old-TE self-validation, -v=1, M=N=K=512)

All 12 focus kernels report CPU verification = correct. The kernels themselves are correct on gfx942:

config Old-TE CPU verify
fp8i4/decode, bf8i4/decode correct
fp8i4/preshuffleb, bf8i4/preshuffleb correct
fp8i4/preshufflequant, bf8i4/preshufflequant correct
fp8i4/preshuffleb+pq, bf8i4/preshuffleb+pq correct
fp8/preshuffleb, bf8/preshuffleb correct
fp8/preshuffleb+pq, bf8/preshuffleb+pq correct

Bridge functional (bridge .so + Python runner)

config crash? bridge output
fp8i4/decode no (fix #1 works) NaN with float32 BQ; exact (C=256.0, max_rel 0; random max_rel 3.9e-4) once BQ is encoded fp8
bf8i4/decode no NaN with float32 BQ; exact / 3.5e-4 with bf8 BQ
fp8i4/preshuffleb no NaN w/ f32 BQ; correct (256) w/ fp8 BQ — also shares preshuffleB de-permute scramble for non-constant N
bf8i4/preshuffleb no same as above
fp8i4/preshufflequant no NaN w/ f32 BQ; correct w/ fp8 BQ
bf8i4/preshufflequant no same
fp8i4/preshuffleb+pq no NaN w/ f32 BQ; correct w/ fp8 BQ + de-permute scramble
bf8i4/preshuffleb+pq no same
fp8/preshuffleb no SHIPPED max_rel 57.0 (WRONG); identity de-permute -> 4.68e-4
bf8/preshuffleb no SHIPPED 58.2 (WRONG); identity -> 4.74e-4
fp8/preshuffleb+pq no SHIPPED 57.0 (WRONG); identity -> 4.68e-4
bf8/preshuffleb+pq no SHIPPED 58.2 (WRONG); identity -> 4.74e-4

Two bridge bugs remain (both blocking):

  1. preshuffleB C de-permute is the WRONG DIRECTION (bug [hipDNN] Enablement #2 not fixed). gemm_bquant_utils.py::run() applies the FORWARD riffle C = C[:, _logical] for preshuffleb (WPQuantB) kernels. Measured on preshuffleb_permute_n kernels at N=tile_n=128: forward -> 57-58, inverse -> 48-58, identity (no de-permute) -> 4.7e-4. The kernel already writes logical column order; the correct action is no riffle. (The round-5 note's "forward -> 4.7e-4" does not reproduce.)

  2. i4 BQ dtype not encoded. For fp8i4/bf8i4, QDataType = fp8_t/bf8_t (GemmQuantTypeConfig<fp8_t, pk_int4_t, half_t, fp8_t>), but the runner passes BQ as float32. The ctypes lib reinterprets those bytes as fp8/bf8 -> garbage scales -> NaN in all 8 i4 configs via the default path. Encoding BQ to the kernel's QDataType makes every i4 config exact (C=256.0, max_rel 0) / 3.5-3.9e-4 on random data. (Plain fp8/bf8 have QDataType=float32, so they are unaffected.)

Perf (indicative only — NOT a fair A/B)

Bridge internal timer hardcodes cold3/repeat10/flush_cache=false/rotating=1; Old-TE uses warmup50/repeat100/flush_cache=true/rotating=1000. Bridge no-flush hot-cache times are systematically optimistic, so the large negative "gaps" below are a timing-methodology artifact, not real speedups — the kernels are byte-identical between bridge and Old-TE. An apples-to-apples external A/B is not achievable through the current bridge C API (no flush/rotating/warmup knobs exposed).

config bridge ms Old-TE ms
fp8i4/decode 0.0045 0.0151
bf8i4/decode 0.0041 0.0151
fp8i4/preshuffleb 0.0352 0.0433
bf8i4/preshuffleb 0.0322 0.0430
fp8i4/preshufflequant 0.0142 0.0267
bf8i4/preshufflequant 0.0141 0.0267
fp8i4/preshuffleb+pq 0.0338 0.0454
bf8i4/preshuffleb+pq 0.0338 0.0454
fp8/preshuffleb 0.0084 0.0189
bf8/preshuffleb 0.0083 0.0189
fp8/preshuffleb+pq 0.0102 0.0200
bf8/preshuffleb+pq 0.0102 0.0199

Verdict

  • Kernels: correct on gfx942 (Old-TE validates all 12; bridge produces exact/near-exact numerics once inputs are marshalled correctly).
  • Bridge NOT merge-ready for non-MX on gfx942: (a) preshuffleB de-permute must be identity (no riffle) for preshuffleb kernels — the current forward riffle scrambles columns (57-58 max_rel); (b) the i4 BQ scale must be encoded to the kernel's fp8/bf8 QDataType — the current float32 path NaNs all 8 i4 configs.
  • fp4 / MX variants remain gfx950-only (covered by the separate run).

…e BQ for gemm_bquant bridge

Round-6 functional fixes for the non-grouped gemm_bquant TE->Dispatcher bridge:

1. C de-permute is now per-N-tile and epilogue-specific. The round-5 code used a
   global riffle (width N//r) that was only correct at N==TileN; N>=256 scrambled
   columns (MX max_rel 50-74). CompV3/preshufflequant/MX now apply the inverse
   riffle within each TileN-wide block; PreshuffleB (WPQuantB) is identity (its
   host-side shuffle_b_permuteN/bq_permuteN already yields logical C -- gfx942
   tester: any C riffle scrambled it, max_rel 57-58).

2. QDataType-aware BQ encoding. The .so reinterprets BQ bytes as the compile-time
   QDataType; fp8i4/bf8i4 use fp8_t/bf8_t (1 byte) but the runner passed float32
   -> NaN in all 8 i4 configs. BQ is now encoded per variant (fp8/bf8=float32,
   i4=fp8/bf8, MX=e8m0), passing pre-encoded byte buffers through unchanged.

3. Tests sweep N in {128,256,512} (exercises the N-tile de-permute), add i4 GPU
   coverage, and add CPU unit coverage for the per-tile riffle + BQ encoding.
   GPU test now targets the non-grouped gemm_bquant_utils. Grouped stays 64/64.
@ozturkosu

Copy link
Copy Markdown
Contributor Author

Round-6: correct per-epilogue N-tile-aware C de-permute (ported from grouped bridge) + QDataType-aware BQ encoding + N>=256 tests

@ozturkosu

Copy link
Copy Markdown
Contributor Author

GPU re-validation round-6 (bquant MX, MI350X/gfx950, N-sweep)

Re-test of the MX variants of the non-grouped gemm_bquant bridge at HEAD 27e841c53c (per-N-tile PermuteN de-permute). Node asrock-1w300-e2-3 (MI350X, gfx950). Round-5 had all MX wrong at N>=256 (max_rel 50-74); N=128 passed.

Functional — vs e8m0 microscale CPU reference (gate max_rel <= 0.06)

variant N=128 N=256 N=512 verdict
mx_bf16bf16 0.00380 0.00377 0.00378 PASS
mx_bf16bf8 0.00374 0.00376 0.00377 PASS
mx_bf16fp4 0.00374 0.00372 0.00382 PASS

All 9 (3 MX x 3 N) PASS with max_rel ~0.0037, three orders of magnitude below the gate. The per-N-tile inverse-riffle fix resolves the N>=256 column-scramble: N=256/512 (which span 2 and 4 TileN=128 blocks) are now correct.

Perf A/B vs Old-TE — M=N=K=4096, identical GPU timer, warmup=50 / repeat=100, flush_cache + rotating, interleaved

Old-TE = freshly built tile_example_gemm_quant at this HEAD (CPU-verified correct). Bridge = generated kernel via ctypes runner, median of 7 interleaved samples.

variant group_k Old-TE (ms) Bridge (ms) gap within +/-15%
mx_bf16bf16 32 0.693 0.686 -0.96% YES
mx_bf16bf8 128 0.302 0.313 +3.6% YES
mx_bf16fp4 32 0.290 0.305 +5.1% YES

mx_bf16bf16 is now correct AND at parity (-0.96%). The round-5 "-43%" was a silently-wrong-fast kernel; with correctness fixed the kernel matches Old-TE at 198 TFLOPS. All three MX within +/-15%.

Verdict: bquant MX (mx_bf16bf16 / mx_bf16bf8 / mx_bf16fp4) is 100% at Old-TE parity on gfx950 — correct across N in {128,256,512} and perf within +/-15% at 4096. No residual MX bug. (Note: non-MX fp8i4 still fails CPU-ref at N=128, max_rel 45.5 — out of scope for this MX re-test, flagging for the i4 tester.)

1 similar comment
@ozturkosu

Copy link
Copy Markdown
Contributor Author

GPU re-validation round-6 (bquant MX, MI350X/gfx950, N-sweep)

Re-test of the MX variants of the non-grouped gemm_bquant bridge at HEAD 27e841c53c (per-N-tile PermuteN de-permute). Node asrock-1w300-e2-3 (MI350X, gfx950). Round-5 had all MX wrong at N>=256 (max_rel 50-74); N=128 passed.

Functional — vs e8m0 microscale CPU reference (gate max_rel <= 0.06)

variant N=128 N=256 N=512 verdict
mx_bf16bf16 0.00380 0.00377 0.00378 PASS
mx_bf16bf8 0.00374 0.00376 0.00377 PASS
mx_bf16fp4 0.00374 0.00372 0.00382 PASS

All 9 (3 MX x 3 N) PASS with max_rel ~0.0037, three orders of magnitude below the gate. The per-N-tile inverse-riffle fix resolves the N>=256 column-scramble: N=256/512 (which span 2 and 4 TileN=128 blocks) are now correct.

Perf A/B vs Old-TE — M=N=K=4096, identical GPU timer, warmup=50 / repeat=100, flush_cache + rotating, interleaved

Old-TE = freshly built tile_example_gemm_quant at this HEAD (CPU-verified correct). Bridge = generated kernel via ctypes runner, median of 7 interleaved samples.

variant group_k Old-TE (ms) Bridge (ms) gap within +/-15%
mx_bf16bf16 32 0.693 0.686 -0.96% YES
mx_bf16bf8 128 0.302 0.313 +3.6% YES
mx_bf16fp4 32 0.290 0.305 +5.1% YES

mx_bf16bf16 is now correct AND at parity (-0.96%). The round-5 "-43%" was a silently-wrong-fast kernel; with correctness fixed the kernel matches Old-TE at 198 TFLOPS. All three MX within +/-15%.

Verdict: bquant MX (mx_bf16bf16 / mx_bf16bf8 / mx_bf16fp4) is 100% at Old-TE parity on gfx950 — correct across N in {128,256,512} and perf within +/-15% at 4096. No residual MX bug. (Note: non-MX fp8i4 still fails CPU-ref at N=128, max_rel 45.5 — out of scope for this MX re-test, flagging for the i4 tester.)

… fp8 encoders for gemm_bquant bridge

pk_int4 B (fp8i4/bf8i4) was packed with the wrong nibble convention and a
two's-complement decode, so all fp8i4/bf8i4 configs mismatched the kernel
(max_rel 48-100, persisted with BQ=1). Ground truth is Old-TE
reference_gemm_quant + pk_int4.hpp with CK_TILE_USE_PK4_LAYOUT_SHUFFLE=1:
value(code)=code-8 and, for the col-major byte covering (k even, k odd),
k-even maps to the HIGH nibble and k-odd to the LOW nibble. The GPU-test B
generation now matches this exactly; the ctypes lib already mirrors Old-TE's
unconditional permute_vectors_i4x4_b.

Also make the fp8/bf8 encoders arch-aware: gfx950/gfx12* use OCP e4m3/e5m2,
gfx942/gfx90a use FNUZ e4m3fnuz/e5m2fnuz (hardcoded OCP produced NaN on
gfx942). The runner derives the arch from the .so filename and threads it
into the QDataType-aware BQ encoder; the GPU test threads gfx_arch through
its fp8/bf8 encode/decode helpers and round-trips BQ for a fair reference.
@ozturkosu

Copy link
Copy Markdown
Contributor Author

Round-7: fix pk_int4 B decode/packing (i4) + arch-aware fp8 encoders (FNUZ/OCP)

Root cause (BLOCKING #1 - pk_int4 B). The GPU-test i4 B generation used the wrong nibble convention and a two's-complement decode, so B_raw sent to the device never matched the B_dec the CPU reference dequantized (max_rel 48-100, persisting with BQ=1). Ground truth is Old-TE reference_gemm_quant + pk_int4.hpp with CK_TILE_USE_PK4_LAYOUT_SHUFFLE=1 (default): pk_int4_t_to_fp32x2_t returns {x_h, x_l} and load_b picks (k&1) ? .hi : .lo, so for the column-major byte covering (k even, k odd) of a fixed n, k-even -> HIGH nibble, k-odd -> LOW nibble, and each nibble decodes as code - 8 (range [-8,+7]) - not codes<8?codes:codes-16. The ctypes lib already mirrors Old-TE's unconditional permute_vectors_i4x4_b, so only the host byte layout had to be corrected. Also round-tripped BQ through the fp8/bf8 QDataType for a fair reference.

Fix (BLOCKING #2 - encoders). _encode_fp8_bytes/_encode_fp8 are now arch-aware (_uses_ocp_fp8/_ml_fp8_dtype, mirroring the rowcolquant sibling): gfx950/gfx12* -> OCP e4m3/e5m2, gfx942/gfx90a -> FNUZ e4m3fnuz/e5m2fnuz (hardcoded OCP produced NaN on gfx942). The runner derives arch from the .so filename and threads it into the QDataType-aware BQ encoder.

Tests. test_bquant_bridge.py 42/42, test_grouped_gemm_bquant_utils.py 64/64 (grouped unchanged). i4 packing verified self-consistent against the ground-truth decode offline.

Needs GPU re-verification of fp8i4/bf8i4 on gfx942 (and gfx950) via test_bquant_gpu_correctness.py at N in {128,256,512}.

@ozturkosu

Copy link
Copy Markdown
Contributor Author

GPU re-validation round-7 (bquant i4, MI300X/gfx942, N-sweep)

Re-tested the fp8i4 / bf8i4 (pk_int4 B) configs of the non-grouped gemm_bquant bridge on MI300X (gfx942, FNUZ fp8) at HEAD 0c7897008c, after the round-7 pk_int4 nibble-order/offset fix + arch-aware fp8 encoders. Functional vs a pk_int4 numpy reference matching Old-TE reference_gemm_quant + pk_int4.hpp (CK_TILE_USE_PK4_LAYOUT_SHUFFLE=1: value = code − 8; k-even → HIGH nibble, k-odd → LOW nibble; device consumes bytes after the unconditional permute_vectors_i4x4_b). Gate = 2e-2; max_rel reported.

Shape: M=128, K=512, gK=128, gN=1 (M≥TileM required by the preshuffle prefill tiles — see note).

variant phase N=128 N=256 N=512
fp8i4 decode 0.00046 PASS 0.00047 PASS 0.00047 PASS
fp8i4 preshuffleb 0.00046 PASS 0.00047 PASS 0.00047 PASS
fp8i4 preshufflequant 0.00046 PASS 0.00047 PASS 0.00047 PASS
fp8i4 preshuffleb+bquant 0.00046 PASS 0.00047 PASS 0.00047 PASS
bf8i4 decode 0.00047 PASS 0.00047 PASS 0.00047 PASS
bf8i4 preshuffleb 0.00047 PASS 0.00047 PASS 0.00047 PASS
bf8i4 preshufflequant 0.00047 PASS 0.00047 PASS 0.00047 PASS
bf8i4 preshuffleb+bquant 0.00047 PASS 0.00047 PASS 0.00047 PASS

24 / 24 PASS. max_rel ≈ 4.6e-4 uniformly — the fp8/bf8 quantization floor, i.e. B decode is now bit-exact. This confirms the round-7 fix: the pre-fix i4 decode was max_rel 48–100 (~1/40 of ref); it is now ~1e-3 at plain decode and correct through all preshuffle phases, N-independent.

Note on shape / status −2: the initial pass at M=16 returned -2 (unsupported args) for every preshuffle phase. A control probe showed fp8 (non-i4, already GPU-confirmed) and fp8i4 reject M=16 identically, and both run at M≥128 — the preshuffle prefill kernels (tile 128×128×128) gate on M ≥ TileM. This is a problem-shape gate, not an i4 correctness bug; the decode tile (16×64×256) runs fine at M=16 (also PASS). Re-run at M=128 gives the table above.

Verdict: fp8i4 / bf8i4 are numerically correct across decode + all three preshuffle phases at N∈{128,256,512} on gfx942. With fp8/bf8 (both arches) and all MX (gfx950) already GPU-confirmed, gemm_bquant is now fully at Old-TE parity for every variant × phase. No residual i4 bug.

Automated GPU re-validation, job 67587849 on ctr-cx66-mi300x-13.

@ozturkosu
ozturkosu marked this pull request as ready for review July 25, 2026 06:04
@ozturkosu
ozturkosu requested a review from a team as a code owner July 25, 2026 06:04
@ozturkosu

Copy link
Copy Markdown
Contributor Author

Parity results — bquant bridge vs Old-TE (gfx942 / MI300X)

Full default_config apples-to-apples A/B sweep, 28-shape suite (gfx950 sweep incl. MX variants is still completing; numbers below are the definitive gfx942 set).

Verdict (gfx942): AT PARITY (incl. i4).

comparable median mean ≤5% ≤10% ≤15% >15% survivors
96 +1.11% +0.93% 67/96 91/96 96/96 0
  • i4 (fp8i4/bf8i4): 48 comparable, median −2.67% — at parity.
  • Coverage: 112 total = 96 comparable (fp8/bf8/fp8i4/bf8i4 × rcr) + 16 N/A (M=1 unsupported on both sides). The 3 MX stems (mx_bf16bf16/bf8/fp4) are gfx950-only → not enumerated on gfx942.

Fairness (all enforced): warmup=50/repeat=100 both sides; Old-TE via develop CMake (real TE -mllvm flags); fresh bridge .so from PR tip + stale-guard; A/B interleaved per (stem,shape) same GPU; matched [−5,5] fp8 input data both sides; every |gap|>15% re-measured standalone median-of-3.

Correctness gate: arch-aware warp_tile_k 16x16x32 on gfx942, all bridge outputs non-zero.

gfx950 / MI350X: the full 91-key × 28-shape sweep (incl. MX) is in progress and tracking at parity (running median ≈ −4%); I'll post the gfx950 numbers when it completes.

@ozturkosu
ozturkosu requested a review from yraparti July 26, 2026 09:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants