Repository navigation
On Metal, the shared-node half of a take is a call, not code inlined into the taken match - #1978
HamedShams wants to merge 2 commits into
Conversation
…into the taken match On an M1 and an M1 Max under macOS 27.2, a program whose taken match sits on a static constructor failed every run on Metal with "a function the device does not hold", sometimes "frontier drained without a result", once a hang; --gpu off and the interpreter agreed with each other. The same source runs right on the same M1 under macOS 15.6.1 and on an M4 Pro under 26.5.2, so the trigger is the code the newer Metal compiler emits. At its default level it inlines ctr_take, and with it the shared-node branch (acquire, free or fade), into the taken match; the shader came out five times bigger and wrong. The shared-node branch moves into span_fade, which already is the shared path's out-of-line function, and span_fade is a real call on Metal (FAR) instead of a static the compiler inlines anyway. On the host and on CUDA the code is unchanged in meaning: OUTLINE as before, with the last-holder free now inside it. The reproducer passes 50/50 at the default level, 10/10 under --gpu 1GB, cleanly under MTL_DEBUG_LAYER=1, and its shader shrinks from 1011 KB to 304 KB. The 17 runtime benches give their checksums on GPU and CPU; lexer, the one bench that walks a static string through ctr_take, runs 5 % faster on the M1 Max (3.06 s to 2.91 s). Rejected: compiling the whole device program at MTLLibraryOptimization- LevelSize passes too but costs raytrace 17 % and gates on the GPU family when the data points at the compiler version; a noinline ctr_take passes but costs lexer 32 %, one call per character.
|
I independently confirmed both the optimization-level finding and the fix in #1978 on my Apple M1 (7-core GPU, macOS 27.2, build 26B123). Using isolated builds of the same generated C:
With GPU task-dispatch instrumentation, the default build produced invalid task words containing floating-point-like patterns, while the size-optimized build produced no invalid task captures across 31 runs. I then tested the PR base (
The builds were isolated, with separate shader archives. The original reproducer was checksum-verified, and the PR head also passed when compiling the shader from source on every run. 15 of 17 benchmarks produced matching GPU results between base and head, consistent with CPU output, with execution times within observed noise. The remaining two ( This independently confirms that #1978 resolves the reproducer's failure on a second M1-family machine without changing Metal's optimization level. Thanks again for tracking this down! |
|
@raramos9 thanks for running it, that's the second machine the PR needed. The |
Merged onto main 1cecb12 the PR took comp.ts to 66,032 of 66,000 ttok. FAR on the host and CUDA too, instead of the Metal-only #ifdef: CUDA's OUTLINE already is FAR, and the host loses only cold and preserve_most on a path that frees or fades a shared node. comp.ts 66,000; gates test 1799/1799, perf 124/124, ping 81/81 on an M4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@HamedShams merged onto main 1cecb12 the PR put bend2/comp.ts at 66,032 of its 66,000 ttok cap, so I pushed fd8fafb on your branch: span_fade is plain FAR on every target instead of the Metal-only #ifdef (CUDA's OUTLINE already is FAR; the host only loses cold/preserve_most on the shared-node path), with a shorter comment. comp.ts is now exactly 66,000; on an M4 test 1799/1799, perf 124/124, ping 81/81, safe unchanged. On Metal it is the same FAR as your version, so it should behave the same, but I can't check that without an M1 on 27.2. Could you (or @raramos9) rerun the #1954 reproducer on this head? If it fails, please just revert fd8fafb and tell us, and we'll find the tokens elsewhere. |
|
@Lorenzobattistela fd8fafb passes here, same M1 Max as before (32-core GPU, macOS 27.2 build 26B123, Xcode 27.1).
Nothing to revert. |
Fixes #1954.
What fails. On an M1 Max (macOS 27.2, Xcode 27.1) the reproducer from the issue fails every run on Metal, on current main and back to v2.0.37: mostly
a function the device does not hold, sometimesfrontier drained without a result, once a hang.--gpu offand the interpreter print57798656. The same program passed 466/466 on raramos9's M1 under macOS 15.6.1 and fails 400/400 on that same M1 after its update to 27.2; on Lorenzo's M4 Pro under macOS 26.5.2 it passes 100/100. So the trigger is the machine code the macOS 27.2 Metal compiler produces, seen so far on the M1 and the M1 Max; whether an M4 on 27.2 is affected is unknown.Where. Bisected on the failing machine (every step rebuilt from main and run 10 times):
Seginstead of the constant passes, and its shader is 208 KB instead of 1011 KB; the only device-side difference in the emitted C is onectr_takecall on the staticSegctr_takenot inlined (static, or__attribute__((noinline))): passes, shader 214 KB / 294 KBctr_take(acquire, last-holder free, or fade) as a real call: passes, shader 304 KBspan_fadealone as a real call, the copy loop not unrolled, everyOUTLINEfunction as a real call on Metal,maxTotalThreadsPerThreadgroup = 128, the language version pinned to 3.2 or 4.0, 16 or 64 threadgroups per pass instead of 128: all still failThe branch that moves never runs in this program. The static
Segis a plain constructor word (term_ctrsets noRFC_BIT), so at that sitectr_takealways returns from the owned path. A counter in the CPU build confirms it: the program makes 16ctr_takecalls in all and none enters the shared-node branch, at this site or at the two four-field sites. Deleting that branch for two-field takes only, on top of main, therefore changes nothing the program executes, and the GPU result goes from wrong 10/10 to right 10/10 (shader 1011 KB to 297 KB).So the Metal compiler at its default level inlines the shared-node branch of
ctr_takeinto the taken match, the shader grows about five times, and on the M1s under macOS 27.2 the live code around that dead branch runs wrong. Code that never runs changing the result of code that does is a compiler defect, unless the live code has undefined behaviour the newer compiler exploits differently; nothing found points at the second, and the same shader is right under the older compiler, on the M4 Pro, on the CPU and in the interpreter. The corruption is there before dispatch, as raramos9 saw on the M1: captured at the failing dispatch here, the entry word is0x72f3baae7b12e2faand the like, function id0xf3bain a program with 40 functions, the same pattern within a few bits across six runs. Shader validation (MTL_SHADER_VALIDATION=1) rewrites the shader and fails every variant, main included, so it is not a usable check here.The change. The shared-node branch moves from
ctr_takeintospan_fade, which already is the shared path's out-of-line function, andspan_fadeis a real call on Metal (FAR) rather than astaticthe compiler inlines anyway. Host and CUDA keepOUTLINEas before, with the last-holder free now inside it.ctr_takeis the copy and one branch. 18 lines in, 15 out;bun gates/repo.tspasses (comp.ts 65990 of 66000 ttok).Evidence on the M1 Max (32-core GPU, 64 GB, macOS 27.2 build 26B123, Xcode 27.1, bun 1.4.2):
+RTS --gpu 1GB, 5/5 compiled from source with no cached.gpu, clean underMTL_DEBUG_LAYER=1;--gpu off,--threads 1and the interpreter print57798656; the heap-built variant still passes 10/10; the shader archive goes from 1011 KB to 304 KBbench/runtimeprograms: same checksums as main on GPU and on CPU, times below, medians of 3 runs, or of 5 alternating runs on an idle machine where a first pass had moved; CPU is+RTS --gpu offon all cores!call, built natively (clang, Metal): 43 pass; the other 2 (printer/offload_bang_g,_h) have mains the compiler refuses to print, so they are check-only, as on maintests/runandtests/rfc, built natively: 124 files: 113 pass, 7 are error tests whose expected output is the build error andexit 1, 4 are check-only; all 11 give byte-identical build logs on mainRejected alternatives, both pass the reproducer:
MTLLibraryOptimizationLevelSizeon the M1 family (supportsFamily:MTLGPUFamilyApple7without Apple8), the shape I announced in the issue: raytrace 0.47 s to 0.55 s here, lexer unchanged, and it gates on the GPU family when raramos9's update shows the compiler version is the variablectr_takeas a whole not inlined: lexer 3.03 s to 4.00 s, one call per character of the static string it walksStill open in the thread: the reporter's macOS version and chip. A run of this branch on raramos9's M1 would be the second-machine test I cannot do here.