Skip to content

[Perf] Optimize rmsnorm/layernorm - #848

Merged
coderfeli merged 29 commits into
mainfrom
cjl/norm_perf
Oct 9, 2026
Merged

coderfeli merged 29 commits into
mainfrom
cjl/norm_perf

Conversation

@cschenjunlin

@cschenjunlin cschenjunlin commented Jul 14, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Optimize rmsnorm/layernorm

Technical Details

Test Plan

Test Result

Submission Checklist

Comment thread kernels/layernorm_kernel.py
Comment thread kernels/layernorm_kernel.py
@coderfeli
coderfeli requested review from jhinpan and sjfeng1999 July 14, 2026 09:20
Comment thread kernels/layernorm_kernel.py Outdated
Comment thread kernels/layernorm_kernel.py Outdated

@jhinpan jhinpan left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Multi-model adversarial review of #848

I ran four independent reviewers (Claude Opus 4.8, Claude Sonnet 5, Composer 2.5, GPT-5.5) over the full diff + post-PR files, then synthesized. Inline comments capture the actionable findings; the tags show how many models independently raised each.

Verdict: no CRITICAL correctness / OOB / race bug was found by any of the four models. The trickiest parts were checked and hold up: large-M sub-warp group_reduce_add (THREADS_PER_ROW is always 32 or 64, which divides the 64-lane wavefront, with rows on aligned lane segments), the bf16 RNE bit-packing + even/odd shuffle, the vec8->int8 quant store mapping (out_idx = tid*2 + tile_i*BLOCK_THREADS*2) with write-once per-row YScale, LDS reuse across add->max reductions with body-level barriers, tail masking, and n_float denominators.

Consensus themes (act on):

  • Test-coverage gaps (all 4 models): bf16-only RMSNorm matrix; untested f32/f16 and >2048 unaligned generic path; N=2048/N=8192 boundaries; f16 LayerNorm fast path never run.
  • LayerNorm eps not parameterized while RMSNorm now is (3 models).
  • LayerNorm vectorizes only at exactly N==8192 (2 models).

Behavior changes to confirm / document (2 models / verified):

  • Loss of the M>8192 dispatch heuristic -> all N<=2048 now uses the large-M kernel regardless of M.
  • RMSNorm smoothquant XScale dtype contract changed fp32 -> elem_dtype (breaking input ABI).

Lower-priority nits (not posted inline):

  • Stale RMSNorm module docstring - still says the fast path is N % tile_cols == 0; the actual condition is N % VEC_WIDTH == 0 (2 models).
  • Redundant arch check (arch == "gfx950") or str(arch).startswith("gfx95") - the first disjunct is subsumed by the second.
  • Fast paths call block_reduce_add2(dummy, sumsq) where a single block_reduce_add exists, doubling the wave-shuffle work.
  • Quant store has no explicit clamp before .to(int8); relies on |q| <= 127 from the exact block max (holds in practice, absorbed by the quant_error <= 1 tolerance).
  • _store_scalar's elem_dtype/store_dtype split is always called with identical arguments.
  • Weakened quant test assertions (dequant / reconstruction checks removed; only quant_error<=1 + scale_error remain).
  • layernorm_kernel.py dynamicquant launcher return indentation differs from the RMSNorm twin (works, but reads like a mis-indent).

Overall: careful, well-structured work; mergeable once test coverage is broadened and the two intentional-looking behavior changes are confirmed/documented.

Comment thread tests/kernels/test_rmsnorm.py Outdated
Comment thread tests/kernels/test_layernorm.py
Comment thread kernels/layernorm_kernel.py Outdated
Comment thread kernels/layernorm_kernel.py Outdated
Comment thread kernels/rmsnorm_kernel.py Outdated
Comment thread kernels/norm/rmsnorm_kernel.py
Comment thread kernels/rmsnorm_kernel.py
Comment thread kernels/rmsnorm_kernel.py Outdated
Comment thread kernels/rmsnorm_kernel.py Outdated
@cschenjunlin cschenjunlin changed the title Cjl/norm perf [Perf] Optimize rmsnorm/layernorm Jul 17, 2026
@cschenjunlin

Copy link
Copy Markdown
Contributor Author

Thanks much for all the reviews. A quick status/scope update:

