Repository navigation
test(prm): run #1235's CPU bf16/fp16 steps on ATen's matmuls, fixing windows-latest 0xc000001d - #1314
Merged
Merged
Conversation
main (0334b97, CI run 36251313755) died on windows-latest 3.10 and 3.12 with `Windows fatal exception: code 0xc000001d` in test_one_step_runs_and_moves_the_norms_and_projections[docs-lr], the first test to run a real step under CPU bf16 autocast. The faulting frame is torch/nn/modules/linear.py:134 (q_proj of layer 0) under torch/amp/autocast_mode.py: the first bf16 matmul of the process. On x86, torch 2.14 routes a bf16 matmul with m*n*k > 16**3 to oneDNN whenever oneDNN reports bf16 for the CPU (AVX-512 or AVX2-VNNI-2), and to ATen's precompiled gemm otherwise. The same hosts ran fp32 training (MKL) and bf16 element-wise ops (ATen) without a fault, so the one run-time-dispatched code on the crash path is oneDNN's JIT kernel. torch.backends.mkldnn.enabled gates both oneDNN routes (mkldnn_gemm and the SDPA brgemm could_pack check). The new onednn_off fixture (tests/conftest.py) switches that flag off for the step through monkeypatch. The tests still run on every cell, under the same autocast; only the matmul library changes, which is the path a box without AVX-512 always took (ONEDNN_VERBOSE=1 logs no primitive for these tests on the dev box). The fp16-autocast test gets the same treatment: it was never reached on the crashing hosts, and fp16 is routed the same way on AVX512-FP16 hosts. The tests assert the flag was off, and a last test asserts it is back on outside the fixture. Test-only change: no changelog fragment. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
oneDNN is not the only library in a CPU bf16 step that picks its instruction set at run time. The fused CPU SDPA kernel multiplies its bf16/fp16 blocks with MKL's cblas_gemm_bf16bf16f32 / cblas_gemm_f16f16f32, which the oneDNN flag does not gate: MKL_VERBOSE=1 over the six bf16/fp16 tests on main logs 768 GEMM_BF16BF16F32 and 384 GEMM_F16F16F32 calls. On the crashing hosts the oneDNN matmul in q_proj simply ran first, so switching oneDNN off alone could have moved the crash into attention. The fixture, renamed aten_half_matmuls, now also puts SDPA on its math backend, which upcasts q/k/v to fp32 on CPU (_scaled_dot_product_attention_math in aten/src/ATen/native/transformers/attention.cpp). With it the same six tests log 0 half-precision MKL GEMMs and 0 oneDNN primitives: only SGEMM and SGEMM_BATCH, the fp32 MKL path the crashing hosts already run without fault, plus ATen's own precompiled bf16 gemm. The tests assert both switches were in effect, and the scoping test checks both are back on afterwards. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The fixture moves half-precision matmuls to ATen, but attention goes to fp32 SGEMM in MKL, not to ATen. The docstrings now say both. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MakazhanAlpamys
added a commit
that referenced
this pull request
Sep 27, 2026
…-precision steps on ATen's matmuls Review follow-ups for #1312, after rebasing onto main with #1314. - setup() creates the fp32 gate on the device of the base's parameters instead of leaving it on the CPU for the Trainer to move. A base loaded to the CPU is still moved by the Trainer, gate and all. A pre-quantized bitsandbytes checkpoint is placed on its GPU at load, and neither Trainer._move_model_to_device nor Accelerate's prepare_model moves a 4/8-bit model, so a CPU gate would have met a CUDA base in compute_loss (main put the gate on "cuda" itself). New test: the base loaded on the meta device, standing in for that GPU, gets a meta fp32 gate. - The seven test functions that run a real CPU bf16/fp16 step request aten_half_matmuls (#1314) and assert it was in effect after the step: cpu_run requests it, and the fp16 GradScaler test, which does not use cpu_run, requests it directly. On the "cuda" branch the frozen base is bf16, so these steps multiply bf16 matrices above oneDNN's m*n*k > 4096 cutoff (85-680 per test) and run the fused CPU SDPA (6-48) even without autocast, the kernels that kill part of the windows-latest fleet with 0xc000001d. The setup-only, fp32, routing-cast and old-bf16-file tests run neither and are left alone. - docs/training.md: the gate's file size as 4 x hidden x N bytes (about 7 KB for the example, 2 MiB at 8192 x 64) instead of "a few kilobytes", and the stale "there is no serve-time MoLE path yet" sentence replaced with a pointer to soup serve --mole. - The test docstrings no longer describe #868 as open or train() as passing no precision flags; cpu_run's flags override whatever train() asks for. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This was referenced Sep 27, 2026
MakazhanAlpamys
added a commit
that referenced
this pull request
Sep 28, 2026
…vice (#1266) (#1312) `task: moe_lora_routing` trains one tensor, the router gate `Linear(hidden, N)`. On the device string `"cuda"`, which is what `soup train` passes, `setup()` cast that gate to bf16. `train()` ran with no autocast and kept no fp32 copy, so the gradient and both AdamW moments were bf16 as well. An AdamW step of about `lr` is below half a bf16 ulp for every `|w| >= 2^-7`, and the gate's default init puts most of its weights there. After 8 steps at the default `lr` (2e-5), 91.4% of the gate was bit-identical at hidden 64 and 82.6% at hidden 576, and training longer did not help. `"cuda:0"` kept the gate fp32, because the check was `== "cuda"`. The gate is now created as an fp32 master weight on every device string, on the device of the base's parameters. The dtype is explicit, so torch's process-wide default dtype cannot change it. The placement means: - a base loaded to the CPU is moved by the Trainer, gate and all; - a pre-quantized bitsandbytes base, which is placed on its GPU at load and which neither the Trainer nor Accelerate moves, already has the gate beside it. `mole_gate.pt` is saved with every floating tensor cast to fp32, so a DeepSpeed engine that holds the module in 16-bit cannot write a 16-bit gate. `compute_loss` already cast the router input to the gate's dtype and the routing weights back to the logits' dtype; tests now pin both casts. The frozen base and the task LoRAs load as before. `docs/training.md` says the gate trains and is saved in fp32 and gives the file size. It also replaces a stale "no serve-time MoLE path" sentence with a pointer to `soup serve --mole`. The new tests' CPU bf16/fp16 steps request `aten_half_matmuls` (#1314) and assert it was in effect. On the CUDA branch the frozen base is bf16, so even a step without autocast runs the half-precision kernels that kill part of the `windows-latest` fleet with `0xc000001d`. What users notice: - On CUDA the whole gate trains. A gate trained on CUDA before this fix kept most of its initial weights, so retrain it. - `mole_gate.pt` is now fp32: `4 x hidden x adapters` bytes, about 7 KB for the docs example. - `soup serve --mole` loads the new file and an older bf16 file alike, and on CUDA serving still casts the gate to bf16. - The gate's weight, gradient and two moments take 16 bytes per parameter instead of 8, which is 8 MiB at hidden 8192 with 64 adapters. Verified on CPU: - The unfixed trainer fails 15 of the 24 new tests, reproducing the issue's 117/128, 951/1152 and 333/1152 bit-identical counts. With the fix, 0 of 128 and 0 of 1152 gate elements are left unmoved. - One-edit mutations were killed across the gate dtype, the gate placement, both routing casts and the save cast. The one survivor, dropping the `is_floating_point()` guard in the save, is equivalent, since the gate has no non-float state. - Taking `aten_half_matmuls` out of the step tests fails 11, so the in-effect assertions are not vacuous. - 1368 tests pass (8 skipped, 0 failed) across the new file and 33 neighbouring files, including the #1235 and #382 guard files. - A freshly trained fp32 gate and an old bf16 gate both load and generate through `load_mole_for_serve`. On a real RTX 5070 (sm_120), no gate element was left bit-identical after 8 steps, where `main` left 951 of 1152. Forcing `fp16=True` fails on `main` with the GradScaler's bf16 unscale error; with this change it trains the whole gate. This lands before #868, whose MoLE `bf16`/`fp16` flags need an fp32 gate. Closes #1266.
MakazhanAlpamys
pushed a commit
that referenced
this pull request
Sep 30, 2026
…f skipping (#1401) Ten real-`train()` tests were skipped on every windows-latest cell under #382, although #1314 had traced that `0xc000001d` crash to CPU bf16/fp16 matmuls that its `aten_half_matmuls` fixture removes, and `_windows_ci()` still blamed bitsandbytes for all of them. Nine of the tests (eight in `test_rewind_hf.py`, the GRPO step in `test_v05311.py`) now request `aten_half_matmuls` instead of skipping. The tenth, `test_hf_training_run_records_every_row_once`, trains fp32 and just loses its skip. `KNOWN_CALL_SITES` and `GUARDED_TESTS` shrink to the two NF4 tests in `test_v07202.py`. A new `FIXTURE_TESTS` check fails if any of the nine stops requesting the fixture or also carries the skip, and the `_windows_ci()` docstring now says which cause belongs to which tests. Users notice nothing: this is tests only. The windows-latest cells now run ten tests they used to skip. Verified on a Windows box with `CI=true` set so the skip would have fired: 147 passed, 3 skipped across the guard file, the narrow-skip class and the three touched test files, all ten formerly skipped tests running; on clean `main` the new guard file fails 9. Six of eight mutations fail a named test, and the other two are the fixture made a no-op (caught by the existing `test_issue1235` fixture self-test) and a skip re-added to the fp32 test (deliberately unpinned). On the previous head the three windows-latest cells were green with all ten tests PASSED and no `0xc000001d`. Closes #1359.
3 tasks
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.
Summary
mainhas been red since #1250 merged:test (windows-latest, 3.10)andtest (windows-latest, 3.12)in run 36251313755 (head
0334b975) die withWindows fatal exception: code 0xc000001d(STATUS_ILLEGAL_INSTRUCTION) in the first test of
tests/test_issue1235_prm_fp32_master_weights.pythat runs a real CPU bf16-autocast step. The crash kills the interpreter, so each cell reports nothing
about the 22,433 of its 25,318 selected tests it had not reached.
This keeps the tests running on every cell instead of skipping them. A new conftest fixture,
aten_half_matmuls, moves the step's half-precision matrix multiplies off the two libraries that picktheir instruction set at run time:
torch.backends.mkldnn.enabled = False, so bf16/fp16 matmuls run in ATen's own precompiledgemminstead of oneDNN's JIT kernels;
cblas_gemm_bf16bf16f32/cblas_gemm_f16f16f32.The dtypes #1235 pins (fp32 master weights, the reward head taking the forward's dtype, the steps moving
the norms) do not depend on which library multiplies the matrices, and autocast stays on.
This cannot be proven before merge. Which runner a Windows job lands on is a lottery: #1250's own
run passed on all three Windows cells, so a green run on this PR proves nothing either way. What
supports the fix is the elimination below. If
0xc000001dcomes back in these tests with the fixtureon, the fallback is the #382 skip (
skip_on_windows_ci) on these tests, withtests/test_issue382_windows_ci_guard_is_shared.pyupdated to match.Test-only change: no changelog fragment, and no
src/change.Root cause: evidence
The crash. Both cells die in
TestBf16MixedPrecisionTrains::test_one_step_runs_and_moves_the_norms_and_projections[docs-lr], rightafter the eight
TestTheLoadDtypetests (setup only, no forward) pass. The faulthandler frame is thefirst bf16 matmul of the step:
No bitsandbytes and no Soup native code are on the stack (Soup has none). Stack: torch 2.14.0 (the CPU
wheel from PyPI), transformers 5.17.0, accelerate 1.15.0, image
windows-2025-vs202620260922.246.2.Which hosts. The fault depends on the host, not the code or the interpreter:
dc042a4eca14f096main0334b975* Both failed an
== 0.0assertion that #1250 then relaxed to<= 0.01: 1 of 320 norm elementsunmoved. Both logged the same loss and grad-norm sequence, and it matches neither ATen path on the dev
box (AVX2:
0.5018 / 47.63at step 2; forcedDEFAULT:0.9504 / 69.96at step 4, against their0.5025 / 47.65and0.9517 / 70.08). So those hosts ran run-time-dispatched half-precision kernels,without a fault.
The ubuntu and macos cells have never crashed on these tests, or on the other CPU bf16 steps below.
Which code. On x86, torch 2.14 routes a CPU bf16 matmul as follows (source at
v2.14.0):cpublas::gemm(bf16 in, bf16 out) callsmkldnn_bf16_gemmfirst(
aten/src/ATen/native/CPUBlas.cpp).mkldnn_gemmtakes it whenuserEnabledMkldnn() && mkldnn_bf16_device_check()andm*n*k > 16**3(
aten/src/ATen/native/mkldnn/Matmul.cpp). The device check isideep::has_bf16_type_support(),which is true on AVX-512 and AVX2-VNNI-2 hosts. Everything else falls to ATen's
gemm_stub(
native/cpu/BlasKernel.cpp: templated C++, no BF16 intrinsics, compiled per ATen capability).cpublas::gemm, whichgoes to MKL's
cblas_gemm_bf16bf16f32/cblas_gemm_f16f16f32when MKL has them. The oneDNN flagdoes not gate that route.
oneDNN and MKL both pick the instructions they execute at run time from their own CPU probe. The
precompiled ATen kernels are selected once, through cpuinfo, from the OS-enabled feature set. So the
question is which of these the crashing hosts could not run:
35254266989),
test_issue341_seed_and_fullft.py::TestFullFinetuneTrainer(a realtrainer.train(),and Soup's CPU flags are
bf16=False, fp16=False) passed at 12%. On today's cells the real fp32GRPO steps of
test_issue1232_grpo_variant_kl.py::TestTheRealTrainerpassed right before the crash.So MKL's SGEMM runs there.
test_issue722_chunked_distill.py'sbfloat16cases (log-softmax/KL, no matmul) passed at 20%.test_rewind_hf.py::test_recorded_rows_match_sampler_orderat 41%, the firsttrainer.train()undertrl's
SFTConfig. trl 0.29.1 defaultsbf16=Trueeven withuse_cpu=True; this was checked, andGRPOConfigdoes the same, which covers the 2026-08-31 GRPO crash intest_v05311.pyas well.Today it is PRM on CUDA trains bf16 weights with no fp32 master copy: the first bf16 step fails with a dtype mismatch, and with that patched the base updates round to zero #1235's first bf16 step.
What is left is the run-time-dispatched half-precision matmul code. Which instruction faults cannot be
read off these logs: the job logs do not name the CPU model, and a 0xc000001d dump carries no
registers. oneDNN 3.12 (bundled with torch 2.14) does gate AMX on Windows by
XCR0[18:17]. A oneDNNAMX matmul has produced exactly this signature before (pytorch/pytorch#99509). An unrelated project hit
the same thing on 2026-09-25:
nn.Linearunder CPU bf16 autocast, 0xC000001D, on somewindows-latesthosts, gone on a rerun (maged15/beamgrad#12). The fix below does not depend on which instruction it is.
Why this cannot be reproduced on the dev box, and what the fix removes. The dev box is an
i9-14900HX (AVX2 + AVX-VNNI, no AVX-512). There
torch.ops.mkldnn._is_mkldnn_bf16_supported()isFalse, so bf16 matmuls never reach oneDNN.
ONEDNN_VERBOSE=1logs 0 primitives across the sixbf16/fp16 tests. The positive control works: an fp32 conv2d logs primitives, and they stop inside
mkldnn.flags(enabled=False).MKL_VERBOSE=1across the same six tests:GEMM_BF16BF16F32GEMM_F16F16F32SGEMMSGEMM_BATCHmainWith the fixture, the step's half-precision matmuls run in ATen's
gemm_stub, and attention runs asfp32 SGEMM, the MKL path the crashing hosts already run without fault.
Why this option
skip_on_windows_ciwould stop the crash for certain, but it would drop thesefive cases on every Windows host, including the ones that run them fine (windows 3.11 on the same
mainrun, and all three on fix(prm): train PRM on fp32 master weights on every device (#1235) #1250's run).ONEDNN_MAX_CPU_ISAandMKL_ENABLE_INSTRUCTIONSareprocess-wide and read once when the library initialises, so they cannot be scoped to one test. They
would change every test on the Windows cells, and choosing the cap needs the faulting ISA, which the
logs do not show.
tests is the same on all nine cells. It uses
monkeypatchplus thesdpa_kernelcontext manager, soboth switches are restored after each test.
Changes
tests/conftest.py: theaten_half_matmulsfixture, with the evidence above in its docstring.tests/test_issue1235_prm_fp32_master_weights.py:precisionrequests the fixture, which covers the four bf16 cases and the fp16-flag case.and fp16 goes through the same two routes.
TestTheAtenMatmulSwitchStaysScoped, placed last in the file, asserts that both switches areback on outside the fixture.
tests/_windows_ci.py: the module docstring points at the fixture as the first thing to try whenthe crash frame is a CPU bf16/fp16 matmul.
Verification (dev box,
PYTHONPATH=<worktree>/src,CUDA_VISIBLE_DEVICES=-1)tests/test_issue1235_prm_fp32_master_weights.py: 25 passed (was 23; +2 scoping tests), and25 passed again with
CI=true, so nothing is skipped on a Windows runner.tests/test_issue382_windows_ci_guard_is_shared.py,tests/test_v07202.py::TestTheWindowsCiSkipStaysNarrow,tests/test_issue833_gpu_marker.py:60 passed.
ruff check src/soup_cli/ tests/ scripts/ benchmarks/: clean.tests fail with the intended message:
precisionwithout the fixture: 5 failed.What this cannot prove before merge. The fault is a host lottery. #1250's own run passed on all
three Windows cells, so a green run on this PR is not evidence either way. The evidence is the
elimination above. If this signature comes back in these tests with the fixture on, the faulting code
is not oneDNN's or MKL's half-precision GEMM. The fallback is then
skip_on_windows_ci, with theguard-sharing ratchet updated.
Open PRs that reach the same kernels
fix(mole),task: moe_lora_routingon CUDA trains the router gate in bf16 with no fp32 master copy: at the default lr, 82-91% of the gate's weights never move #1266):tests/test_issue1266_mole_gate_fp32_master.pyruns real CPUwrapper.train()steps with a bf16 frozen base (thedevice="cuda"branch loads the base in bf16, sothe forward runs bf16 matmuls even without autocast), under
cpu_run(bf16=True), and in a CPU fp16autocast GradScaler step, at hidden sizes 64 and 576. That is the same class of kernel. It should
request
aten_half_matmulsincpu_run, in the fp16 test, and in the other tests that train on thebf16 base.
is
m*n*k<= 960, under oneDNN's 4096 threshold, and there is no SDPA. Low risk.Linear(8, 8)under bf16 autocast on a (2, 8) input,m*n*k= 128. Low risk.gpumarker): not affected. feat(trainer): robustly handle multi-turn, ShareGPT, and Alpaca formats in GRPO dataset preparation #1153's diff is toolarge for the API (>20k lines); its delta against
mainadds no CPU half-precision test code.Follow-ups (not in this PR)
test_rewind_hf.pyandtest_v05311.py::test_real_trl_grpotrainer_end_to_end_stepskip on WindowsCI under CI: windows-latest cells die with 0xc000001d in the NF4 training-step test — three occurrences, now on main #382. Their crash sites are this same CPU bf16 matmul class, because trl's configs default to
bf16. They could request
aten_half_matmulsand drop the skip, which would restore their Windowscoverage.
tests/_windows_ci.pystill attributes the whole signature to bitsandbytes. That fits only theoriginal NF4 site; the 2026-09-18 comment on CI: windows-latest cells die with 0xc000001d in the NF4 training-step test — three occurrences, now on main #382 already noted this for the rewind crashes.
🤖 Generated with Claude Code