You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The two compute-dispatch escalate ops reach the GPU by different machinery. The batched op records per-binding image barriers into the engine's shared command recorder, submits once and waits once; the single op binds and dispatches through the kernel's own fence and records no barriers at all records its barriers into a separate, freshly-minted throwaway recorder that submits and waits on its own, then binds and dispatches through the kernel's own fence. Routing the single op through the batch machinery as a batch of one removes the divergence and, to the best of our current knowledge, fixes a real correctness hole rather than merely tidying upcollapses two submissions and up to three host stalls per dispatch into one of each, and removes a per-dispatch command-pool-and-fence allocation — see the corrected Design below.
Both paths already build their resolved bindings through the same shared resolver, so the change is expected to be net-negative in lines.
Design
Corrected 2026-08-22 at pickup. The first bullet below was written against the pre-#1899 tree and is no longer true. a28c3ce0 (#1899, 2026-08-21) added transition_bound_kernel_inputs_into_descriptor_layouts to bind_and_dispatch_compute_kernel, so the single path does barrier its bound inputs into their descriptor-required layouts today, and publishes the layouts it left them in. The correctness hole is already closed; what survives is the divergence in how, and its cost.
Three things this buys, all believed true but worth re-verifying at pickup:
Barriers the single path lacks. That path dispatches storage descriptors against images that were never transitioned to GENERAL — texture registration defaults to UNDEFINED and the pool does not transition. It is the spec-broken path today, and the tree already admits this in a comment.Superseded — the barriers landed in feat(engine)!: cross-process texture import for Python processors #1899. What remains is that they land in a different command buffer from the dispatch, on a recorder created and destroyed per dispatch, requiring its own submission and its own fence wait before the dispatch is recorded at all. Folding them into the batch recording makes barrier and dispatch one submission, and reuses the engine's cached batched_compute_dispatch recorder instead of minting a fresh command pool and fence every frame.
A latent hang disappears. The kernel's own dispatch carries the fail-then-deadlock fence flaw the command recorder was explicitly built to avoid: a failed queue submit leaves the fence unsignaled forever, hanging the next dispatch. The recorder tracks in-flight state so it cannot.
One fewer host stall per dispatch — the kernel path waits both before and after its submit.More than one, now that the transition recorder is in the path. A dispatch whose bindings need a transition costs 2 submissions and 3 fence waits — the transition recorder's submit-and-wait, then the kernel's drain-the-prior-fence wait, its submit, and its post-submit wait. One whose bindings already sit right costs 1 submission and 2 waits. Through the batch method both become 1 submission and 1 wait.
A prototype on the rig came in around −22/+11 in one file with the engine's compute-dispatch suite passing unchanged and no validation-layer findings. Treat that as encouraging, not as proof.Stale baseline — that prototype predates #1896 and #1899, which reshaped this function twice. Expect a different (still net-negative) delta.
Ruled out: routing through the batch handler rather than the batch method, which would prefix every single-dispatch error with "dispatch 0 of this batch".
Done means
The single compute-dispatch escalate op records its barriers into the same recording as its dispatch, on the same shared recorder the batched op uses, and submits and waits once.
Existing engine and rig Python kernel suites pass unchanged, including the refusal messages callers already depend on — a single dispatch's refusals keep saying binding \x``, never "dispatch 0 of this batch".
Docs that describe the single path as dispatching on the kernel's own fence no longer say so.
The batching cost test still distinguishes the two paths meaningfully, or says plainly what it now measures.
Validation shape
The existing compute-dispatch engine suite is the regression net and should need no new cases to stay green. Add coverage that the single op now leaves its bindings in the layouts their descriptors require — the tracked resting layout is the observable, since pixels do not distinguish a missing transition on this driver — and that it costs one submission and one stall. Rig Python kernel suites run after a wheel rebuild.
Needs the physical rig?
GPU
Non-derivable notes
This does not make the kernel's own dispatch method internal-only — callers remain in the tone mapper, the colour converter and the vulkan-jpeg SDK crate. It is a prerequisite step toward the plan's bindings-at-dispatch convergence, not the retirement of that method; expect a separate follow-up to migrate those three.
The engine's escalate gate serializes runtime-wide and waits for device idle on exit, so the shared recorder's mutex is not a contention point for this path today. That stops being true if a non-escalate full-access caller ever uses the batch API, because the mutex would then be held across a whole GPU round-trip.
The graphics-draw and ray-tracing escalate ops keep the pre-transition recorder and their own kernel fences — same divergence, different kernel kinds, out of this ticket's scope.
What & why
The two compute-dispatch escalate ops reach the GPU by different machinery. The batched op records per-binding image barriers into the engine's shared command recorder, submits once and waits once; the single op
binds and dispatches through the kernel's own fence and records no barriers at allrecords its barriers into a separate, freshly-minted throwaway recorder that submits and waits on its own, then binds and dispatches through the kernel's own fence. Routing the single op through the batch machinery as a batch of one removes the divergence and, to the best of our current knowledge,fixes a real correctness hole rather than merely tidying upcollapses two submissions and up to three host stalls per dispatch into one of each, and removes a per-dispatch command-pool-and-fence allocation — see the corrected Design below.Both paths already build their resolved bindings through the same shared resolver, so the change is expected to be net-negative in lines.
Design
Three things this buys, all believed true but worth re-verifying at pickup:
Barriers the single path lacks. That path dispatches storage descriptors against images that were never transitioned toSuperseded — the barriers landed in feat(engine)!: cross-process texture import for Python processors #1899. What remains is that they land in a different command buffer from the dispatch, on a recorder created and destroyed per dispatch, requiring its own submission and its own fence wait before the dispatch is recorded at all. Folding them into the batch recording makes barrier and dispatch one submission, and reuses the engine's cachedGENERAL— texture registration defaults toUNDEFINEDand the pool does not transition. It is the spec-broken path today, and the tree already admits this in a comment.batched_compute_dispatchrecorder instead of minting a fresh command pool and fence every frame.One fewer host stall per dispatch — the kernel path waits both before and after its submit.More than one, now that the transition recorder is in the path. A dispatch whose bindings need a transition costs 2 submissions and 3 fence waits — the transition recorder's submit-and-wait, then the kernel's drain-the-prior-fence wait, its submit, and its post-submit wait. One whose bindings already sit right costs 1 submission and 2 waits. Through the batch method both become 1 submission and 1 wait.A prototype on the rig came in around −22/+11 in one file with the engine's compute-dispatch suite passing unchanged and no validation-layer findings. Treat that as encouraging, not as proof.Stale baseline — that prototype predates #1896 and #1899, which reshaped this function twice. Expect a different (still net-negative) delta.Ruled out: routing through the batch handler rather than the batch method, which would prefix every single-dispatch error with "dispatch 0 of this batch".
Done means
binding \x``, never "dispatch 0 of this batch".Validation shape
The existing compute-dispatch engine suite is the regression net and should need no new cases to stay green. Add coverage that the single op now leaves its bindings in the layouts their descriptors require — the tracked resting layout is the observable, since pixels do not distinguish a missing transition on this driver — and that it costs one submission and one stall. Rig Python kernel suites run after a wheel rebuild.
Needs the physical rig?
Non-derivable notes
This does not make the kernel's own dispatch method internal-only — callers remain in the tone mapper, the colour converter and the vulkan-jpeg SDK crate. It is a prerequisite step toward the plan's bindings-at-dispatch convergence, not the retirement of that method; expect a separate follow-up to migrate those three.
The engine's escalate gate serializes runtime-wide and waits for device idle on exit, so the shared recorder's mutex is not a contention point for this path today. That stops being true if a non-escalate full-access caller ever uses the batch API, because the mutex would then be held across a whole GPU round-trip.
The graphics-draw and ray-tracing escalate ops keep the pre-transition recorder and their own kernel fences — same divergence, different kernel kinds, out of this ticket's scope.
Surfaced by PR #1889 (#1776).