fix(kernels): never launch an unresolved optional kernel symbol (#180) - #183
Merged
Conversation
Closes #180. The null pointer in the #180 crash is the FUNCTION pointer, not a buffer. In the reported trace `_Cfunc_ccall_wrapper(0x0, ...)`, the 0x0 is Ccall's `fn` argument; the input and output device pointers in the frame above are both non-null, so the "dst was never allocated" hypothesis in the issue is not what happened. launch_repeat_interleave_f32 is an OPTIONAL symbol (purego.go optionalSyms): when dlsym cannot find it, openKernelLib leaves the pointer at 0 and continues, on the documented contract that "callers check before use". RepeatInterleaveF32 checked only `k == nil`, so on any deployment whose libkernels.so predates the kernel it called cuda.Ccall(0, ...) -- a jump to address 0, i.e. SIGSEGV PC=0x0 inside cgo. That kills the process outright, which is why zerfoo's Reshape -> Repeat -> Reshape fallback never ran and why every GQA model died on GPU. The deployment in question is real and measured: the GB10 standing gate puts /opt/zerfoo/lib on LD_LIBRARY_PATH and loads a prebuilt libkernels.so from there rather than building this tree. That library exports launch_repeat but not launch_repeat_interleave_f32. RepeatInterleaveF32 was not a one-off. An AST gate over the package found 34 cuda.Ccall sites launching optional symbols with no zero-check, across 14 wrappers; 22 of those symbols are absent from the currently deployed library, so they are live process kills rather than latent ones. All 34 now check. Also adds IsRepeatInterleaveF32Supported so callers and tests can tell "the fused kernel ran" from "the engine silently fell back", which GPUEngine.RepeatInterleave otherwise hides. Tests (ADR 091): - TestRepeatInterleaveF32_UnresolvedSymbol_ReturnsError reproduces the exact #180 trace with no GPU required. Against the unfixed wrapper it does not fail, it CRASHES the test binary: SIGSEGV: segmentation violation PC=0x0 m=0 sigcode=1 addr=0x0 cuda._Cfunc_ccall_wrapper(0x0, ...) kernels.RepeatInterleaveF32(0x460686809eb8, 0x460686809db8, 1, 2, 2, 8, 2, 0) internal/cuda/kernels/fused_repeat_interleave_purego.go:23 It is paired with a no-library case that must produce a DISTINCT error, so a blanket "always return an error" cannot satisfy it. - TestOptionalKernelSymbolsAreGuarded is the class gate: it parses optionalSyms and walks every wrapper's AST, failing on any optional launch whose function pointer is not compared to 0 first. Red before this change with all 34 sites named; green after. It refuses to pass if its own extraction finds no symbols or no call sites. - compute.TestGPUEngine_RepeatInterleave_CPUParity is the engine-parity harness for the op. Because RepeatInterleave falls back on any error, it asserts the fused kernel is actually present first and FAILS rather than skips otherwise -- verified red against the stale /opt/zerfoo/lib library. Values are compared exactly (a pure gather does no arithmetic; a float tolerance here would repeat the #182 mistake) against a closed-form reference rather than a second ztensor call, and each element encodes its own source coordinate so a wrong head mapping cannot compare equal. A self-test asserts that encoding is discriminating before the comparison is trusted (docs/lore.md L-0009); mutating the reference mapping to q%numKV turns both the self-test and the comparison red. gradcheck (ADR 091 harness 1) is not extended here: it is defined over graph.Node implementations, and RepeatInterleave is an engine-level forward gather with no graph.Node and no analytic Backward in ztensor.
RepeatInterleave falls back to the generic Reshape -> Repeat -> Reshape chain on any failure and returns a correct result either way, so callers cannot tell from its output whether the fused kernel ran. zerfoo needs that distinction to assert its restored fused GQA path is genuinely fused rather than quietly measuring the fallback, and kernels.IsRepeatInterleaveF32Supported lives under internal/ where zerfoo cannot reach it. Mirrors the existing kernels.IsPagedAttentionSupported usage in compute/gpu_paged_gqa.go.
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.
Closes #180.
Why
RepeatInterleaveF32SIGSEGVs the whole process on the GB10, taking down everyGQA model on GPU (Llama, Mistral, Qwen, Gemma). zerfoo currently ships a
mitigation that disables the fused path entirely (zerfoo#980), paying a
performance cost.
Root cause
The null pointer is the function pointer, not a buffer. The issue guessed
an unallocated
dst; the trace says otherwise. In0x0isCcall'sfnargument. Theinputandoutputdevice pointers oneframe up are both non-null.
launch_repeat_interleave_f32is an optional symbol.purego.go'soptionalSymsset exists so a library missing a newer kernel is not fatal; ondlsymfailureopenKernelLibleaves the pointer at0and continues, on thestated contract that "callers check before use"
(
internal/cuda/kernels/purego.go:496-497).fused_repeat_interleave_purego.go:19-26checked onlyk == nil. It neverchecked
k.launchRepeatInterleaveF32 == 0, so it calledcuda.Ccall(0, ...),a jump to address 0 —
SIGSEGV PC=0x0inside cgo. That is a process kill, notan error return, which is exactly why zerfoo's
Reshape -> Repeat -> Reshapefallback never got a chance to run.
Why the symbol is missing there, measured rather than assumed. The standing
gate pod (
zerfoo docs/bench/manifests/validate-arm64.yaml) does not buildkernels from source; it sets
LD_LIBRARY_PATH=/usr/local/cuda/lib64:/opt/zerfoo/liband
kernelLibPaths[0]("libkernels.so") resolves to the prebuilt library there:That library (built 2026-07-03) exports 79 symbols; a build of current
mainexports 130. The kernel source, the header, and the Makefile
SRCSentry wereall fine — only the deployed binary was behind.
Scope: this was a class, not a one-off
An AST gate over the package found 34
cuda.Ccallsites launching optionalsymbols with no zero-check, across 14 wrappers. Cross-referencing against the
pre-refresh deployed library, 19 of those symbols were absent from it, i.e.
they were live process kills rather than latent ones; the other 15 are latent
(that library happened to export them). 20 sibling wrappers already had
the guard (
flash_decode_purego.go:40,gemv_warp_purego.go:20,fp4_gemv_purego.go:42, ...), so this restores the intended, already-establishedpattern rather than inventing one. All 34 sites now check; the diff is the same
three lines each time.
What changed
symbol instead of jumping to 0.
IsRepeatInterleaveF32Supported()(matching the existingIsQ4KSm121Supported()pattern), becauseGPUEngine.RepeatInterleavefallsback on any error and otherwise gives callers and tests no way to tell "the
fused kernel ran" from "we silently took the slow path".
optionalSymscomment now states the invariant and names the gate.Verification (ADR 091)
Red first, then green. Against the unfixed wrapper,
TestRepeatInterleaveF32_UnresolvedSymbol_ReturnsErrordoes not fail — itcrashes the test binary, reproducing #180 exactly, with no GPU required:
The class gate was red with all 34 sites named:
After the fix, on the GB10:
These assertions were checked for sensitivity, not just for green. This repo
has a documented habit of gates that pass with real bugs present (a parity
harness that loaded the same file per row, #182's tolerance 1000x looser than
the measured error, a softmax that saturated until the test was position-blind
— lore L-0009/L-0018), so each assertion was made to fail on purpose:
RepeatInterleavefalls back to the generic chain on any error, so avalue-only parity test would go green while the fused kernel never ran. The
test asserts the kernel is present first and fails rather than skips.
Verified by pointing
LD_LIBRARY_PATHat the stale/opt/zerfoo/lib:launch_repeat_interleave_f32 is not in the loaded libkernels.so; ... this test would prove nothing about the fused kernel.(b, kv, s, d)and a self-test asserts that encoding actually distinguishesheads before the comparison is trusted. Mutating the reference mapping from
q/reptoq%numKVturns both red (12544/14336 elements differ, plusreference is position-blind: output heads 0 and 2 are identical).and performs no arithmetic, so any nonzero difference is a real defect.
distinct error, so an unconditional
return errorcannot satisfy it.(
if len(fieldForSym) < 100 { t.Fatalf(...) },if callSites == 0 {...}).No regressions.
go test ./internal/cuda/...on the GB10 host produces afailure set byte-identical to clean
mainin the same environment (118failures both sides,
diffclean). Those failures are environmental — thishost is not the gate pod, and several are tests that assert error returns "without
CUDA" on a machine that has CUDA. The standing gate pod remains the authority.
gradcheck (ADR 091 harness 1) is deliberately not extended: it is defined over
graph.Nodeimplementations, andRepeatInterleaveis an engine-level forwardgather with no
graph.Nodeand no analyticBackwardin ztensor. Theclosed-form reference used here is a stronger oracle for a pure gather than
torch would be, since there is no floating-point convention to diverge on.
Risk
Behaviour only changes where the process previously died. Every new branch
converts a crash into an error return on a path that already had an error
return, and every affected caller already handles it (
GPUEngine.RepeatInterleavefalls back to
Repeat). Thecuda-tagged build is unaffected apart from thenew always-true
IsRepeatInterleaveF32Supported.Follow-ups, not done here
/opt/zerfoo/lib/libkernels.soon the DGX is 7 weeks behindmain(79 exported symbols vs 130). This fix makes a stale library safe,but the fused GQA kernel cannot actually run until that library is
refreshed. Refreshing it re-activates roughly 50 currently-dormant optional
kernels at once, which is a much wider blast radius than this PR and wants
its own validated rollout.
nsa_attention_f32,kv_dequant_*,iq_dequant_*are declared and resolvedin
purego.gobut have no.cuinSRCSand no Go caller. Dead entries;left alone.