Repository navigation
Conversation
Adds csrc/hip/, a HIP port of the NF12 codec and fused linear kernels for
AMD RDNA4 (gfx1200/gfx1201), consuming and producing the identical tiled byte
stream as the CUDA kernels so checkpoints stay portable.
* format_hip.cuh -- SPEC section 4 geometry, the AMD v_perm_b32 selector
conversion, decode_bulk/decode_group, tiling and wmma fragment helpers.
Restates the geometry from include/nf12/format.cuh and static_asserts that
they match, so a format change breaks the build instead of diverging.
* decode.hip -- nf12_hip_decode (bit-exact NF12 -> BF16), nf12_hip_linear_fused
(small-M fused decode+GEMM for m <= 16), nf12_hip_selftest.
* encode.hip -- native analyze/count/scan/write encoder with a hand-written
block-wide exclusive scan, since ROCm ships no CUB. Byte-identical to the
reference codec.
* wmma.hip -- nf12_hip_linear_wmma, the matrix-core fused path for
17 <= m <= 128 using v_wmma_f32_16x16x16_bf16_w32_gfx12. Decodes each 16x64
super-atom once into shared memory and reuses it for every token tile, so
the packed weight is read exactly once and the dense weight never exists.
Build is additive and off by default: `-DNF12_ENABLE_HIP=ON` adds the target
without touching any CUDA target, and `cmake -S csrc/hip -B build-hip` builds
libnf12_hip.so standalone on a host with no CUDA toolkit.
Verified on a Radeon AI PRO R9700 (gfx1201):
* decode reproduces every input BF16 bit, including +/-0, subnormals, Inf and
NaN payloads;
* encode matches the reference packed bytes and escape table on 30 cases
spanning 16x64 .. 11008x4096;
* fused linear: 0 violations of |err| <= 2e-3 + 2e-3*|ref| over 21 shapes,
rms_rel ~1.66e-3 (the FP64 oracle's own BF16 rounding floor);
* linear wins 1.30-1.44x over a dense BF16 GEMM at 11008x4096 for m=17..96,
running at ~640 GB/s against a measured 633 GB/s HBM read ceiling.
Known gaps, documented in csrc/hip/README.md: the library is not yet wired into
python/nf12 or the torch extension, m > 128 still needs a larger-tile kernel,
and only gfx1200/gfx1201 are validated (gfx1250 has no wmma-128b-insts and
falls back).
Porting notes recorded there: v_perm_b32 selector semantics are reversed versus
__byte_perm, hip_bfloat16 degrades to int in a hipified TU, __gfx1201__ is
device-pass only so the arch test must be a runtime gcnArchName check, and
__builtin_amdgcn_fdot2 is FP16 rather than BF16.
Makes the HIP path reachable rather than a subtree you have to build by hand.
CMake:
* CUDA is no longer a required project language. `check_language(CUDA)`
enables it only when a compiler exists, so an ROCm-only host configures and
builds instead of failing at project(). The CUDA targets stay behind
NF12_HAVE_CUDA; NF12_ENABLE_HIP stays additive and off by default, and
configuring with neither is a clear fatal error.
* nf12_hip lands its library in the build root, next to libnf12.so.
* csrc/hip/CMakeLists.txt only declares its own project() when built
standalone, so add_subdirectory() does not reset the parent scope.
JIT (`nf12._jit`):
* Detects a ROCm build and builds libnf12_hip.so instead of the CUDA pair,
using the visible gcnArchName targets and amdclang++ (CMake >= 4.2 rejects
the hipcc wrapper). Compiler discovery honours CMAKE_HIP_COMPILER,
amdclang++ on PATH, then ROCM_PATH / HIP_PATH / /opt/rocm.
* The cache key includes the backend, so CUDA and HIP builds of the same
source cannot collide.
Loader (`nf12._library`):
* `library_path()` resolves libnf12_hip.so on ROCm and still defaults to
libnf12.so on CUDA.
* `load()` raises a clear error on ROCm instead of trying to dlopen a torch
extension that does not exist there yet.
Adds include/nf12/nf12_hip.h: the C ABI actually implemented, with hipStream_t
and hipError_t. Every declared symbol was link-checked against the built
libnf12_hip.so from a plain C compiler.
Verified on this host:
* CUDA-only top-level build still produces libnf12.so unchanged;
* ROCm-only top-level build (no CUDA compiler) produces libnf12_hip.so;
* standalone csrc/hip build produces libnf12_hip.so;
* on ROCm, nf12._jit.library_directory() builds and caches the library,
nf12.library_path() resolves to it, and the built library launches the
selftest kernel (sentinel 0x4E463132) with wmma_supported(64) == 1.
Docs: top-level README gains an RDNA4 (ROCm) section; csrc/hip/README.md now
documents the real C signatures (the previous block had the wrong argument
order) and the JIT integration.
Name both upstream sources in the RDNA4 README and the top-level README, so the attribution is visible without opening the SPDX headers. No code change.
Qubitium
force-pushed
the
feat/rdna4-hip
branch
from
November 8, 2026 11:27
cfd1958 to
c0d4bdb
Compare
* bench/hip_linear.py reuses bench/common.py's k x n grid so the HIP numbers are directly comparable with the CUDA results, and reports warm/cold ratios plus the FP64 accuracy gate. * tests/hip/test_hip.py drives the C ABI: selftest, byte-identical encode, bit-exact decode (specials included), and fused/wmma linear against an FP64 oracle. * Replace the ad-hoc 11008x4096 table in csrc/hip/README.md with the canonical grid.
* m=16 was the worst point (0.42-1.18x of BF16) because it ran the v_dot2 small-M kernel. A 16-token call is exactly one WMMA tile, so route m >= 16 to the matrix-core path and keep the fused kernel for m <= 15. m=16 improves to 1.20-1.28x on the large canonical shapes. * Fix a latent multi-block bug: origin = m0 + tile_row[t] double-counted m0, so the path only worked while m_blocks == 1. Compute the absolute tile origin inline instead. * Extend the matrix-core route to m <= 256 (two M blocks); above that the decode-then-rocBLAS fallback wins on traffic. * Refresh the README tables and the HIP tests (m=16 now covers WMMA).
At kMt=8 the 8x8 FP32 accumulator costs 64 VGPRs and the kernel lands at 167 VGPRs / 9 waves per SIMD, which under-fills the machine at m=128. When K is long (k_blocks >= 128) and the launch is short of CTAs (n_blocks <= 64), run the 4-tile kernel over two M blocks instead. Measured on gfx1201 (warm / cold, vs dense BF16 F.linear): 8192x8192 m=128 0.68 -> 0.89x 14336x4096 m=128 0.79 -> 0.83x Short-K and wide-N shapes keep the 8-tile kernel and do not regress. m_blocks is now derived from the tile count the dispatcher actually launches (min(tiles, kMaxTiles)), not the unclamped one. At m=256 full_tiles=16 but only the 8-tile kernel is instantiated, so the old expression produced m_blocks=1 and silently dropped tokens 128..255 (caught by the FP64 oracle). Also drop the ModelCloud SPDX line and note the Qubitium adaptation.
Regenerate the warm/cold table from a fresh sequential run on one R9700 and document the shape-aware M split. Also drop the ModelCloud SPDX line from the HIP sources (keeping the AlpinDale/contributors line) and add an adaptation note for Qubitium.
Every split wrote its FP32 partial plane with bias_v already folded in, and wmma_reduce_kernel sums the planes, so the bias was multiplied by kSplits: 4x at m<=64 and 2x at m=128. Add it to the blockIdx.z == 0 plane only, which keeps the FP32-accumulate-then-round-once semantics. Caught by the validator's bias section once m=16 started using this kernel (N=1024, K=1024, m=16: max_abs 0.89 -> 6.1e-05). Regression test covers m = 16, 32, 64, 128 and asserts the delta equals the bias exactly once.
The decoded super-atom is 16x64 BF16, a 128-byte shared row, so every lane in a row group lands on the same LDS banks: the store was 8-way conflicted and the B-fragment load 16-way. Eight BF16 of padding (144-byte rows, 36 banks) turns that into 4-way / 2-way. It costs 4 KiB of LDS per CTA, which drops the resident CTAs per CU from 4 to 3, so the padded stride is a template variant selected only when the launch fits in three CTAs per CU (n_blocks * m_blocks <= 96). Split-K is now chosen from CTA count, K length and M instead of CTA count alone. A split reads a DISJOINT k slice, so the packed weight is never read twice; the cost is the per-split FP32 plane and the reduce pass. Very narrow N (896 rows -> 7 CTAs) splits 8 ways; long-K shapes split twice at small M and once at m=128; 4096x4096 does not split below m=128. R9700, warm, versus dense BF16 (m=16 / m=64): 896x4864 0.62 -> 0.83 0.50 -> 0.69 2048x8192 0.66 -> 0.83 0.59 -> 0.65 4096x4096 0.48 -> 0.54 0.45 -> 0.48 25600x5120 1.15 -> 1.33 0.98 -> 1.27 8192x8192 m=128 0.68 -> 0.90 No shape regresses; 0 accuracy violations, 22/22 hip tests pass.
Three fixes to the fused matrix-core path, aimed at the weights whose dense BF16 form fits in the 64 MiB Infinity Cache (4096x4096, 2048x8192, 896x4864). * The store no longer runs quad_transpose. It is a 4x4 dword transpose inside each 16-byte chunk (16 ds_bpermute_b32 + 32 selects per super-atom), and every ds_bpermute occupies the same LDS crossbar as the store and the B-fragment load. It is self-inverse, so undoing it on the address instead of the data is equivalent: dword d of lane l's chunk (h,q) belongs to dword (lane&3) of chunk (d+4q) in row (lane>>2)+8h. That is 16 STS.32 whose bank is 4*(lane>>2)+(lane&3)+const, i.e. all 32 banks exactly once. * The next super-atom's loads are issued before the mma and decoded after it, so the L3 latency is covered by the matrix-core work instead of stalling the decode. A 4096x4096 launch at m=16 is only two warps per SIMD. * Split-K is picked per shape from measured optima rather than a CTA-count rule: narrow N splits 8 ways, very wide N splits 2 ways at eight token tiles, everything else stays unsplit. R9700, warm, versus dense BF16 (m=16 / m=64), before -> after: 4096x4096 0.48 -> 0.81 0.45 -> 0.65 2048x8192 0.66 -> 1.03 0.59 -> 0.78 896x4864 0.62 -> 1.14 0.50 -> 0.84 25600x5120 1.15 -> 1.35 0.98 -> 1.21 8192x8192 m=16 1.21 -> 1.29 0 accuracy violations, 22/22 hip tests pass.
Qubitium
marked this pull request as ready for review
November 8, 2026 14:12
The split-K reducer was launched with (total+255)/256 blocks, and on gfx1201 exactly 256 blocks is a pathological 14.2 us case against 3.3 us at 64, 128 or 512. Cap the grid at 128. pick_splits also split shapes whose K is too short to give each split a real slice. 896x4864 (14 k-blocks) was taking 4 ways and paying four prologues for three super-atoms each, costing 9.7 -> 13.5 us; it now stays unsplit, while 4096x4096 (64 k-blocks) keeps 4 ways at m=16 (27.3 -> 25.7 us) and 2 above it. m=16 warm ratios against dense BF16: 4096x4096 0.88, 2048x8192 1.02, 896x4864 1.14 (cold 1.09 / 1.23 / 1.09). Refresh both README performance tables and correct the stale k-block figures in the pick_splits comment block.
One full bench run at bf0d676 across the canonical k x n grid and all six token counts, replacing numbers gathered across several kernel revisions. m=16 warm/cold: 4096x4096 0.89/1.10, 2048x8192 1.03/1.23, 896x4864 1.25/1.25. Wide shapes are 1.07-1.35x at m=16..32 and 0.96-1.24x at m=64; m=128 is 0.75-1.20x.
The m <= 15 fused kernel took m as a runtime argument, so acc[16][2] was indexed dynamically -- which keeps it out of VGPRs -- and the warp-partial plane was always sized for 16 tokens. Making the token count a template parameter fixes both and unrolls the token loop. At m=1 that raises occupancy enough to hurt: the kernel then runs eight CTAs per CU and 256 concurrent packed streams, which thrashes the 64 MiB LLC. Over-allocating the partial plane to 32 KiB throttles it to two CTAs per CU. Same-process A/B, old -> new: 4096x4096 m=1 28.6 -> 20.5 us (1.40x), m=8 32.6 -> 24.2 (1.35x); 2048x8192 m=1 19.4 -> 18.5 (1.05x), m=8 32.0 -> 23.9 (1.34x); 896x4864 m=8 15.4 -> 11.6 (1.33x). Against dense BF16 that is m=1 1.20x / 1.38x / 1.69x and m=8 0.94x / 0.97x / 0.97x, up from 0.84 / 1.33 / 1.63 and 0.67 / 0.80 / 0.83. m=16 still routes to the matrix-core kernel, which is 1.2-2.3x faster there on every measured shape.
Qubitium
added a commit
to Qubitium/GPTQModel
that referenced
this pull request
Nov 8, 2026
Mirrors dphnAI/nf12#3. The m <= 15 fused kernel took m as a runtime argument, so acc[16][2] was indexed dynamically -- which keeps it out of VGPRs -- and the warp-partial plane was always sized for 16 tokens. Making the token count a template parameter fixes both and unrolls the token loop. At m=1 that raises occupancy enough to hurt: eight CTAs per CU and 256 concurrent packed streams thrash the 64 MiB LLC. Over-allocating the partial plane to 32 KiB throttles it to two CTAs per CU. Same-process A/B, old -> new: 4096x4096 m=1 28.6 -> 20.5 us (1.40x), m=8 32.6 -> 24.2 (1.35x); 2048x8192 m=1 19.4 -> 18.5 (1.05x), m=8 32.0 -> 23.9 (1.34x); 896x4864 m=8 15.4 -> 11.6 (1.33x).
Qubitium
added a commit
to ModelCloud/GPTQModel
that referenced
this pull request
Nov 8, 2026
…3645) Mirrors dphnAI/nf12#3. The m <= 15 fused kernel took m as a runtime argument, so acc[16][2] was indexed dynamically -- which keeps it out of VGPRs -- and the warp-partial plane was always sized for 16 tokens. Making the token count a template parameter fixes both and unrolls the token loop. At m=1 that raises occupancy enough to hurt: eight CTAs per CU and 256 concurrent packed streams thrash the 64 MiB LLC. Over-allocating the partial plane to 32 KiB throttles it to two CTAs per CU. Same-process A/B, old -> new: 4096x4096 m=1 28.6 -> 20.5 us (1.40x), m=8 32.6 -> 24.2 (1.35x); 2048x8192 m=1 19.4 -> 18.5 (1.05x), m=8 32.0 -> 23.9 (1.34x); 896x4864 m=8 15.4 -> 11.6 (1.33x).
Author
|
@AlpinDale PR ready to review. RDNA4 is fast enough (still has headroom). I will get MI300X up as well. |
AlpinDale
self-requested a review
November 8, 2026 16:30
Two knobs were hand-tuned and are now computed from the grid. Double buffering costs 18 KiB of shared memory per CTA (36 against 18 single-buffered) out of 128 KiB per CU. At kMt <= 2 the VGPR budget allows well over three CTAs, so that footprint is what caps occupancy; from kMt = 3 the ~135 VGPRs already cap a CU at three and dropping the buffer buys nothing (25600x5120 m=128: 447 us buffered against 479 us not). Where LDS does bind, dropping it only helps in a narrow band -- the launch must be starved enough that one more resident CTA matters, but not so starved that the buffer's own latency hiding wins. 4096x4096 m=16 is four waves against a cap of three: 28.4 -> 23.5 us. The wide shapes are 14-28 waves deep and only lose (4096x28672 m=16: 1.14 -> 1.08x). Split-K multiplies CTAs without duplicating a read, but only pays while the extra CTAs are resident. Four ways is now used up to m=32 (4096x4096: 30.1 -> 27.1 us) and not beyond (m=64: 38.2 -> 43.3, m=128: 62.9 -> 79.8). 4096x4096 m=16 goes 0.88 -> 1.02x warm (1.12x cold) and m=32 0.76 -> 0.81x. Everything else is unchanged within noise.
Qubitium
added a commit
to Qubitium/GPTQModel
that referenced
this pull request
Nov 8, 2026
Mirrors dphnAI/nf12#3. Double buffering costs 18 KiB of shared memory per CTA out of 128 KiB per CU. At kMt <= 2 the VGPR budget allows well over three CTAs, so that footprint is what caps occupancy; from kMt = 3 the ~135 VGPRs already cap a CU at three and dropping the buffer buys nothing. Where LDS does bind, dropping it only helps in a narrow band: 4096x4096 m=16 is four waves against a cap of three and goes 28.4 -> 23.5 us, while the 14-28-wave wide shapes only lose. Split-K now goes four ways up to m=32 (4096x4096: 30.1 -> 27.1 us) and not beyond (m=64: 38.2 -> 43.3, m=128: 62.9 -> 79.8), because the accumulator already caps a CU at three CTAs. 4096x4096 m=16 goes 0.887 -> 1.004x against dense BF16 in the validator.
Qubitium
added a commit
to ModelCloud/GPTQModel
that referenced
this pull request
Nov 8, 2026
…pe (#3648) Mirrors dphnAI/nf12#3. Double buffering costs 18 KiB of shared memory per CTA out of 128 KiB per CU. At kMt <= 2 the VGPR budget allows well over three CTAs, so that footprint is what caps occupancy; from kMt = 3 the ~135 VGPRs already cap a CU at three and dropping the buffer buys nothing. Where LDS does bind, dropping it only helps in a narrow band: 4096x4096 m=16 is four waves against a cap of three and goes 28.4 -> 23.5 us, while the 14-28-wave wide shapes only lose. Split-K now goes four ways up to m=32 (4096x4096: 30.1 -> 27.1 us) and not beyond (m=64: 38.2 -> 43.3, m=128: 62.9 -> 79.8), because the accumulator already caps a CU at three CTAs. 4096x4096 m=16 goes 0.887 -> 1.004x against dense BF16 in the validator.
Each header stated the same three facts twice: an SPDX copyright line and then 'Copyright (c) 2026 AlpinDale and the nf12 contributors', an SPDX licence identifier and then 'Licensed under the Apache License, Version 2.0', and the dphnAI/nf12 URL both inside the reference-kernel block and one line above it. The decode, encode and format headers also repeated the 'adapted for GPT-QModel by Qubitium' sentence that the line above already makes. Collapse all of that into one three-line credits block that names each party once. Net -23 lines, no content lost. Also fix the gfx1200/gfx1200/gfx1250 typo in format_hip.cuh and retitle the encode section from 'Why the port is not a copy' to 'Port notes', since the paragraph above it already says the port is a transliteration.
Qubitium
added a commit
to ModelCloud/GPTQModel
that referenced
this pull request
Nov 8, 2026
…3650) Mirrors dphnAI/nf12#3. Each header stated the same three facts twice: an SPDX copyright line and then 'Copyright (c) 2026 AlpinDale and the nf12 contributors', an SPDX licence identifier and then 'Licensed under the Apache License, Version 2.0', and the dphnAI/nf12 URL both inside the reference-kernel block and one line above it. Three files also repeated the 'written for GPT-QModel (ModelCloud.ai)' sentence that the SPDX line above already makes. Collapse all of that into one three-line credits block that names each party once. Net -28 lines, no content lost. Also fix the gfx1200/gfx1200/gfx1250 typo and retitle the encode section from 'Why the port is not a copy' to 'Port notes'.
AlpinDale
requested changes
Nov 8, 2026
AlpinDale
left a comment
Member
There was a problem hiding this comment.
Thanks for the contribution! I left a few comments.
Also please compare against BF16 instead of FP64, as our spec only guarantees losslessness against a BF16 baseline (see: §6.2 of the spec).
Qubitium
added a commit
to Qubitium/nf12
that referenced
this pull request
Nov 9, 2026
The CTA-wide activation staging is re-read once per column stripe, and at m=64 that traffic (33.5 MB) is larger than the packed weight (25.2 MB). It is the main thing diluting the format's 1.33x byte advantage, so widening the stripe group is worth more than it looks. Eight stripes per CTA was gated on n_tiles >= 512. Measured, it also wins at n=4096 (256 tiles) once the token tile is at least four tiles tall: m=64 26.1 -> 25.8 us, m=128 36.5 -> 35.5 us, and nf12/bf16t 1.07 -> 1.09 and 1.06 -> 1.08. A one- or two-tile CTA has too little MFMA per barrier to amortise the wider staging block, so it keeps four. Grid, cold L2, nf12 vs the bf16t twin: shape m=1 m=16 m=32 m=64 m=96 m=128 4096x4096 1.067 1.017 1.052 1.098 1.074 1.076 4096x14336 1.109 1.064 1.113 1.100 1.077 1.079 14336x4096 1.115 1.058 1.079 1.148 1.112 1.079 tests/api + tests/hip: 591 passed, 27 skipped. gfx1200/gfx1201 device code unchanged (218 dphnAI#3 functions identical, 52 additive).
Analyze, count and write each stream the weight once. The 64 MiB Infinity Cache only retains the tail of the previous pass, so reading every pass front-to-back made the next pass start on the evicted end. Counting back-to-front starts where analyze ended and finishes on the head, which is where the write pass starts. Counts are still stored by flat unit id, so scan order, escape order and packed bytes are unchanged. Public nf12.encode wall clock, 8 warmup / 31 alternating samples, median: 1.165x on 4096x14336, 1.170x on 14336x4096, 1.130x on 8192x8192, 1.074-1.078x on 4096x28672 and 5120x25600, 1.017x on 8192x57344, neutral at 4096x4096 and below. Kernel-only encode_gpu 1.212x/1.224x. A 512 MiB L3 flush before each call still shows 1.152x, so the gain is not a warm-benchmark artifact.
Dense decode and bf16t unpack already ran at the streaming ceiling, so the remaining cost was occupancy: two super-atoms per warp took 48 VGPRs and halved the CTA count. One super-atom per warp drops that to 32 VGPRs and doubles CTAs; the arithmetic is untouched. moe_decode was genuinely under-parallelised. Its grid was (row blocks, experts) and each warp walked k_blocks/kWarpsPerBlock super-atoms serially, reaching 435-496 GB/s. blockIdx.x now packs (row block, K slice) so each CTA owns a disjoint contiguous run of whole super-atoms and each warp owns one. Every output element is still written exactly once; there is no reduction. Cold, interleaved A/B on gfx1201: - decode 4096x4096 105.16 -> 102.03 us, 14336x4096 360.28 -> 350.63, 8192x8192 411.66 -> 403.27, 896x4864 30.14 -> 28.42 (+2.1-6.1%); unpack_bf16t improves similarly. - moe_decode E=4 14336x4096 1891.6 -> 1426.6 us (+32.6%), E=4 4096x4096 485.4 -> 405.6 (+19.7%), E=8 4096x4096 947.6 -> 818.5 (+15.8%), E=4 2048x8192 424.2 -> 404.3 (+4.9%); now 574-581 GB/s. WMMA m=16..256 is unchanged (median ratio 1.000, min 0.965, max 1.040). Verified bit-exact for all 65536 BF16 patterns through encode/decode, both forced exp_bases, pack_bf16t/unpack, and MoE decode at K=4096 with slices; full suite 600 passed, 12 skipped.
Qubitium
added a commit
to Qubitium/nf12
that referenced
this pull request
Nov 9, 2026
Refresh the MI300X dense table for the CTA-count split rule (m=16 21.2 -> 19.0 us, m=32 24.0 -> 21.6 us) and state the two baselines separately: nf12 beats the bf16t twin by 1.04-1.15x on every PR dphnAI#3 grid shape, but loses to torch.matmul above m=1 (0.56-0.86x). Explain the loss in bytes rather than hand-waving: split-K is what fills 304 CUs from 256 column tiles, and at m=128/n=4096 the eight-split FP32 partial plane is 16.8 MB written plus 16.8 MB read, so nf12 moves 59.8 MB where rocBLAS moves 34.5 MB. Dropping to kSplits=1 restores the format's 1.32 byte ceiling but costs 4x absolute time (108.5 us vs 25.9 us at m=64).
Qubitium
added a commit
to Qubitium/nf12
that referenced
this pull request
Nov 9, 2026
A split whose range is shorter than two 64-column K blocks spends more on its FP32 partial plane (m*n*4 bytes written and read) than the extra CTAs buy back. This is the floor the OPUS gfx942 launch plan applies as min_iters; it binds on short-K shapes, where 896x4864 gives 14 K blocks so eight splits is 1.75 iterations each. Measured on MI300X, 896x4864, cold L2, graph timing, median of five runs of 100 samples: m nf12 us bf16t us ratio before 32 10.88 11.91 1.094 1.064 64 16.21 17.75 1.095 1.020 128 23.63 25.17 1.065 1.057 Restricted to a CTA that batches token tiles: a one-tile CTA has so little work per split that the extra CTAs are what fills the machine, and halving them measured 12% slower at m = 8 on the same shape. Every other cell of the PR dphnAI#3 grid is unchanged, because its k_blocks/kSplits is already >= 2.
Qubitium
added a commit
to Qubitium/nf12
that referenced
this pull request
Nov 9, 2026
k = 896 is 14 super-atoms, so the sixteen-warp launch gives every warp a single K block: the plane prefetch has nothing to overlap and the load latency is paid in full on a kernel whose decode is already the dominant work. Eight warps give two blocks each and the prefetch overlaps again. Only a one- or two-tile launch takes it. From three tiles up the decode is amortised over more MFMAs and the wider CTA measured faster, so the choice is gated on both the K depth and the token-tile count. 896x4864, cold L2, graph timing, median of three runs of 100 samples, ratio against the bf16t twin: m 8 warps 16 warps 1 1.017 0.990 2 1.029 1.023 4 1.012 1.011 5 1.006 1.007 8 1.002 1.002 Every other shape in the PR dphnAI#3 grid has 32 or more K blocks, so its launch is unchanged.
Qubitium
added a commit
to Qubitium/nf12
that referenced
this pull request
Nov 9, 2026
Fifty-two of fifty-four cells are at or above parity, peak 1.194. The two on the line are m=16 on the two smallest weights, where a CTA holds one token tile and the decode latency is the same order as the bytes it saves. Also corrects the aiter item 6 verdict: the OPUS min_iters guard is not a no-op, it binds at k=896 and is now adopted.
Qubitium
added a commit
to Qubitium/nf12
that referenced
this pull request
Nov 9, 2026
Per-symbol comparison of the gfx1200/gfx1201 offload bundles against PR dphnAI#3 (24ee25d) by encoded instruction words: 218 PR dphnAI#3 functions, 0 changed, 0 removed, 88 added, all of them CDNA3-only kernels. Also widens the quoted nf12/bf16t range to 1.05-1.17x, which is what the grid now measures.
Qubitium
added a commit
to Qubitium/nf12
that referenced
this pull request
Nov 9, 2026
Measured at m = 2, 4, 96, 256 on three shapes: nothing below parity (min 1.013), and the peak is 1.511x at 5120x25600 m=256 because m >= 129 leaves the fused route for decode + rocBLAS. That is where the 1.3 figure lives; the fused band caps at 1.19-1.24 because it carries the split-K partial plane and the decode in the same launch.
Author
|
@AlpinDale branch passing unit tests on r9700, 4090 and 5090. Will pause now for review. |
Author
|
@AlpinDale I rechecked the current PR head against the requested changes:
|
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds source-built RDNA4 ROCm support behind the same nf12 C, Python, and torch APIs as CUDA. Both backends share the packed format, C ABI frontend, and torch operator layer. The HIP backend covers encode/decode, BF16t packing, dense linear, embedding, and MoE, including the single-token fused route. Inference is graph-capturable and uses caller-owned or torch-managed scratch. The default wheel compiles and installs native libraries at package build time, following the CUDA project.
NF12_BUILD_NATIVE=OFFships sources and JIT-builds the libraries on first use.Native HIP entry points enforce the shared ABI buffer checks. ROCm PyTorch selects HIP automatically, and incompatible torch-backend builds fail at configuration. MoE handles dropped assignments and BF16t weights without reading absent metadata. Public signatures are unchanged.
Validation
R9700 (
gfx1201, ROCm SDK 10.1.0 / HIP 7.16.26385), RTX 4090, and RTX 5090; free-threaded Python with GIL disabled and two visible devices per backend.Top-level ROCm libraries build with C++17; CUDA-only configuration succeeds, and a mixed HIP/CUDA torch-extension build is rejected.
Tests cover exact NF12/BF16t equality, codec bit patterns, concurrent streams and devices, graph replay, native invalid-buffer rejection, and dropped MoE assignments. The deselected test is the separate reference-tolerance case tracked in PR #7. A ROCm wheel built without isolation contained both native libraries and 78 source files; the
NF12_BUILD_NATIVE=OFFwheel contained 78 sources and no.so. Both installed wheels passed an exact BF16 encode/decode round trip, with first-use JIT verified for the source-only wheel. The RTX 5090 run uses PR head98c8265merged with CUDA masterbaa1c419, including its warp-deadlock fix. Hardware validation does not cover gfx1200.Performance
On R9700, public encoding improves up to 1.170× on large weights; cold dense decode improves 2.1–6.1%, and MoE decode 4.9–32.6%. Single-token fused MoE is 1.08–3.00× faster than the corrected composed path across the measured shapes. Small encode shapes are essentially unchanged. Dense linear is still below BF16 on some shapes; the linked tables include those regressions and warm/cold measurements.
BF16T split-K regression coverage
Commit
9fc6283addstest_bf16t_twin_matches_nf12_split_k_plans, covering the reachable split-2, split-4, and split-8 dense plans. GPT-QModel PR #3716 carries the same coverage for the vendored kernel and links this PR as the upstream implementation proof.