[MISC] Fix broken Apple Metal CI.#3054
Merged
duburcqa merged 2 commits intoJul 16, 2026
Merged
Conversation
hughperkins
marked this pull request as ready for review
July 16, 2026 13:01
The macOS CI runners expose a virtualized 'Apple Paravirtual device' GPU. Running test_deformable_uv_textures drives that virtualized Metal driver into a persistent, VM-wide broken state: afterwards newComputePipelineStateWithFunction returns 'Compilation failed (code=2)' for every kernel - even a trivial copy kernel. The wedge is unrecoverable within the VM (survives MTLCompilerService restart and process exit) and cascades to every later test in the job, turning the Metal GPU job permanently red. Component isolation narrowed the trigger to the test's FEM Elastic implicit (Newton/PCG) solver run for many PCG iterations: the rasterizer render path, the UV textures, the PBD Cloth solver, and the explicit / low-PCG FEM solvers do not wedge the GPU. Skip this test on the macOS Metal backend until the underlying Apple driver bug is resolved. Co-authored-by: Cursor <cursoragent@cursor.com>
hughperkins
force-pushed
the
hp/skip-metal-deformable-render
branch
from
July 16, 2026 13:02
07b435a to
96eaedc
Compare
duburcqa
force-pushed
the
hp/skip-metal-deformable-render
branch
from
July 16, 2026 20:09
50ca09a to
80a78fe
Compare
duburcqa
approved these changes
Jul 16, 2026
Collaborator
Author
|
🙌 |
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.
The macOS CI runners expose a virtualized 'Apple Paravirtual device' GPU. Running test_deformable_uv_textures (FEM Elastic + PBD Cloth deformables rendered through the rasterizer) drives that virtualized Metal driver into a persistent, VM-wide broken state: afterwards newComputePipelineStateWithFunction returns 'Compilation failed (code=2)' for every kernel - even a trivial copy kernel. The wedge is unrecoverable within the VM (survives MTLCompilerService restart and process exit) and cascades to every later test in the job, turning the Metal GPU job permanently red.
The failure was isolated to exactly this test. Skip it on the macOS Metal backend until the underlying Apple driver bug is resolved.
Description
Full analysis by AI:
Metal Genesis CI failure: experiments and results
Author: AI (2026-07-15).
Subject: Genesis CI job
macos-15-3.12-gpu-ndarrayfailing on pull requests.Debug branches:
hp/metal-ci-debug(workflow.github/workflows/metal-debug.yml,scripts
scripts/metal_repro.py,scripts/metal_scene.py) andhp/metal-limit(workflow
.github/workflows/metal-limit.yml, scriptsscripts/metal_device_info.swift,scripts/metal_buffer_sweep.py,scripts/metal_adstack.py,scripts/metal_compiler_stress.py) onhughperkins/genesis-world.This document records the experiments run and their observed results. Sections
0-9 record experiments with no analysis. Sections 10-16 (added 2026-07-15, second
session) isolate the trigger.
Sections 18-24 (added 2026-07-15, third session) CORRECT the earlier
conclusion. The second-session conclusion (concurrency-driven GPU-compiler
contention, fixable by retrying
newComputePipelineStateWithFunction) isdisproven by direct experiment: an in-process retry fails 6/6 despite firing
120x/run, a single worker fails 5/5 on every host, and the offline cache is not
the discriminator. The real cause is a persistent, VM-wide, unrecoverable wedge
of the Apple Paravirtual GPU device, triggered by exactly one test
(
test_deformable_uv_textures). See the revised "## Conclusion".0. Observed CI failure
Genesis org repo
Genesis-Embodied-AI/genesis-world.Run
29365292338(PR [BUG FIX] Fix fastcache support. #3034). Job conclusions: all jobssuccessexceptmacos-15-3.12-gpu-ndarray=failure. The other macOS jobs(
macos-15-3.12-cpu-ndarray,macos-15-3.12-cpu-field,macos-15-3.10/3.11/3.13-cpu-ndarray), the Linux jobs, and the Windows joball passed.
In the failing job, the repeated error text is:
Job environment banner:
Running on [Apple M1 (Virtual)] with backend gs.metal. Device memory: 7.00 GB.Job software versions: quadrants
1.1.1(llvm 22.1.0, commit 35b1a060),torch
2.13.0, python3.12.10, runner imagemacos-15-arm64 20260706.0213.1.Environment variables set on the failing job include
GS_ENABLE_NDARRAY=1,GS_TORCH_FORCE_CPU_DEVICE=1,QD_OFFLINE_CACHE=1,QD_OFFLINE_CACHE_FILE_PATH=.cache/quadrants,QD_ENABLE_METAL=1,QD_DEBUG=0.The test command is
pytest -v -ra --logical --dev --backend gpu -m 'required and not slow' --forked ./tests.The generic workflow restores
.cacheunder the key${OS_FAMILY}-${MACHINE_ARCH}-${QUADRANTS_VERSION}(=Darwin-arm64-1.1.1), andsaves that same key from the
macos-15cpu/ndarray/ py3.12 job only.0.0 Underlying RHI diagnostic (from org logs)
Grepping the org PR #3042 failing job log for the quadrants RHI log line that
precedes each
runtime.cppexception yields, for every failing kernel:msl_bytesof 1641, 1360, 1020.cannot create compute pipeline state for kernellines: 181 (for theext_arr_to_ndarray_matrix_c20kernel) plus one each for the other two.Materializing kernel ext_arr_to_ndarray_matrix_c20_0 in AutodiffMode.NONE,i.e. the failure occurs on the first kernel materialized during
Building sceneinside a freshly-forked test process.quadrants/rhi/metal/metal_device.mm,create_compute_pipeline) attributes this line tonewComputePipelineStateWithFunctionreturning nil with theNSErrorlocalizedDescription=Compilation failedandcode= 2.0.1 Other PRs
Reading the
macos-15-3.12-gpu-ndarraycheck-run conclusion on each PR head SHA:failure: PRs [MISC] Significantly improve non-batched CPU simulation speed (up to 30%). #3033, [BUG FIX] Fix fastcache support. #3034, [MISC] Fix pyramidal Newton perf regression from separate prev_active snapshot pass. #3035, [MISC] Restructure test suite into per-component folders with per-capability files. #3037, [MISC] Clean up the mesh parsing unit tests. #3040, [FEATURE] Support explicit mounting transform in RigidEntity.attach. #3041, [MISC] Speed CPU-based simulation with elliptic cone friction by reusing the cone-free Hessian across rebuilds. #3042.PR #3042 failing job (run
29423732829, job87380938028) log contents:ext_arr_to_ndarray_matrix_c20_0_0_t00,matrix_to_ext_arr_c22_42_0_t00,ndarray_to_ext_arr_c8_3_0_t00.SPIR-V shader was rejected; 549 lines containingRhiResult=-1.PASSED, 300FAILED.PASSED/FAILED): the runbegins with 176 consecutive
PASSED, after whichPASSEDandFAILEDrunsinterleave for the remainder (e.g.
13 FAILED, 3 PASSED, 1 FAILED, 6 FAILED, ..., and later70 PASSED, 2 FAILED). There is no single point after whichevery subsequent test fails.
Running on [Apple M1 (Virtual)] ... Device memory: 7.00 GB;pytest reports
created: 2/2 workers.0.2 Date range (from PR head-SHA check-runs)
macos-15-*-gpu-ndarray=success.failure.1. Local, real Apple Silicon Metal (isolated copy kernel)
Machine: local Apple Silicon Mac (darwin 25.5.0).
Ran
qd.init(arch=qd.metal, debug=True)then, for element types matching the CIfailing kernel names, allocated a quadrants ndarray, called
.from_numpy(host)and
.to_numpy(), and compared:qd.Matrix.ndarray(4, 5)(20 components,ext_arr_to_ndarray_matrix_c20)qd.Matrix.ndarray(2, 11)(22 components)qd.Vector.ndarray(8)(8 components)qd.Vector.ndarray(3)(3 components)Result with quadrants
0.8.0: all pass, roundtrip max error0.0.Result with quadrants
1.1.1(separate py3.10 venv): all pass, roundtrip maxerror
0.0.In both, quadrants logged
Out-of-bound access checking is not supported on arch=metal.quadrants
1.1.1provides wheels for cp310, cp311, cp312, cp313; no cp314 wheelexists.
2. Fork CI, isolated copy kernel, macos-15 virtualized runner
Repo
hughperkins/genesis-world,runs-on: macos-15.sysctl -n machdep.cpu.brand_stringprintsApple M1 (Virtual). Installedquadrants==1.1.1andnumpyonly. Scriptscripts/metal_repro.pyallocatesqd.Matrix.ndarray(4, 5)and runs.from_numpy/.to_numpy.2.1 Init-flag sweep (run
29439255683)Configs:
baseline,no_advanced_opt,no_scalarize,no_fast_math,cfg_opt. Result: all 5 jobssuccess, each printingRESULT ext_arr_to_ndarray_matrix_c20 OK roundtrip_err=0.000e+00.2.2 Genesis-exact init flags plus ad-stack sweep (run
29439514762)Init mirrored
genesis/__init__.pyon Metal:enable_fallback=False,debug=False,check_out_of_bound=False,force_scalarize_matrix=True,advanced_optimization=True,cfg_optimization=False,fast_math=False,cpu_max_num_threads=1,ad_stack_experimental_enabled=True,ad_stack_sparse_threshold_bytes=200_000_000.Configs:
genesis_exact,no_ad_stack,ad_stack_small_bytes(
ad_stack_sparse_threshold_bytes=1_000_000),no_advanced_opt,no_scalarize. Result: all 5 jobssuccess, roundtrip error0.0.3. Fork CI, full Genesis install, trivial scene and single test
Installed
pip install ".[dev,render,usd]"plustorch --upgrade(index
download.pytorch.org/whl/cpu) plusmatplotlib<3.11. quadrants1.1.1.pytest tests/test_rigid_physics.py::test_gravity(run29439730976): pytest reportedno tests ran/ exit code 5. On this branchthe file is
tests/rigid/test_dynamics.py(tests were reorganized intoper-component folders); the old path collects 0 items.
29440166086), jobsuccess:scripts/metal_scene.py:gs.init(backend=gs.metal, debug=True, logging_level="debug"), scene withPlane+Sphere,build(), 3step()s. Ran toSCENE_STATUS=PASS.pytest tests/rigid/test_dynamics.py::test_gravity --logical --dev --backend gpu --forked: passed.4. Fork CI, full test file, CI-like parallelism (run
29440658358)Command per config:
pytest -v -ra --logical --dev --backend gpu --forked -m 'required and not slow' --numprocesses=$NPROC tests/rigid/test_collision.py.ci_like(QD_OFFLINE_CACHE=1,--numprocesses=auto): completed, jobsuccess.serial(QD_OFFLINE_CACHE=1,--numprocesses=0),no_cache(
QD_OFFLINE_CACHE=0,--numprocesses=auto),serial_no_cache(
QD_OFFLINE_CACHE=0,--numprocesses=0): cancelled before completion (eachwas still running at ~15 min). No result recorded.
5. Fork CI, cross-backend offline cache (run
29442109831)Two configs, both
QD_OFFLINE_CACHE=1,QD_OFFLINE_CACHE_FILE_PATH=.cache/quadrants, runningpytest ... --backend gpu --forked -m 'required and not slow' tests/rigid/test_collision.py:metal_only_cache: Metal run only, starting from an empty cache.cross_backend_cache: first runstests/rigid/test_collision.pywith--backend cputo populate.cache/quadrants, then runs the same file with--backend gpu.Result: both jobs
success.cross_backend_cache: the CPU warm step populated.cache/quadrants/python_side_cache/*.cache.txt; the subsequent Metal runreported
62 passed, 1 xfailed.metal_only_cache: Metal run reported62 passed, 1 xfailed.5b. Runner comparison (org vs fork)
The org failing job (
29423732829) and the fork jobs both print the same devicebanner
Running on [Apple M1 (Virtual)] ... Device memory: 7.00 GBand pytestcreated: 2/2 workers.6. Fork CI, full
./testssuite mirroring the org GPU job (run29443749352, commita9eb95cc, branchhp/metal-ci-debug)Command mirrors the org
generic.ymlgpu job:pytest -v -ra --logical --dev --backend gpu -m 'required and not slow' --forked ./tests, with the same env (QD_OFFLINE_CACHE=1,QD_OFFLINE_CACHE_FILE_PATH=.cache/quadrants,QD_ENABLE_METAL=1,GS_ENABLE_NDARRAY=1,GS_TORCH_FORCE_CPU_DEVICE=1,QD_DEBUG=0), starting froman empty cache.
Job completed in about 70 minutes. Result: 304 passed, 150 failed. The failing
kernels and their exact RHI diagnostics match the org run:
All three are small copy/fill/clear kernels (
msl_bytes708-1968). This run didnot capture the system log.
7. Fork CI, Metal device limits and buffer/adstack size sweeps (run
29444269087, branchhp/metal-limit)runs-on: macos-15.7.1 Device limits (
scripts/metal_device_info.swift)7.2 Single-buffer size sweep (
scripts/metal_buffer_sweep.py, quadrants 1.1.1)Allocates one
qd.field(qd.f32, shape=n)of the given size on Metal, fills it ina kernel, sums via
to_numpy. Sizes 50, 100, 150, 200, 300, 500, 1000 MB: alljobs
success(each printsRESULT MB=<n> OK).7.3 Reverse-mode adstack size sweep (
scripts/metal_adstack.py, quadrants 1.1.1)Runs a reverse-mode autodiff kernel whose backward pass uses a dynamic-loop-bound
adstack, with
ad_stack_experimental_enabled=Trueandad_stack_sparse_threshold_bytesset to 10M, 50M, 100M, 150M, 200M: all jobssuccess(each printsRESULT ad_stack_bytes=<n> OK grad0=2.000).8. Fork CI, synthetic Metal compiler-service stress (run
29447591837, branchhp/metal-limit)scripts/metal_compiler_stress.pycompiles structurally distinct kernels (uniquecompile-time unroll count per iteration,
offline_cache=False) so each requiresa fresh MSL translation and
newComputePipelineStateWithFunction. Two configs:single_process: one process compiling 4000 distinct kernels. Jobsuccess(
RESULT compiled 4000 distinct kernels OK).repeated_process: 80 sequential processes, each compiling 60 distinctkernels. Job
success.The step then ran
sudo log showfiltered forEXC_RESOURCE|watermark|phys_footprint|jetsam|MTLCompiler|GPUCompiler|AGXCompiler|XPC.*interrupt|Compiler.*(exit|crash|kill).Observed lines:
MTLCompilerServiceCompilation BEGIN/Compilation SUCCESSfor
MTLBuildLibraryFromSourceToArchive - source(approx 400-760 ms each) andMTLBuildFunctions - pipeline(approx 17 ms). NoEXC_RESOURCE,watermark,jetsam, or compiler-crash lines were present.9. Fork CI, real heavy folders (rigid + rendering) with system-log capture (run
29448183249, branchhp/metal-limit)Runs
pytest -v -ra --logical --dev --backend gpu -m 'required and not slow' --forked tests/rigid tests/renderingwith the org gpu-job env (QD_OFFLINE_CACHE=1,QD_ENABLE_METAL=1,GS_ENABLE_NDARRAY=1,GS_TORCH_FORCE_CPU_DEVICE=1,QD_DEBUG=0). After the run,sudo log show(since job start) is filtered forEXC_RESOURCE|watermark|phys_footprint|jetsam|memorystatus, forMTLCompiler|GPUCompiler|AGXCompilerlines containingfail|error|exit|crash|kill|interrupt|timeout, and forXPClines containinginterrupt|invalid|CONNECTION.Result: 267 passed, 19 failed. The reproduction matches the org and the full-suite
run:
System-log observations for the run window (job start to end):
EXC_RESOURCE,high watermark,phys_footprint: no matching log lines (theonly occurrences of these tokens in the job log are the echoed
grepfiltercommand itself).
jetsam/memorystatusfilter returned onlyrunningboarddbackgroundlines:
Ignoring jetsam update because this process is not memory-managed(for
anon<Python>and variousosserviceprocesses) andmemorystatus_control error: MEMORYSTATUS_CMD_CONVERT_MEMLIMIT_MB(-1|0) returned -1 22 (Invalid argument). None reference a memory limit being crossedby a test process.
MTLCompiler|GPUCompiler|AGXCompilerlines matchingfail|exit|crash|kill|interrupt: the only matches are at21:24:12(jobteardown), where
launchdSIGKILLs thecom.apple.MTLCompilerServiceinstances with the note
sent by launchd during teardown of process-scoped services after host exited, ran for ~3440000 ms, and the XPC connection isinvalidated because the client process ... either cancelled the connection or exited. NoMTLCompilerServicecrash, kill, or compilation-failure log lineappears during the pytest run itself.
XPC+interrupt|invalid|CONNECTIONfilter returned no matching loglines from the run.
10. Fork CI, repeated init/teardown in one long-lived process (run
29453644680, branchhp/metal-reinit)Motivation: on macOS the Genesis
conftest.pyforcesconfig.option.forked = False(fork is only honored on Linux), so eachpytest-xdistworker runs manytests in a single long-lived process, calling
gs.init(metal)/gs.destroy()(hence
qd.init/qd.reset) once per test. None of sections 1-9 exercisedthat repeated-init axis in one process. Two scripts mirror it.
scripts/metal_reinit_qd.py(pure quadrants1.1.1): loopqd.init(metal)with the genesis-exact flags of section 2.2, do a
qd.Matrix.ndarray(4, 5)from_numpy/to_numpyroundtrip (theext_arr_to_ndarray_matrix_c20kernel),qd.reset(), for 600 cycles. Matrixnocache_1p(cache off, 1 proc),cache_1p(cache on, 1 proc),cache_2p(cache on, 2 concurrent procs).Compilation failed (code=2)/RhiResult=-1in any log); the Metalroundtrip succeeded on every cycle. (The script's own check used exact
float equality
float(out)==-9.81against a float32 roundtrip and soreported
AssertionErroron every cycle; that is a bug in the check, not aMetal failure. Corrected in a later commit.)
scripts/metal_reinit_gs.py(full Genesis): loopgs.init(metal)-> build aPlane+Sphererigid scene -> 3step()s ->gs.destroy(), for 300 cycles.kernel compilation); each later cycle ~2 s.
11. Fork CI, init/reset x distinct-kernel churn in one process (run
29454911916, branchhp/metal-churn)scripts/metal_churn_qd.py(pure quadrants1.1.1), 800 cycles, one process,combining the two axes that sections 8 and 10 tested only separately:
reinit_distinct:qd.init/qd.resetevery cycle AND a structurallydistinct kernel (unique compile-time unroll) compiled every cycle.
reinit_same:qd.init/qd.resetevery cycle, same kernel.noreinit_distinct: singleqd.init, a distinct kernel every cycle.Result: all three modes 0/800, 0 compile failures. Repeated init/teardown,
distinct-kernel compilation, and their combination in a single process do not
reproduce.
12. Fork CI, concurrency synthetic + real-folder split (run
29455302366, branchhp/metal-conc)Two job families.
conc-np{1,2,3}: launch N concurrent processes (each its own offline-cachedir), each running
metal_churn_qd.py reinit_distinctfor 800 cycles on theone Apple Paravirtual GPU. Result: all procs 0/800, 0 compile failures at
1, 2, and 3 concurrent processes. Concurrent Metal compilation alone (no
rendering) does not reproduce.
suite-*: real Genesis install, org gpu-job env, default parallelism(
created: 2/2 workers).rendering_auto(tests/rendering): reproduces - 25 failed, 4 passed,14 skipped in ~170 s. Failing kernels:
ext_arr_to_ndarray_matrix_c20(dominant),
kernel_init_geom_fields_c254,kernel_pbd_rigid_collide_c84.Ordered outcomes show a few RASTERIZER tests pass, then after the first
failure in a worker essentially every later test in that worker fails.
rigid_auto(tests/rigid,-n auto, 2 workers, no rendering):258 passed, 0 compile failures. Two-worker concurrency without rendering
does not reproduce.
13. Fork CI, single-process rasterizer isolation (run
29455788553, branchhp/metal-raster)scripts/metal_raster_gs.py, one process,gs.init(metal)per cycle, 40cycles, three modes:
render: build aSpherescene + camera,camera.render(rgb/depth/seg/normal).build_only: build a rasterizer-camera scene (creates the offscreen GLcontext) +
step(), but norender().physics: no camera, juststep().Result: all three modes 0/40, 0 compile failures. A single process doing
offscreen rasterizer rendering (or GL-context creation) repeatedly does not
reproduce.
14. Fork CI, rendering per-file + serial/parallel folder bisection (run
29456194019, branchhp/metal-bisect)Real Genesis install, org gpu-job env.
-n auto(2 workers):test_debug_draw(3 passed),test_imgui_overlay(2 passed),test_interactive_viewer(9 passed),test_offscreen(10 passed) - all pass in isolation;test_dynamic_meshes- reproduces (ext_arr_to_ndarray_matrix_c20,kernel_clear_external_force_c274).tests/renderingfolder:-n auto(2 workers): reproduces - 4 passed, 26 pipeline-creationfailures.
-n 0(1 process): 26 passed, 14 skipped, 3 xfailed, 0 failures.test_dynamic_meshes.pyis the unique rendering file that triggers the failure,and the whole folder passes when run in a single process.
15. Fork CI, dynamic_meshes serial vs parallel (run
29456993054, branchhp/metal-serial)pytest ... tests/rendering/test_dynamic_meshes.py, real Genesis install, orggpu-job env, differing only in worker count. The same three RASTERIZER tests
under each:
-n 0(1 proc)-n auto(2 workers)test_deformable_uv_textures[RASTERIZER]test_set_vverts[RASTERIZER]test_set_vverts_sphere_to_box[RASTERIZER]Single process: 3 passed, 0 compile failures. Two workers: all three fail
with
Failed to create pipeline ... (RhiResult=-1) ... Compilation failed (code=2). In this pair of jobs the only changed variable is the number ofconcurrent worker processes; section 17 shows single-process jobs are not always
clean, so 2 workers guarantee the failure rather than being the sole cause.
16. Fork CI, two independent single-worker processes run concurrently (run
29457579661, branchhp/metal-cap)Capstone to test whether the trigger is
pytest-xdist-specific or plaincross-process contention. After a single-process warm step, the job launches two
independent
pytest ... -n 0 tests/rendering/test_dynamic_meshes.pyprocesses(each with its own
GS_CACHE_FILE_PATHandQD_OFFLINE_CACHE_FILE_PATH, so noshared cache file) at the same time and waits for both.
RhiResult=-1present). So the trigger is ordinary OS-level concurrency of two processes on
the one GPU, not anything internal to xdist.
in 337 s), whereas the identical single-process run in section 15 passed. So a
lone process is intermittent, not a guaranteed pass; concurrency raises the
failure probability toward certainty.
17. Fork CI, failure-rate over repeated runs (run
29458328690/ re-run29459447352, branchhp/metal-stats)Same file, org gpu-job env, cache warmed once, then the identical command
repeated N times back-to-back (fresh process each), counting how many runs emit
RhiResult=-1.parallel_auto(-n auto, 2 workers, run29458328690): 8/8 runsreproduced (9
RhiResult=-1lines each). Two-worker reproduction isdeterministic.
serial_n0(-n 0, 1 process): the outcome is uniform per job, not per run:29458328690: warm step passed and the first repeat was clean (0RhiResult=-1).29459447352: 10/10 repeats reproduced (9RhiResult=-1each).show the same split: some whole single-process jobs pass, others fail.
So a lone process does not have a stable per-run failure probability; instead
an entire job either passes or fails. The offline cache is not the
discriminator (both stats jobs warmed the cache first, yet one passed and one
failed 10/10). A whole-job pass/fail split points to a per-VM/host factor -
contention on the shared, virtualized Apple Paravirtual GPU compiler from
whatever else is co-scheduled on that physical host - rather than anything in
the test itself. Two xdist workers add a guaranteed concurrent load of their
own, which is why the real 2-worker CI fails consistently. Single-process
failures are the same
RhiResult=-1/Compilation failed (code=2)pipeline-creation error.
18. Third session: is the failure recoverable in-process? Retry fix disproven (branch
hp/metal-prove)Goal: prove the second-session mitigation #1 (retry
newComputePipelineStateWithFunction).The retry was emulated at the nearest Python boundary,
quadrants.lang.kernel.Kernel.__call__: "Failed to create pipeline" is raisedinside
launch_kernelduringprog.compile_kernel, andcompiled_kernel_datais cached only on success, so re-invoking the kernel re-drives compilation +
pipeline creation - exactly what a C++ retry would do. Loaded as a pytest plugin
in each worker, gated by
QD_PIPELINE_RETRY. Three arms interleaved on ONE host(controls for per-host state):
0baseline (no patch),1retry,2passthrough wrapper (identical extra frame, no retry - a heisenbug control).
29465439295(valid; 6 iterations, warm cache,-n autotest_dynamic_meshes.py):patched=3): FAIL ~5/6, retry(
patched=3,retry_events=120): FAIL 6/6.Passthrough failing like baseline rules out a timing/heisenbug artifact.
29462466840/29463513156gave a false "retry passes": theplugin hit
INTERNALERROR: 'function' object has no attribute 'Kernel'-quadrants.lang.__init__doesfrom quadrants.lang.kernel_impl import *, whichrebinds the attribute
quadrants.lang.kernelto thekerneldecorator; thesubmodule must be fetched with
importlib.import_module. Tests never ran, soRhiResult=-1count was 0 = a bogus pass. Fixed, then the real result above.)Conclusion: pipeline creation stays broken for the whole process; an in-process
retry cannot recover. Mitigation #1 is refuted.
19. Single worker vs two workers, across four hosts (run
29466875938, branchhp/metal-prove2)test_dynamic_meshes.py, warm cache, interleaved-n auto(2 workers) and-n 0(1 process), 5 iterations, on 4 independent runners (
h1-h4).-n 0(single worker): FAIL 5/5 on all four hosts - just as reliably as-n auto(also 5/5 on all four).This demolishes the concurrency hypothesis (sections 15-17): a single process
with no concurrency at all fails identically. The only thing these runs share and
the earlier "serial passed" runs (section 15) lacked is that they run after a
prior rendering run on the same VM (the warm-up step) - i.e. run-order, not
worker count.
20. Cold cache vs warm cache, single worker (run
29468572792, branchhp/metal-prove3)test_dynamic_meshes.py,-n 0, interleavedQD_OFFLINE_CACHE=0(alwaysrecompile) vs a pre-populated warm cache.
h2the first pytest invocation(cold, 401 s) passed 3/3, then the next 6 fresh processes (cold and warm
alike) all failed on the first kernel, over 3+ minutes. On
h1even thefirst run failed. So the real variable is simply "has a rendering run already
happened on this VM", not cache/concurrency. The failing kernel is a trivial
~1.6 KB copy kernel (
ext_arr_to_ndarray_matrix_c20_0_0_t00,msl_bytes=1641)21. Is the wedge recoverable? killall / cache-clear (run
29470334885, branchhp/metal-fix)A fast no-render probe (
scripts/metal_probe.py:gs.init(metal)+qd.Matrix.ndarray(...).from_numpy-> the exact failing kernel, no OpenGL). Eachjob: run
test_dynamic_meshes.pyuntil the probe fails (VM poisoned), then applya recovery and re-probe x3. Matrix:
none/killall/clearcache/both.poisoned, the no-GL probe process crashes with no output (hard native abort
in
gs.init/pipeline creation).passed), one after the 1st.
killall -9 MTLCompilerService-> 0/3, clear Metalshader caches (
~/Library/Caches,/private/var/folders/**/com.apple.metal*)-> 0/3, both -> 0/3.
So the wedge is not in the
MTLCompilerServiceprocess (respawning it does nothelp) and not in the on-disk shader cache. It is persistent state in the Apple
Paravirtual GPU device / VM, unrecoverable short of a fresh VM.
22. What triggers the wedge? render vs GL-context vs physics (run
29471301393, branchhp/metal-trigger)scripts/metal_trigger.pyrun repeatedly in fresh processes, probing VM healthbetween, 8 runs x 3 replicate hosts per mode:
render(simple sphere + camera +cam.render): never wedged (0/8 x3).glbuild(camera scene built, GL context created, no render): never wedged.physics(no camera, 40 steps of Metal compute): never wedged.72 clean runs. Generic rasterization, GL-context creation, and Metal compute do
NOT wedge the VM. This matches section 13 (simple-sphere render 0/40). The
trigger is specific to
test_dynamic_meshes, not rendering in general.23. Which test wedges? (run
29472155741, branchhp/metal-triggerb)Each of the three
test_dynamic_meshes.pyRASTERIZER tests run alone, repeated infresh processes, probe between, 6 runs x 2 hosts:
test_deformable_uv_textures(PBD Cloth + FEM Elastic + render): wedges theVM on run 1 (and the test itself fails); every subsequent run fails; probe
crashes.
test_set_vverts(custom vverts +n_envs=3+set_vverts+ render):6/6 pass, VM healthy both hosts.
test_set_vverts_sphere_to_box: 6/6 pass, VM healthy both hosts.The sole trigger is
test_deformable_uv_textures- the FEM/PBD-deformable +UV-texture-render combination. The dynamic-mesh
set_vvertspath (initiallysuspected) is not the trigger.
24. Prove the fix: deselect the trigger (run
29473010006, branchhp/metal-provefix)Fresh VM per arm (baseline poisons its VM, so arms cannot share one), 3 replicate
hosts each, probe between runs:
baseline=test_dynamic_meshes.pyfull.fixed= same,-k 'not deformable_uv_textures'.fixedfolder= wholetests/rendering,-n auto,-k 'not deformable_uv_textures'.RESULT (baseline from run
29473010006; fixed / fixedfolder from run29476315670after fixing a-k-quoting bug that had made the fixed arms run 0tests - caught by an explicit "no tests ran" guard):
2/3, 2/3, 3/3 runs hit
RhiResult=-1(the one clean run per VM is always theVM's first, cold run). Matches the org cascade.
-k 'not deformable_uv_textures'), 3 VMs x 3 runs = 9/9 clean:every run
2 passed, 3 deselected,rhi=0, and the between-run probe isPROBE_OK- the VM never wedges.tests/rendering,-n auto, minus deformable), 3 VMs x3 runs = 9/9 clean: every run
25-26 passed, 13 skipped, 2-3 xfailed,rhi=0, VM healthy throughout.So deselecting the single trigger
test_deformable_uv_texturesmakes the entirerendering suite pass reliably and keeps the VM's Metal compiler healthy, on every
host tried. This is the proven, reliable fix; retry (section 18), single-worker
(section 19), and cache changes (section 20) are all disproven.
Conclusion
Root cause (CORRECTED; supersedes the second-session conclusion below): the
macos-15-*-gpu-ndarrayfailures are a persistent, VM-wide wedge of the GitHubmacos-15runner's Apple Paravirtual GPU device, triggered bytest_deformable_uv_textures(FEM/PBD deformables rendered through therasterizer). Once that test runs on a VM, the VM's Metal compiler can no longer
create a compute pipeline for any kernel - even a trivial ~1.6 KB copy kernel
(
ext_arr_to_ndarray_matrix_c20,msl_bytes=1641) - returningCompilation failed (code=2)fromnewComputePipelineStateWithFunction. The state isunrecoverable within the VM (surviving
MTLCompilerServicerestarts, Metal cacheclears, and process exit) and crashes even a no-OpenGL probe process. This
produces the observed org-CI cascade: the suite runs fine until the deformable
render test executes, then all remaining kernel compilations fail (the "176 pass,
then everything fails" pattern of section 0.1).
Evidence chain (third session):
6/6 despite firing 120x/run (section 18).
two workers (section 19); the second-session "2 workers required" reading
(sections 15-17) was a run-order confound (the 2-worker jobs always ran the
trigger; the "serial passed" jobs happened to be the VM's first run).
warm; the discriminator is run-order on the VM (section 20).
killing
MTLCompilerServiceand clearing Metal caches do not recover it(section 21).
clean runs, section 22); of the three dynamic-mesh tests only
test_deformable_uv_textureswedges (section 23).This is almost certainly a bug in Apple's virtualized-GPU (Paravirtual device)
Metal driver on the CI image, provoked by the deformable-render workload - not a
Genesis or quadrants logic bug (the same kernels compile fine before the trigger
runs, and on real hardware). The actionable outputs are therefore a CI mitigation
plus an upstream report.
Fix / mitigation (CORRECTED - the second-session list below is superseded):
xfailtest_deformable_uv_textures(and, defensively, any FEM/PBD-deformablemacos-15Metal GPU job - e.g. a@pytest.mark.skipifkeyed on Metal + the paravirtual device, or deselect itin the job. Section 24 proves the rest of the rendering suite then passes
reliably and the VM never wedges. This is deterministic because the
VM-poisoning workload never runs. (It does not "fix" the deformable render on
virtualized macOS - that path is broken by the driver bug - it stops one
broken test from taking down the other ~180.)
throwaway job/VM so its wedge cannot cascade. Note even alone it is flaky on
this driver (fails ~on the first run on some hosts), so prefer skip.
the
macos-15runner image is driven into a permanentnewComputePipelineStateWithFunctioncode=2state by a specific Metal +rendering workload; include the minimal repro (
test_deformable_uv_textures-> then any trivial pipeline creation fails process-wide and VM-wide).
render path provokes the driver (e.g. a specific texture / buffer / compute
interaction) and evaluate a headless or Metal-native render path.
Superseded second-session conclusion (kept for the record; disproven by sections 18-24)
Root cause (diagnosed): the
macos-15-*-gpu-ndarrayfailures are GPU-compilercontention on the shared Apple Paravirtual device of the GitHub
macos-15runner, triggered specifically by Genesis's rasterizer (offscreen OpenGL via
pyglet) rendering running concurrently with Metal (quadrants) compute-pipeline
compilation. When two
pytest-xdistworkers execute rendering tests at thesame time, Metal's
newComputePipelineStateWithFunctionstarts returning nilwith
Compilation failed (code=2); once a worker hits this, its Metal statestays poisoned and every later kernel it compiles fails, producing the observed
"many pass, then a worker cascades to all-fail" pattern (section 0.1's 176
consecutive passes then interleaved failures).
Evidence chain:
tests/renderingreproduces (section 12); of its files onlytest_dynamic_meshes.py(mesh-from-file + rasterizer render) reproduces inisolation (section 14);
tests/rigidwith 2 workers and no rendering does not(section 12, 258 passed); pure-Metal synthetics never fail (sections 8, 10,
11).
fail in 8/8 runs (section 17); two independent single-worker processes run
concurrently both fail (section 16), so it is cross-process contention on the
shared GPU, not an xdist internal.
concurrent pure-Metal churn processes never fail (sections 12, 11). The
in-process rasterizer(OpenGL) + Metal combination is the fragility; concurrent
GPU-compiler load is the stressor that tips it over.
serial job 10/10 fail, another clean; sections 14/15 clean), independent of
the offline cache. A whole-job split is what you expect if the stressor is
contention on the shared, virtualized GPU compiler from other tenants
co-scheduled on the same physical macOS host - a sibling xdist worker is
simply a guaranteed, in-job source of that same contention.
isolation, sections 1, 2, 8), not PR [BUG FIX] Fix fastcache support. #3034-specific (section 0.1, many
PRs and
mainaffected), not the offline cache (section 5), and notmemory/resource exhaustion (no
EXC_RESOURCE/jetsam/compiler-crash in syslog,sections 8, 9).
Suggested mitigations (in rough order of preference) [DISPROVEN - see sections
18-24 and the corrected list above]:
MakeDisproven (section 18): thecreate_compute_pipelineretrynewComputePipelineStateWithFunctionon transient
Compilation failed (code=2).failure is persistent, not transient; retry fails 6/6 despite 120 retries/run.
The "identical MSL succeeds when recompiled without contention" premise was
never actually tested and is false here.
Run macOS Metal rendering tests with a single worker.Disproven (section19): a single worker fails 5/5 on every host.
Isolate the rasterizer's OpenGL context from Metal compute.Not themechanism: generic GL-context creation and rendering never wedge the VM
(section 22).
the immediate cause is the deformable-render driver wedge, not OpenGL per se.
Appendix: run index (fork
hughperkins/genesis-world; brancheshp/metal-ci-debugandhp/metal-limit)Related Issue
Resolves Genesis-Embodied-AI/Genesis#
Motivation and Context
How Has This Been / Can This Be Tested?
Screenshots (if appropriate):
Checklist:
Submitting Code Changessection of CONTRIBUTING document.