This PR is still WIP. The current change set mainly focuses on the RMSNorm path updates; the LayerNorm side is not intentionally excluded, but the corresponding eps parameterization and broader vector-path generalization are still in progress and will be handled in follow-up updates unless reviewers prefer folding them into this PR.

For this PR, I’ll focus on:

  • Broadening the RMSNorm test matrix to cover f32/f16, N=2048, and an unaligned >2048 generic fallback case.
  • Cleaning up the RMSNorm large-M small-N path, including the unused reduction boilerplate and duplicated sizing logic.
  • Documenting/confirming the RMSNorm smoothquant XScale dtype contract.
  • Fixing stale comments and small low-risk nits.

For the bf16 conversion helper: the manual RNE pack path predates this PR and follows the documented gfx942/gfx950 split. I also verified on gfx942 with cache disabled that the default y.to(elem_dtype) path compiles and passes the RMSNorm bf16 vector-path tests, so I’m checking whether we should simplify that helper or keep the existing documented non-gfx95 software path.

cschenjunlin and others added 4 commits July 28, 2026 03:48
Thread caller-provided epsilon through dynamic, smooth, and fused-add quant builders, with coverage that detects silently hard-coded defaults.
Vectorize partial large-N tiles and small-N prefixes with scalar tails while preserving mixed weights and rstd behavior. Add boundary and source-IR coverage for the new routing.

Co-authored-by: Cursor <cursoragent@cursor.com>
Thread caller-provided epsilon through fused and quantized forward builders, with tests that detect silently hard-coded defaults.

Co-authored-by: Cursor <cursoragent@cursor.com>
Use masked vector iterations for aligned f16 and bf16 row widths, including a partially active final iteration in the test matrix.

Co-authored-by: Cursor <cursoragent@cursor.com>
cschenjunlin and others added 12 commits July 29, 2026 17:42
Isolate scalar, single-tile, and multi-tile XScale handling so Navi CI can identify the failing path without running the quarantined benchmark matrix.

Co-authored-by: Cursor <cursoragent@cursor.com>
Run the full plain SmoothQuant matrix on Navi now that targeted scalar and vec8 diagnostics pass.

Co-authored-by: Cursor <cursoragent@cursor.com>
Exercise the historical f32 shape and adjacent scalar boundaries so gfx1201 failures distinguish output, scale, and fused-residual corruption.

Co-authored-by: Cursor <cursoragent@cursor.com>
Keep the exact regression matrix as expected-failure diagnostics while preventing the known incorrect path from blocking unrelated Navi coverage.
Document the activation-dtype XScale ABI and gfx1201 quarantine so callers do not rely on an unsupported path.

Co-authored-by: Cursor <cursoragent@cursor.com>
Use vec8 for the aligned prefix and scalar copies only for the tail, avoiding whole-row scalar fallback.

Co-authored-by: Cursor <cursoragent@cursor.com>
block_reduce_max (rmsnorm/layernorm quant) and block_reduce (softmax) store
their per-wave partials into a scratch buffer that the preceding reduction is
still broadcast-reading on exit. Nothing ordered that read against the new
store, so a lagging wave could observe the next reduction's value and quantize
a row with the wrong statistic. That race is the source of the intermittent
gfx1201 RMSNorm/LayerNorm quant mismatches in CI: the window is visible in the
generated ISA on both gfx1201 and gfx942, and a host-side model of the
corrupted scale matched 539/539 observed bad rows.

Add the missing barrier at the head of those helpers, which closes the window
at under 1% cost, and lift the now-unnecessary gfx1201 quarantine from the
SmoothQuant tests.
Co-authored-by: Cursor <cursoragent@cursor.com>
cschenjunlin and others added 2 commits August 25, 2026 13:43
Select one-row or multi-row forward kernels from runtime m_in, preserving the M > 8192 crossover without specializing the build cache on M.

Co-authored-by: Cursor <cursoragent@cursor.com>
Reduce wide-row latency with streaming accesses, gamma reuse, fast BF16 packing, and runtime geometry dispatch while keeping M dynamic. Add a three-backend benchmark to reproduce FlyDSL, HIP, and Triton comparisons.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cschenjunlin

Copy link
Copy Markdown
Contributor Author

