Skip to content

Compose-time buffer binding: argument-free fused kernels + DeviceBuffer.from_ptr - #8

Open
johnnynunez wants to merge 1 commit into
Libraries-Openly-Fused:mainfrom
johnnynunez:feat/compose-time-buffer-binding
Open

Compose-time buffer binding: argument-free fused kernels + DeviceBuffer.from_ptr#8
johnnynunez wants to merge 1 commit into
Libraries-Openly-Fused:mainfrom
johnnynunez:feat/compose-time-buffer-binding

Conversation

@johnnynunez

Copy link
Copy Markdown
Contributor

What

Input/output buffers can now be bound directly on the read/write ops at compose() time, producing a fused kernel that is fully wired and callable with no arguments:

k = fkl.compose(
    fkl.TensorRead(x),      # cuda torch tensor / CAI object / DeviceBuffer
    fkl.Cast("float32"),
    fkl.Div((255.0,) * 3),
    fkl.TensorSplit(out),
)
k()             # no arguments: reads x, writes out
k(stream=s)     # argless + async on a caller-owned stream

For C-style integrations, raw device pointers enter the same way through a new non-owning wrapper:

buf = fkl.DeviceBuffer.from_ptr(ptr, (1080, 1920, 3), "uint8")
k = fkl.compose(fkl.TensorRead(buf), ..., fkl.TensorWrite(dst))

Why

  • Ergonomics: steady-state pipelines (video loops, preallocated ping-pong buffers, DNN pre/post-processing) re-launch the same kernel on the same buffers every frame; threading the same arguments through every call was noise. A bound kernel is one object that captures the whole dataflow.
  • Predictable latency: a bound read fixes the input dtype/shape at compose() time, so compilation happens eagerly there instead of on the first call — the kernel is ready-to-run before the hot loop starts (disk-cache hit when the signature was ever compiled before).
  • Correctness surface: bound kernels validate call-time overrides against the compiled signature and raise clear ValueErrors on dtype/shape mismatch, instead of silently compiling additional variants.

Design

  • Binding lives on the read/write descriptors (TensorRead(x), TensorWrite(out); also TensorSplit/SplitWrite/TensorTSplit dest and TensorPack(ch, source)), matching how emit_read()/emit_write() already make IO the IOp's own concern. No new compose() kwargs.
  • The binding is VALUE-level, exactly like params[]: it never appears in token(), the signature, or the generated C++ — bound and unbound composes of the same chain share one cached .so (generate_cu output is byte-identical to main for all chains).
  • Bound buffers are call defaults: k(y) / k(y, out=z) override them (validated); read-only binding auto-allocates the output per call; write-only binding keeps the lazy pipe(x) path with the bound buffer as default out=. Unbound chains behave exactly as before.
  • DeviceBuffer.from_ptr(ptr, shape, dtype, stream=None, device=0) is NON-OWNING: __del__ and the DLPack deleter never free external memory (new _owns guard); the caller keeps the allocation alive for as long as the wrapper is in use (documented). The optional producer stream is advertised through __cuda_array_interface__ v3.
  • Because from_ptr is a raw-pointer API, malformed inputs fail loudly at the call site instead of corrupting memory later: a NULL/None pointer raises ValueError; passing a vector dtype AND the trailing channel dim ((H, W, 3) + "uint8x3", which would silently size the wrapper at 3x the real allocation) raises TypeError with the two valid spellings; a producer stream handle of 0 is advertised as stream: 1 per CAI v3 (the spec's spelling of the legacy default stream — raw 0 is disallowed), so the synchronization contract is preserved rather than dropped.
  • GPU-only (target="cpu" rejects bindings), lists (batch HF) cannot be bound, and compose_divergent rejects bindings — all with explicit errors.

Tests

  • New tests/test_bound_compose.py (dependency-free harness, 41 checks): argless call incl. in-place input refill, k(stream=s) on a driver-API stream, eager compile + cached second compose (same .so), read-only/write-only bindings, overrides, from_ptr round-trips (wrapping another buffer's pointer as input AND as raw output, vector dtype spec, owner memory surviving wrapper deletion), from_ptr guards (null pointer, ambiguous vector-dtype + trailing-dim spec, CAI v3 stream spelling for 0/None/explicit handles), and 10 error cases (dtype/shape override mismatch, out= mismatch, bound-out mismatch at compose, argless without binding, batch override, cpu target, list binding, divergent binding) plus unbound-path regression checks.
  • 5 torch-marked checks added to tests/test_torch_integration.py (existing skip-if-no-torch pattern): eager compile, argless call, argless on an external torch stream, override, mismatch error.
  • Full core suite re-run on GB10 (sm_121, CUDA 13.0) on BOTH backends (clang, nvcc): all suites green except one pre-existing Saturate failure (fails identically on pristine main on this machine, unrelated to this change).

Docs

README section + status table row, examples/12_bound_pipeline.py (+ index entry), and updates to the fkl-python-usage / fkl-python-testing skills.

🤖 Generated with Claude Code

Read/write descriptors optionally take a concrete buffer at construction
(TensorRead(x), TensorWrite(out); also TensorSplit/SplitWrite/TensorTSplit
dest and TensorPack source). Binding is VALUE-level, like params[]: it
never touches token()/codegen — only which pointer the launch uses.

A chain with a bound read knows its dtype/shape at compose() time, so
FusedKernel compiles EAGERLY there and runs argument-free: k(), k(stream=s).
Bound buffers are call defaults — k(y) / k(y, out=z) override them,
validated against the compiled signature (ValueError on dtype/shape
mismatch; a bound kernel is one signature by design). Read-only binding
auto-allocates the output per call; write-only binding keeps the lazy
pipe(x) path with the bound buffer as default out=. compose_divergent
rejects bindings explicitly (its batch arrives at call time). Unbound
chains are untouched (generate_cu output verified byte-identical vs
upstream/main).

DeviceBuffer.from_ptr(ptr, shape, dtype, stream=None, device=0) wraps an
external raw device pointer NON-OWNING (C-style integrations): __del__ and
the DLPack deleter never free it — the caller keeps ownership/lifetime.
The optional producer stream is advertised via __cuda_array_interface__ v3.
Guards on a raw-pointer API: NULL/None ptr raises ValueError; passing a
vector dtype AND the trailing channel dim (shape (H, W, 3) + uint8x3, which
would silently size the wrapper 3x the allocation) raises TypeError; a
producer stream handle of 0 is advertised as 1 (CAI v3 spells the legacy
default stream as 1 — the raw 0 is disallowed by the spec).

tests/test_bound_compose.py: 41 checks (argless call, eager compile +
cache-hit second compose, overrides, from_ptr round-trip incl. raw-pointer
output, from_ptr guards, error cases) — 41/41 on GB10 (sm_121, CUDA 13.0)
on BOTH backends (clang, nvcc). Full core suite re-run on both backends:
all green except the pre-existing Saturate failures (also fail on pristine
upstream/main on this machine, unrelated). torch additions in
test_torch_integration.py skip cleanly without torch. New example:
examples/12_bound_pipeline.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant