Skip to content

Fix inference layer_norm/rms_norm launching no kernel for fp32 hidden sizes 65-128 - #8827

Open
jayden1711 wants to merge 2 commits into
deepspeedai:masterfrom
jayden1711:fix/inference-norm-threads-per-group-32
Open

jayden1711 wants to merge 2 commits into
deepspeedai:masterfrom
jayden1711:fix/inference-norm-threads-per-group-32

Conversation

@jayden1711

Copy link
Copy Markdown

Fixes #8826

Summary

For float32, one 16-byte load holds 4 elements. So hidden sizes 65..128 take the elems_per_row <= 128 sub-block schedule with threadsPerGroup = next_pow2(ceil(H / 4)) = 32. The dispatch in that branch only handled 1/2/4/8/16 threads, so no kernel was launched and the output tensor (at::empty_like) was returned uninitialized. This PR adds the missing threadsPerGroup == 32 branch to the four sub-block launchers in csrc/transformer/inference/csrc/:

  • launch_fused_ln, launch_fused_residual_ln and launch_fused_residual_ln_store_pre_ln_res in layer_norm.cu
  • launch_rms_norm in rms_norm.cu

reduce::partitioned_block<..., 32> already supports 32 threads (a full-warp reduce). fp16/bf16 (8 elements per load) never reach 32 threads in this branch, so their behaviour is unchanged. +8 lines, no other changes.

End-to-end evidence

deepspeed.init_inference(model, dtype=torch.float32, replace_with_kernel_inject=True) on google/bert_uncased_L-2_H-128_A-2 (BERT-tiny, hidden 128), with last_hidden_state compared against a float64 Hugging Face run. Script and outputs: https://gist.github.com/jayden1711/c81e4ba679caa804ab6cf1a91a9019b9

  • before: the encoder output is garbage. Max error is 4.7-4.8 (outputs are about that size), and the minimum per-token cosine similarity ranges from -0.22 to 0.17 across runs, because the uninitialized memory differs from run to run.
  • after: min per-token cosine 0.999999 (max error 4.7e-3, DeepSpeed's normal fp32 inference error level; a hidden-256 BERT, which this bug does not affect, shows the same level).

Tests

I extended the existing parametrization in tests/unit/ops/transformer/inference/test_layer_norm.py and test_rms_norm.py from channels in [384, ..., 14432] to also include 64, 96 and 128. The existing tests only used channels >= 384, so they could not reach this branch.

Commands (from tests/, matching the nv-inference workflow's marker):

pytest -m inference_ops unit/ops/transformer/inference/test_layer_norm.py
pytest -m inference_ops unit/ops/transformer/inference/test_rms_norm.py
master (5f266e2) this PR
test_layer_norm.py (extended) 8 failed, 596 passed 604 passed
test_rms_norm.py (extended) 16 failed, 184 passed 200 passed

All 24 failures on master are float32 at channels 96 and 128 (test_layer_norm, test_rms_norm, test_pre_norm). All the original channel sizes pass before and after, and so does channel 64 (it maps to 16 threads).

Note on the residual layer-norm tests: test_layer_norm_residual and test_layer_norm_residual_store_pre_ln_res pass at 96/128 even on master. That is because LayerNormOp.layer_norm_residual and layer_norm_residual_store_pre_ln_res in op_binding/layer_norm.py are pure PyTorch (F.layer_norm), so those tests never call launch_fused_residual_ln*. I checked the two residual launchers directly through the CUDA bindings (inference_module._layer_norm_residual and inference_module.layer_norm_residual_store_pre_ln_res), fp32, shape (32, 128, H). Before each call, the allocator was pre-filled with NaN blocks so that an unwritten output shows up as NaN:

H master this PR
64 correct (max err 1.9e-6) correct (1.9e-6)
96 all outputs NaN (not written) correct (1.4e-6)
128 all outputs NaN (not written) correct (1.4e-6)
256 correct (1.9e-6) correct (1.9e-6)

launch_fused_residual_ln is also used by the injected MLP path (mlp_after_attn in pt_binding.cpp), which is how BERT-tiny reaches it. I did not change the residual tests to call the CUDA bindings. If you want them covered by CI, that would be a small follow-up in this PR or a separate one.

The InferenceBuilder op was JIT-built separately from master and from this branch (separate TORCH_EXTENSIONS_DIR, TORCH_CUDA_ARCH_LIST=12.0, MAX_JOBS=4), and each run logged which transformer_inference.so it loaded.

Hardware: 1x NVIDIA GeForce RTX 5080 Laptop GPU (sm_120), driver 595.99.02, CUDA 12.8, torch 2.11.0+cu128, Python 3.12.3. Only this GPU was tested.

Compatibility matrix: this only touches the inference (v1, kernel-injection) layer_norm/rms_norm CUDA kernels. Validated: fp32/fp16/bf16 through the extended unit tests, and fp32 end-to-end on BERT-tiny. Not applicable: ZeRO stages, offload, training. Not validated: other GPUs/architectures and ROCm.

Duplicate and sibling-path checks

  • gh issue view 8826 --comments: no comments. gh pr list --state open --search "8826 in:body": none.
  • Searches for layer_norm threadsPerGroup, inference layer norm, rms_norm, layer_norm.cu and threadsPerGroup: the only related open PR is Return early from inference norm launchers when rows == 0 #8743 ("Return early from inference norm launchers when rows == 0", for Inference layer_norm / rms_norm kill the process (SIGFPE) on an empty batch #8742). It touches the same launchers and test files but fixes a different bug (empty batch) and does not add the 32-thread branch. Whichever lands second may need a trivial rebase.
  • Sibling paths: the inference v2 kernels deepspeed/inference/v2/kernels/core_ops/cuda_layer_norm/layer_norm_cuda.cu and cuda_rms_norm/rms_norm_cuda.cu have the same 1/2/4/8/16-only dispatch and list float32 as supported. They look affected too (by code reading; not run). I left them out to keep this PR to the reported v1 path and can add the same branch there if you want. csrc/quantization/quantize.cu has a similar ladder, but I did not check whether it can reach 32 threads.

Notes

  • Formatting: pre-commit run --files <changed files>. All hooks pass, including yapf, flake8, codespell, check-license and clang-format (clang-format 23.1.1 was put on PATH, since the hook uses the system binary).

AI assistance was used to prepare this change (code, tests and this description). I reviewed every changed line and ran the tests above.

…ng threadsPerGroup==32 launch)

Fixes deepspeedai#8826

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-15T10:34:41.461910Z f8386f9 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f8386f9296

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

} else if (threadsPerGroup == 16) {
LAUNCH_FUSED_LN(1, 16, maxThreads);
} else if (threadsPerGroup == 32) {
LAUNCH_FUSED_LN(1, 32, maxThreads);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Make 32-thread launches tail-safe

When float32 hidden size is 96 or 128 and rows > 8 is not divisible by 8 (for example an input shaped (1, 9, 128)), this branch uses the scheduler above to launch block=(32, 8) with grid=ceil(rows / 8). fused_ln receives no row count and unconditionally processes every threadIdx.y, so the last block reads and writes up to seven rows beyond the input/output allocations; the residual and RMS branches added here have the same layout. The new tests only exercise row counts 1, 32, 128, and 4096, so make these launches tail-safe before enabling them.

AGENTS.md reference: AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Confirmed with compute-sanitizer memcheck: at fp32 H=96/128 with rows 9/13/17/33, the new 32-thread launches overran the last block (70-224 errors per kernel). Fixed in the latest commit by launching one row per block (grid(rows), block(32)), as the >128 path does; memcheck now reports 0 errors across all 40 cases (5 bindings x H 96/128 x rows 9/13/17/33). Tests now include seq_len 9 and a direct test of the CUDA residual bindings.

Note: the existing 16-thread (and 8-thread) branches on master have the same pre-existing overrun when rows exceed the per-block row count and aren't a multiple of it. I'll file that separately.

… in the kernels)

Addresses review: partially filled multi-row blocks read/wrote past the last row. Adds seq_len 9 and a direct CUDA residual test.

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>

This branch has not been deployed

No deployments
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.

[BUG] Inference layer_norm / rms_norm return uninitialized memory for float32 inputs with hidden size 65-128

1 participant