performance comparison on MI308:

       shape  dtype   FlyDSL(us)    HIP(us)   Triton(us)   FlyDSL/HIP    FlyDSL/Triton
       1x512   bf16         3.04       1.88         1.71        0.62x            0.56x
      16x512   bf16         3.47       2.36         2.26        0.68x            0.65x
     128x512   bf16         3.50       2.53         3.10        0.72x            0.89x
    1024x512   bf16         6.23       3.26        10.59        0.52x            1.70x
    8192x512   bf16        24.49      10.45        84.95        0.43x            3.47x
    8193x512   bf16        11.21      10.34        12.84        0.92x            1.15x
   32768x512   bf16        28.05      29.11        34.08        1.04x            1.21x
      1x1024   bf16         2.78       1.92         1.83        0.69x            0.66x
     16x1024   bf16         3.16       2.45         2.43        0.78x            0.77x
    128x1024   bf16         3.27       2.63         3.13        0.80x            0.95x
   1024x1024   bf16         7.19       4.02        10.45        0.56x            1.45x
   8192x1024   bf16        28.36      17.43        68.89        0.61x            2.43x
   8193x1024   bf16        16.45      17.44        21.17        1.06x            1.29x
  32768x1024   bf16        51.05      58.18        66.02        1.14x            1.29x
      1x2048   bf16         2.62       2.01         2.01        0.77x            0.77x
     16x2048   bf16         3.28       2.81         2.54        0.86x            0.78x
    128x2048   bf16         3.66       2.90         3.49        0.79x            0.95x
   1024x2048   bf16         7.80       5.71        12.90        0.73x            1.65x
   8192x2048   bf16        33.76      31.51        85.84        0.93x            2.54x
   8193x2048   bf16        24.05      31.52        36.06        1.31x            1.50x
  32768x2048   bf16        81.53     118.02       125.68        1.45x            1.54x
      1x4096   bf16         2.88       2.31         2.42        0.80x            0.84x
     16x4096   bf16         3.07       2.83         2.83        0.92x            0.92x
    128x4096   bf16         3.73       3.54         4.18        0.95x            1.12x
   1024x4096   bf16         8.78       8.48        16.88        0.97x            1.92x
   8192x4096   bf16        48.41      50.11       110.64        1.04x            2.29x
   8193x4096   bf16        48.44      49.96       110.88        1.03x            2.29x
  32768x4096   bf16       191.55     191.76       426.44        1.00x            2.23x
      1x8192   bf16         3.40       2.71         3.07        0.80x            0.90x
     16x8192   bf16         3.74       3.30         3.51        0.88x            0.94x
    128x8192   bf16         5.39       4.71         5.52        0.87x            1.02x
   1024x8192   bf16        15.03      16.21        25.12        1.08x            1.67x
   8192x8192   bf16        97.80     100.00       171.50        1.02x            1.75x
   8193x8192   bf16        96.92      99.63       171.56        1.03x            1.77x
  32768x8192   bf16       399.77     415.77       673.44        1.04x            1.68x

Co-authored-by: Cursor <cursoragent@cursor.com>
jhinpan
jhinpan previously approved these changes Sep 8, 2026

@jhinpan jhinpan left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

APPROVE. Both items from my previous CHANGES_REQUESTED review (at bbdde41a41ae) are closed at this head, and I found no new material defect.

Prior items

[P1] BF16 round-to-nearest-away regression in the gfx942 fast path — fixed.
Commit cae767d63cfd ("restore the RNA semantic to RNE") deletes to_elem_vec_rna from kernels/norm/rmsnorm_common.py, drops the USE_FAST_BF16_OUTPUT gate in build_rmsnorm_module, and makes the vector store call _to_elem_vec(...) unconditionally (kernels/norm/rmsnorm_kernel.py:331). How I checked at this commit:

  • grep -rn "to_elem_vec_rna\|USE_FAST_BF16_OUTPUT" over the tree: no hits, so no dangling reference or dead gate remains.
  • grep -rn "0x8000" under kernels/norm/: no hits. The only rounding bias left is the RNE bias = lsb + 0x7FFF form in rmsnorm_common.py:107 and layernorm_kernel.py:89, which is the conversion my earlier tie probe showed matching both _to_elem_vec and PyTorch.
  • kernels/norm/rmsnorm_common.py at this head is byte-identical to main (diff against contents?ref=main), i.e. the helper was added and removed entirely within the PR and this file now carries no PR-local delta.

[P2] Reversed speedup column labels in tests/kernels/benchmark_rmsnorm_backends.py — outdated.
Commit b98ff768d95f removes the file (+0/-288); it no longer exists under tests/kernels/, and grep -rn "benchmark_rmsnorm_backends" over the tree returns nothing, so no script, doc, or workflow references it. The misstated committed artifact is gone.

What else I verified at this commit

  • Scope of change since my last review. I diffed all four PR files at 7f2b9aa3 against their bbdde41a versions: layernorm_kernel.py, test_rmsnorm.py, and test_layernorm.py are byte-identical, and rmsnorm_kernel.py differs only by the three RNA hunks above. The merge from main (7f2b9aa3) left the PR-owned files unchanged. This matters for the test evidence: on the gfx950 host I used last round, USE_GFX942_BF16_FAST_PATH is False, so the code that actually executed there was already the now-unconditional _to_elem_vec path — my previous run (45 passed / 7 skipped on the non-large RMSNorm + LayerNorm suites, both large-shape tests, the Softmax test, and the N=7/8/2047/2048/2049/3073 and M=8192/8193 boundary probes) applies unchanged to this head.
  • Static checks that run without a build. python3 -m py_compile on the four changed files plus rmsnorm_common.py: clean. python3 scripts/check_repo.py: check_docs_api OK (17 files, 869 symbols, 16 skills); the typed-arithmetic check errored only because this pinned checkout has no origin/main ref for its merge-base, so I ran its rule directly over the PR's added lines (maximumf|minimumf|maxsi|maxui|minsi|minui|maxnumf|minnumf|ceildivsi|ceildivui) — no hits. No conflict markers under kernels/ or tests/. GitHub now reports the branch as mergeable: true, resolving the DIRTY state I noted last time.
  • CI at this head. Check Python Code Style, Check C++ Code Style, CodeQL, and Documentation are green. I could not reproduce the style gate locally (no black/ruff in this environment) and I could not re-run the device suite here (this review checkout has no build-fly/ and no torch), so I am relying on CI plus the byte-identity argument above for that. Note that the Fly DSL test workflow is still in_progress at 7f2b9aa3; please confirm it lands green before merging.

Non-blocking note

The MI308 and MI350 tables in the PR conversation were produced by the now-deleted script and still carry the reversed headers — e.g. the MI350 1x512 row prints 1.05x under FlyDSL/HIP for 1.89 us FlyDSL vs 1.99 us HIP, which is HIP/FlyDSL. The committed code no longer misstates anything, so this is only about how a future reader should interpret those posted tables.

@cschenjunlin

Copy link
Copy Markdown
Contributor Author

APPROVE. Both items from my previous CHANGES_REQUESTED review (at bbdde41a41ae) are closed at this head, and I found no new material defect.

Prior items

[P1] BF16 round-to-nearest-away regression in the gfx942 fast path — fixed. Commit cae767d63cfd ("restore the RNA semantic to RNE") deletes to_elem_vec_rna from kernels/norm/rmsnorm_common.py, drops the USE_FAST_BF16_OUTPUT gate in build_rmsnorm_module, and makes the vector store call _to_elem_vec(...) unconditionally (kernels/norm/rmsnorm_kernel.py:331). How I checked at this commit:

  • grep -rn "to_elem_vec_rna\|USE_FAST_BF16_OUTPUT" over the tree: no hits, so no dangling reference or dead gate remains.
  • grep -rn "0x8000" under kernels/norm/: no hits. The only rounding bias left is the RNE bias = lsb + 0x7FFF form in rmsnorm_common.py:107 and layernorm_kernel.py:89, which is the conversion my earlier tie probe showed matching both _to_elem_vec and PyTorch.
  • kernels/norm/rmsnorm_common.py at this head is byte-identical to main (diff against contents?ref=main), i.e. the helper was added and removed entirely within the PR and this file now carries no PR-local delta.

[P2] Reversed speedup column labels in tests/kernels/benchmark_rmsnorm_backends.py — outdated. Commit b98ff768d95f removes the file (+0/-288); it no longer exists under tests/kernels/, and grep -rn "benchmark_rmsnorm_backends" over the tree returns nothing, so no script, doc, or workflow references it. The misstated committed artifact is gone.

What else I verified at this commit

  • Scope of change since my last review. I diffed all four PR files at 7f2b9aa3 against their bbdde41a versions: layernorm_kernel.py, test_rmsnorm.py, and test_layernorm.py are byte-identical, and rmsnorm_kernel.py differs only by the three RNA hunks above. The merge from main (7f2b9aa3) left the PR-owned files unchanged. This matters for the test evidence: on the gfx950 host I used last round, USE_GFX942_BF16_FAST_PATH is False, so the code that actually executed there was already the now-unconditional _to_elem_vec path — my previous run (45 passed / 7 skipped on the non-large RMSNorm + LayerNorm suites, both large-shape tests, the Softmax test, and the N=7/8/2047/2048/2049/3073 and M=8192/8193 boundary probes) applies unchanged to this head.
  • Static checks that run without a build. python3 -m py_compile on the four changed files plus rmsnorm_common.py: clean. python3 scripts/check_repo.py: check_docs_api OK (17 files, 869 symbols, 16 skills); the typed-arithmetic check errored only because this pinned checkout has no origin/main ref for its merge-base, so I ran its rule directly over the PR's added lines (maximumf|minimumf|maxsi|maxui|minsi|minui|maxnumf|minnumf|ceildivsi|ceildivui) — no hits. No conflict markers under kernels/ or tests/. GitHub now reports the branch as mergeable: true, resolving the DIRTY state I noted last time.
  • CI at this head. Check Python Code Style, Check C++ Code Style, CodeQL, and Documentation are green. I could not reproduce the style gate locally (no black/ruff in this environment) and I could not re-run the device suite here (this review checkout has no build-fly/ and no torch), so I am relying on CI plus the byte-identity argument above for that. Note that the Fly DSL test workflow is still in_progress at 7f2b9aa3; please confirm it lands green before merging.

Non-blocking note

The MI308 and MI350 tables in the PR conversation were produced by the now-deleted script and still carry the reversed headers — e.g. the MI350 1x512 row prints 1.05x under FlyDSL/HIP for 1.89 us FlyDSL vs 1.99 us HIP, which is HIP/FlyDSL. The committed code no longer misstates anything, so this is only about how a future reader should interpret those posted tables.

Thanks for the thorough review.
There are still some optimization work to do in this PR, so I’d like to keep this PR open for now. I’ll continue the tuning and update the PR.

Compile both cache policies into one gfx95x launcher and pick between them on the runtime row count, since streaming only helps once the working set outgrows the last-level cache. Widen the default block for wide gfx95x rows and mirror it in the autotune default config. Rename _build_rmsnorm_large_m_small_n_module to _build_rmsnorm_small_n_module.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cschenjunlin

Copy link
Copy Markdown
Contributor Author

performance comparison on MI350:

       shape  dtype  FlyDSL(us)  FlyDSL(GB/s)   HIP(us)  HIP(GB/s)  Triton(us)  Triton(GB/s)  FlyDSL/HIP  FlyDSL/Triton
       1x512   bf16        1.91           1.6      1.95        1.6        1.92           1.6       1.02x          1.00x
      16x512   bf16        3.23          10.5      2.67       12.7        2.78          12.1       0.83x          0.86x
     128x512   bf16        3.26          80.8      2.72       96.7        2.79          94.4       0.83x          0.86x
    1024x512   bf16        3.41         614.8      2.90      723.2        4.38         479.4       0.85x          1.28x
    8192x512   bf16        7.14        2349.2      5.52     3038.3       18.49         907.3       0.77x          2.59x
    8193x512   bf16        4.65        3607.9      5.34     3141.1        4.94        3394.7       1.15x          1.06x
   32768x512   bf16       11.66        5758.0     14.22     4719.2       11.44        5863.9       1.22x          0.98x
      1x1024   bf16        1.89           3.3      1.97        3.1        1.91           3.2       1.04x          1.01x
     16x1024   bf16        3.17          21.3      2.72       24.8        2.83          23.9       0.86x          0.89x
    128x1024   bf16        3.20         164.2      2.75      191.3        2.92         180.0       0.86x          0.91x
   1024x1024   bf16        3.70        1134.3      3.07     1366.8        4.65         902.6       0.83x          1.26x
   8192x1024   bf16        8.88        3778.8      7.22     4650.4       21.05        1594.0       0.81x          2.37x
   8193x1024   bf16        6.54        5133.3      7.12     4715.5        7.66        4383.2       1.09x          1.17x
  32768x1024   bf16       19.62        6842.2     22.82     5881.2       20.88        6426.7       1.16x          1.06x
      1x2048   bf16        1.85           6.6      2.27        5.4        1.91           6.4       1.22x          1.03x
     16x2048   bf16        3.14          43.1      2.85       47.4        2.90          46.6       0.91x          0.92x
    128x2048   bf16        3.20         329.1      2.98      353.6        2.96         355.3       0.93x          0.93x
   1024x2048   bf16        3.93        2138.0      3.69     2273.6        5.20        1615.2       0.94x          1.32x
   8192x2048   bf16       11.51        5832.3     14.58     4603.6       24.81        2704.8       1.27x          2.16x
   8193x2048   bf16       11.15        6018.8     14.33     4683.5       11.53        5820.9       1.29x          1.03x
  32768x2048   bf16       37.61        7138.4     41.49     6469.7       40.66        6602.1       1.10x          1.08x
      1x4096   bf16        1.90          12.9      1.91       12.9        1.92          12.8       1.01x          1.01x
     16x4096   bf16        3.22          83.8      2.89       93.5        3.15          85.9       0.90x          0.98x
    128x4096   bf16        3.46         609.2      3.10      678.5        3.13         673.4       0.90x          0.90x
   1024x4096   bf16        4.83        3476.5      4.65     3610.8        6.28        2671.4       0.96x          1.30x
   8192x4096   bf16       13.26       10125.2     21.79     6160.7       29.16        4603.3       1.64x          2.20x
   8193x4096   bf16       17.35        7738.7     21.74     6175.0       29.39        4568.1       1.25x          1.69x
  32768x4096   bf16       79.46        6756.6     78.01     6882.2      107.71        4984.5       0.98x          1.36x
      1x8192   bf16        2.08          23.7      1.96       25.1        1.95          25.3       0.94x          0.94x
     16x8192   bf16        3.46         156.3      3.25      166.5        3.35         161.5       0.94x          0.97x
    128x8192   bf16        3.74        1125.1      3.72     1131.0        3.79        1111.4       0.99x          1.01x
   1024x8192   bf16        6.37        5270.6      6.82     4923.4        8.57        3919.2       1.07x          1.34x
   8192x8192   bf16       35.41        7580.9     41.30     6500.8       47.82        5614.0       1.17x          1.35x
   8193x8192   bf16       39.08        6870.2     41.45     6477.9       42.75        6280.2       1.06x          1.09x
  32768x8192   bf16      176.30        6090.5    189.63     5662.5      214.27        5011.2       1.08x          1.22x

cschenjunlin and others added 3 commits September 18, 2026 05:26
Use masked vector iterations for vec8-aligned row widths instead of requiring an exact tile multiple, including a partially active final iteration in the test matrix.

Co-authored-by: Cursor <cursoragent@cursor.com>
The quantized RMSNorm and LayerNorm launchers specialize every non-quantized operand (Gamma, Beta, the fused-add residuals, and SmoothQuant XScale) on the activation dtype, but that ABI was implicit and unchecked: an operand passed with any other dtype was reinterpreted bit-for-bit and silently produced corrupted scales instead of failing, and callers written against the older fp32-only SmoothQuant XScale ABI broke the same silent way. Add a validate_norm_operand_dtypes helper, assert the ABI in all eight quantized launchers so a mismatch raises ValueError at compile time, and document the operand dtype requirement in the RMSNorm section of docs/prebuilt_kernels_guide.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
The scalar paths moved one element through a copy atom, a (1, 1) logical divide and a one-element register-memory tensor, which is boilerplate for what buffer-tensor indexing already expresses. Replace the load_scalar/store_scalar helpers in rmsnorm_common.py and the _load_scalar/_store_scalar/_store_yscale helpers in layernorm_kernel.py with direct t[index] reads and writes at every call site, and drop the scalar copy atoms, (1, 1) divides and element-width locals that only fed them; this also retires the elem_dtype/store_dtype split _store_scalar needed. rmsnorm_bwd_kernel.py is touched because it imported the deleted helpers. The generated ISA is byte-identical to before the change (502 instructions, 36 VGPRs, no spills on the N=3073 and N=3001 bf16 configurations), so this is a source-level cleanup with no codegen impact.

Co-authored-by: Cursor <cursoragent@cursor.com>

@jhinpan jhinpan left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Requesting changes for one reproducible silent-correctness hole in the new quantized-norm ABI guard; details are inline. I found no other material defect in the gfx95x cache-policy dispatch, partial vec8 ownership, or reduction/barrier ordering.

Non-blocking: the guide still describes the old LayerNorm N == 8192 vector gate and a fixed 256-thread RMSNorm default, while the current gfx95x wide-row default is 512. Also, the direct-indexing cleanup is numerically clean in focused tests, but it is not codegen-neutral for every touched path (for example, LayerNorm BF16 N=3001 uses 32 rather than 30 VGPRs), so the no-codegen-impact claim should be kept narrow.

Comment thread kernels/norm/rmsnorm_common.py
cschenjunlin and others added 2 commits September 21, 2026 18:24
…_str

validate_norm_operand_dtypes compared every operand against Input.dtype but never against dtype_str, so a self-consistent operand set that did not match the builder specialization passed the guard and was reinterpreted bit-for-bit by the kernel: building a bf16 module and passing fp16 tensors produced corrupted residuals and int8 output with no error raised. Take dtype_str in the validator and reject Input.dtype != dtype_to_elem_type(dtype_str) ahead of the existing operand comparison, then pass dtype_str at all eight quantized launchers in rmsnorm_kernel.py and layernorm_kernel.py. Add negative tests covering the four RMSNorm and the four LayerNorm quantized launchers; both fail with DID NOT RAISE ValueError when the validator change is reverted. Update the RMSNorm quantized-variant paragraph in docs/prebuilt_kernels_guide.md, which stated the same Input-relative contract.

Co-authored-by: Cursor <cursoragent@cursor.com>
…kernels

Several claims in the norm sections predate the current kernels. The LayerNorm
vector path is gated on a 16-bit element type and `N % VEC_WIDTH == 0` rather
than the old `N == BLOCK_THREADS * VEC_WIDTH * 4` fast path, and a row that is
not divisible falls back to a fully scalar two-pass body, so both the guide
bullets and the module docstring are rewritten around `USE_VEC_N`. `VEC_ALIGN`
and `USE_NONTEMPORAL` were dropped from layernorm_kernel.py and no longer exist
anywhere under kernels/, so their table rows go away, and `WARP_SIZE` now reads
as `get_warp_size()` resolves it. RMSNorm's `BLOCK_THREADS` defaults to `None`
and resolves through `default_block_threads` to 512 on gfx95x rows of
`N >= 8192`, which the section now states in a table matching LayerNorm and
Softmax. Its forward is two passes with the row held in registers, not three
with an LDS row cache; LDS carries only the cross-wave reduction slots, sized by
wave count rather than by N. Both "Kernel signature" blocks described a
GPU_MODULE_NAME / self / __call__ class API that no longer exists and are
replaced by the actual @flyc.kernel and @flyc.jit signatures.

Co-authored-by: Cursor <cursoragent@cursor.com>
jhinpan
jhinpan previously approved these changes Sep 24, 2026

@jhinpan jhinpan left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@jhinpan

jhinpan commented Sep 24, 2026

Copy link
Copy Markdown
Collaborator

/rerun-ci

1 similar comment
@cschenjunlin

Copy link
Copy Markdown
Contributor Author

/rerun-ci

#1159 (336d6ff) dropped source_ir from CompiledArtifact.__getstate__, so it
reads back as None on any artifact restored from disk and the five norm
assertions on pre-lowering IR dereference it unconditionally. Only
test_rmsnorm_large_shape trips on CI's cold cache: build_rmsnorm_module is a
factory, so each call gets a fresh JitFunction with an empty in-memory cache,
and M is not a builder argument, so it reloads the (8192, bf16) entry
test_rmsnorm wrote earlier in the session. The other four survive only because
their keys are unique to one test; a second run against the same build fails all
five. Guarding loses nothing: manager_key hashes the kernel source through the
launcher's closure, so any edit that could drop the wide copy invalidates the
entry and the assertion runs against a real compile.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cschenjunlin

Copy link
Copy Markdown
Contributor Author

The latest test_rmsnorm_large_shape CI failure

Problem

test_rmsnorm_large_shape fails on all three GPU runners with
TypeError: argument of type 'NoneType' is not iterable in
_assert_rmsnorm_forward_copy_width. It is the only failure in the run
(mi35x 1 failed / 9321 passed, mi325 1 / 7288, navi 1 / 6581), and the assertion
it dies on checks copy width in the pre-lowering IR, not numerics.

Cause

Not the norm changes. #1159 removed source_ir from CompiledArtifact.__getstate__, so it reads back
as None on any artifact loaded from the disk cache. Five assertions that
inspect pre-lowering IR predate that change and dereference it unconditionally:

site checks
test_rmsnorm.py:197 forward copy width, via _assert_rmsnorm_forward_copy_width
test_rmsnorm.py:1629 FP32 weight copy width, small-N tail
test_rmsnorm.py:1720 FP32 weight copy width, staged backward (from #884)
test_rmsnorm_autotune.py:76 known_block_size specialization
test_rmsnorm_autotune.py:81 FP32 weight copy width

The commit message has the mechanism, including why only
test_rmsnorm_large_shape trips on a cold cache.

Fix

A source_ir is not None guard at each site. No production code touched.
Skipping costs nothing: manager_key hashes the kernel source through the
launcher's closure, so any edit that could drop the wide copy invalidates the
cache entry and the assertion runs against a real compile anyway.

Verified on gfx950:

unfixed fixed
cold cache, test_rmsnorm + test_rmsnorm_large_shape 1 failed, 1 passed 2 passed
the four inline assertions (5 test cases), second run against the same build 5 failed 5 passed
rmsnorm + rmsnorm_autotune + layernorm, cold and warm — 58 passed, 3 skipped (both)

@coderfeli
coderfeli merged commit 1bd7ebd into main Oct 9, 2026
16 checks passed
@coderfeli
coderfeli deleted the cjl/norm_perf branch October 9, 2026 00:22
dimitri91209 added a commit to dimitri91209/gfx120x-ops that referenced this pull request Oct 9, 2026
Includes 1bd7ebd (ROCm#848). The gfx120 SmoothQuant and DynamicQuant
quarantine stays off. The guide names the current small-N builder,
_build_rmsnorm_small_n_module.

Signed-off-by: dimitri91209 <dimitri.spencer912@gmail.com>
jhinpan added a commit that referenced this pull request Oct 9, 2026
On gfx1250 the RMSNorm kernels from #848 lose time in four places:

- BF16 outputs use integer rounding, although gfx1250 has
  v_cvt_pk_bf16_f32. The integer rounding also turns NaN payloads
  >= 0x7FFF8000 into +-0.
- The compiler cannot move a load above a store that may alias it. The
  plain and small-N kernels load gamma in pass 2, after the first output
  store, and the fused kernel interleaves its x and residual loads with
  the residual_out stores. Each such load waits one full memory latency.
- At N = 32768 the bounds checks of full tiles raise the plain kernel to
  258 VGPRs, which needs s_set_vgpr_msb bank switches.
- For 16-bit rows with N <= 2048, every M <= 8192 takes one workgroup per
  row. At N = 512 only 64 of its 256 threads have work.

On gfx1250 only:

- has_hw_cvt_pk_bf16_f32() selects the hardware conversion in every
  RMSNorm builder (forward, small N, fused add, fused-add quant,
  backward).
- The plain and small-N kernels load gamma in pass 1. The fused kernel
  issues every x, residual and gamma load before the first residual_out
  store. preload_weight_vec() keeps 16-bit gamma unconverted in
  registers.
- The plain and fused kernels skip the bounds checks of full tiles.
- The small-N module takes the multi-row kernel at every M when each lane
  handles at most three vectors of a row (N <= 768), and for M > N above
  that.

The LLVM IR for gfx942 and gfx950 does not change.
test_rmsnorm_bf16_output_keeps_nan checks that NaN payloads in FP32
weights stay NaN in BF16 outputs on targets with the hardware
conversion.

Signed-off-by: Jin Pan <jin.pan@amd.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants