Skip to content

plan(python): spell CPU readback and write-back of texture-backed surfaces in the wheel #1758

Description

@tato123

Re-scoped by /reconcile-tracker 2026-08-08. The kernel half of this ticket is superseded — #1773 (compute), #1775 (GLSL), #1776 (batching) and #1777 (graphics + ray tracing) deliver it, and the bridge-installation approach this body proposed is superseded by ADR decision 5 (docs/decisions/python-kernel-api.md), which deletes the bridge traits entirely rather than installing them from the wheel's spawn path. Every [NEEDS DECISION] in this ticket's own comment now has an ADR answer.

The cpu-readback half survives, and it is narrower than this body implies.

What already works — no ticket owed

A Python author CPU-reads a pixel-buffer-backed surface today with zero glue. lock() / as_numpy() route through host_visible_pixel_plane() (sdk/streamlib-python-wheel/src/python_gpu_surface_pixel_exchange.rs:113-122) — these are host-visible linear allocations, so no GPU→CPU copy runs at all. The parity bar is already discharged here.

The actual gap

A numpy-only author cannot get a texture-backed surface — a kernel output — onto the CPU.

lock() raises "surface has no host mapping; it is a DEVICE_LOCAL allocation" (src/python_processor_context.rs:317-330); the host DLPack arm fails the same way (:416-427) and as_numpy requests device="cpu" (:465-468), landing on it. The only working path is a CUDA capsule over the OPAQUE_FD device-export staging, which needs a live CUDA runtime plus torch/cupy to copy device→host — numpy.from_dlpack cannot consume a CUDA capsule. #1779's as_device_tensor() does not close this: it delivers a device tensor for third-party GPU packages, not CPU bytes.

This gap only becomes reachable once #1773/#1777 let a Python author produce kernel outputs, and #1774 gives the engine the readback capability. So it is real, and it is new.

Why this is design-first, not an implementation ticket

The plan states the capability (§Graphics: "Python reaches every GPU capability Rust authoring reaches … and CPU readback") but no plan entry states the spelling. There is a genuine fork:

  • Extend GpuSurfaceHandle's host arm so a texture-backed surface refills a host-mappable staging and as_numpy() simply works on kernel outputs — the one-system shape, likely over the existing device-export staging.
  • Or a dedicated readback vocabulary, resurrecting the legacy acquire_read / acquire_write / try_acquire_* context managers (sdk/streamlib-python/python/streamlib/adapters/cpu_readback.py:487-520).

Either way the contended wire response (escalate_request.yaml:143-148, handler subprocess_escalate.rs:1112-1146) needs a non-exception Python shape — in the legacy vocabulary it yielded None. That is an architecture decision; tickets implement the plan, they never make it.

Depends on #1774 (the engine capability) and #1773 (something to read). Its resolution may ADD a bullet to docs/plan/changes/python-kernel-surface.md or ride a small follow-up change — an /align call, not an implementer's.

Original body (pre-reconciliation, preserved 2026-08-08)

Named disposition from #1714's escalate-parity research (owner, 2026-08-06). Three of three,
and the largest.

A Rust processor can build and dispatch compute, graphics and ray-tracing kernels and build
acceleration structures. A Python processor cannot spell any of it — not because the path is
missing, but because the wheel never grew the vocabulary.

What already exists

Every op is defined and served. packages/escalate/schemas/escalate_request.yaml carries
register_compute_kernel, run_compute_kernel, register_graphics_kernel, run_graphics_draw,
register_acceleration_structure_blas, register_acceleration_structure_tlas,
register_ray_tracing_kernel, run_ray_tracing_kernel, run_cpu_readback_copy and
try_run_cpu_readback_copy; handle_escalate_op implements all of them; and the wheel's spawn
host already attaches the bridge that routes a helper's escalate frames into that handler
(python_helper_process_spawn_host.rs:484-488). The old subprocess SDK drove them through
adapter classes — sdk/streamlib-python/python/streamlib/adapters/vulkan.py is the template,
and examples/polyglot-vulkan-{compute,graphics,ray-tracing} are the live consumers.

What is missing

  1. Wheel-side spelling — the Python surface a processor calls.
  2. Bridge installation. The four kernel/readback domains are trait-dispatched and their
    implementations are installed by application setup glue today
    (core/context/compute_kernel_bridge.rs:21-27; e.g.
    examples/polyglot-vulkan-compute/src/main.rs:298). Without an installed bridge the host
    answers a typed err. Under the importable-library model there is no app main.rs to do it,
    so the wheel's spawn path has to.

Shape constraint

The subprocess-Vulkan carve-out stands (.claude/rules/polyglot.md): import-side only —
no allocation, no modifier choice, no kernel construction child-side. Everything privileged
escalates. ARCHITECTURE.md §Graphics already decides the surface: "the engine's kernel
primitives are exposable to Python as configured blocks: shader/compute source and binding
config passed from Python, compiled and executed by the engine on its device — no user-side
Vulkan, ever. Shape only; the Python-facing API design is its own session."

That last clause means this needs an API design pass before implementation.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    shape:design-firstloop classification: architecture brief before build

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions