Skip to content

Add JAX FFI Host support#1446

Open
loney7 wants to merge 1 commit into
NVIDIA:mainfrom
loney7:loney7/ffi-host-support
Open

Add JAX FFI Host support#1446
loney7 wants to merge 1 commit into
NVIDIA:mainfrom
loney7:loney7/ffi-host-support

Conversation

@loney7

@loney7 loney7 commented May 8, 2026

Copy link
Copy Markdown

Description

This PR adds support for running JAX FFI callbacks on the CPU (Host) in addition to CUDA.

Changes:

  • Registered FFI targets for both "CUDA" and "Host" platforms in register_ffi_callback.
  • Handled the "Host" platform case in ffi_callback by using the CPU device and bypassing CUDA-specific features like streams and graphs.
  • Updated FfiCallable to reconstruct arguments and execute the function on the CPU when running on the Host platform.

Checklist

  • I am familiar with the Contributing Guidelines.
  • New or existing tests cover these changes.
  • The documentation is up to date with these changes.

Test plan

You can verify these changes by running the JAX interop tests which include FFI tests. Ensure they pass on both CPU and GPU (if available).

uv run warp/tests/interop/test_jax.py


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
  * Separate CUDA and CPU execution paths for FFI callbacks with device-aware execution scoping.
  * CUDA graph compatibility enabled only for CUDA execution.
  * CPU/host path can take a direct Python execution route for faster host calls.
  * Separate CUDA and Host callback registrations for JAX interop.

* **Tests**
  * Added CPU/host FFI tests covering kernels and callables (add, sincos, in/out args, scale).
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

@copy-pr-bot

copy-pr-bot Bot commented May 8, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented May 8, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This pull request extends JAX experimental FFI callbacks to support both CUDA and Host (CPU) execution paths. Callbacks now accept a platform parameter, register separate FFI targets for each platform, and conditionally dispatch to platform-appropriate device and kernel launch logic. CUDA-specific traits and graph capture modes are guarded by platform checks.

Changes

CUDA and Host FFI Execution

Layer / File(s) Summary
Callback Protocol & Platform Parameter
warp/_src/jax_experimental/ffi.py
FfiKernel.ffi_callback(), FfiCallable.ffi_callback(), and register_ffi_callback() callbacks gain platform="CUDA" parameter. CUDA graph compatibility traits are now enabled only when platform=="CUDA".
Dual-Platform FFI Registration
warp/_src/jax_experimental/ffi.py
FfiKernel and FfiCallable register separate FFI capsules for both CUDA and Host platforms, each passing the appropriate platform argument. register_ffi_callback() stores capsules under distinct registry keys (_cuda and _host suffixes).
FfiKernel Platform-Conditional Launch
warp/_src/jax_experimental/ffi.py
FfiKernel execution branches by platform: CUDA selects CUDA device and stream, calls wp_cuda_launch_kernel; Host uses CPU device with no stream, calls wp_cpu_launch_kernel.
FfiCallable Host Execution Short-Circuit
warp/_src/jax_experimental/ffi.py
When platform=="Host", FfiCallable reconstructs Warp arrays on the CPU device from the call frame and directly invokes the wrapped Python function, bypassing all CUDA graph logic.
FfiCallable CUDA Execution & Graph Capture
warp/_src/jax_experimental/ffi.py
CUDA graph modes are guarded by device.is_cuda. Execution scopes conditionally use ScopedStream (when stream present) or ScopedDevice, restricting graph capture and replay to CUDA devices only.
CPU Host Tests
warp/tests/interop/test_jax.py
New CPU-only jax_kernel and jax_callable tests added and registered in TestJax with devices=None.

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add JAX FFI Host support' directly and clearly summarizes the main change: adding Host/CPU platform support to JAX FFI callbacks alongside existing CUDA support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented May 8, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds Host (CPU) platform support to the JAX FFI interop layer, enabling jax_kernel, jax_callable, and register_ffi_callback to dispatch Warp kernels and callables on CPU devices via JAX's FFI mechanism.

  • Dual-platform callback registration: FfiKernel, FfiCallable, and register_ffi_callback now use a make_callback factory to create separate platform-scoped closures for "CUDA" and "Host", each registered independently with jax.ffi.register_ffi_target. The Host registration is wrapped in a try/except AttributeError to gracefully degrade on older JAX versions.
  • Platform-guarded execution paths: CUDA-only operations (stream and device-ordinal retrieval from call frames, wp_cuda_launch_kernel, CUDA graph traits) are gated behind if platform == "CUDA" guards; the Host path uses wp.get_device("cpu") and calls wp_cpu_launch_kernel via _build_cpu_args_structs. ExecutionContext and FfiBuffer in xla_ffi.py are updated to accept a platform kwarg, conditionally querying the CUDA stream and conditionally exposing __cuda_array_interface__ vs. __array_interface__.
  • New Host test coverage: Nine test functions covering kernel add/sincos/in-out/scale, callable scale/in-out, the low-level FFI callback API, and tile-kernel regression on CPU are added to test_jax.py.

Confidence Score: 5/5

All previously flagged correctness issues have been resolved; the Host execution path is correctly separated from CUDA throughout the call chain.

The make_callback factory eliminates the NameError on ffi_capsule_host; platform guards correctly prevent CUDA stream/device queries on Host call frames; wp_cpu_launch_kernel receives the right arguments; ExecutionContext conditionally skips the CUDA stream accessor; and ScopedStream safely accepts None. The new test suite covers the concrete Host call paths including a tile-kernel regression.

No files require special attention. The core logic in warp/_src/jax/ffi.py has been carefully updated, and the test additions in test_jax.py cover the new paths well.

Important Files Changed

Filename Overview
warp/_src/jax/ffi.py Core Host-platform additions: make_callback factory, platform-guarded stream/device retrieval, correct wp_cpu_launch_kernel invocation, and module preload for CPU devices. All previously flagged issues resolved.
warp/_src/jax/xla_ffi.py ExecutionContext now conditionally queries CUDA stream based on platform; FfiBuffer exposes array_interface for Host and raises AttributeError for platform-mismatched interfaces.
warp/tests/interop/test_jax.py Nine new Host-platform tests added; merge conflict markers from earlier revision are absent; tests use correct JAX device context and skip decorators.
CHANGELOG.md Changelog entry added for Host platform FFI support.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant JAX as JAX Runtime
    participant FFI as ffi.py (FfiKernel/FfiCallable)
    participant XLA as xla_ffi.py
    participant WP as Warp Runtime

    Note over FFI: __init__: make_callback("CUDA") + make_callback("Host")
    FFI->>JAX: "register_ffi_target(name, capsule_cuda, platform="CUDA")"
    FFI->>JAX: "register_ffi_target(name, capsule_host, platform="Host")"

    alt CUDA dispatch
        JAX->>FFI: "ffi_callback(call_frame, platform="CUDA")"
        FFI->>XLA: get_stream_from_callframe(call_frame)
        FFI->>WP: get_cuda_device(device_ordinal)
        FFI->>XLA: "ExecutionContext(callframe, platform="CUDA")"
        XLA->>XLA: "stream = get_stream_from_callframe(callframe)"
        FFI->>WP: wp_cuda_launch_kernel(...)
    else Host dispatch
        JAX->>FFI: "ffi_callback(call_frame, platform="Host")"
        Note over FFI: skip stream/device_ordinal from callframe
        FFI->>WP: get_device("cpu")
        FFI->>XLA: "ExecutionContext(callframe, platform="Host")"
        XLA->>XLA: "stream = None"
        FFI->>WP: _build_cpu_args_structs(kernel, hooks, params, False)
        FFI->>WP: wp_cpu_launch_kernel(func, bounds, args, None, None)
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant JAX as JAX Runtime
    participant FFI as ffi.py (FfiKernel/FfiCallable)
    participant XLA as xla_ffi.py
    participant WP as Warp Runtime

    Note over FFI: __init__: make_callback("CUDA") + make_callback("Host")
    FFI->>JAX: "register_ffi_target(name, capsule_cuda, platform="CUDA")"
    FFI->>JAX: "register_ffi_target(name, capsule_host, platform="Host")"

    alt CUDA dispatch
        JAX->>FFI: "ffi_callback(call_frame, platform="CUDA")"
        FFI->>XLA: get_stream_from_callframe(call_frame)
        FFI->>WP: get_cuda_device(device_ordinal)
        FFI->>XLA: "ExecutionContext(callframe, platform="CUDA")"
        XLA->>XLA: "stream = get_stream_from_callframe(callframe)"
        FFI->>WP: wp_cuda_launch_kernel(...)
    else Host dispatch
        JAX->>FFI: "ffi_callback(call_frame, platform="Host")"
        Note over FFI: skip stream/device_ordinal from callframe
        FFI->>WP: get_device("cpu")
        FFI->>XLA: "ExecutionContext(callframe, platform="Host")"
        XLA->>XLA: "stream = None"
        FFI->>WP: _build_cpu_args_structs(kernel, hooks, params, False)
        FFI->>WP: wp_cpu_launch_kernel(func, bounds, args, None, None)
    end
Loading

Reviews (21): Last reviewed commit: "Add JAX FFI Host (CPU) support" | Re-trigger Greptile

Comment thread warp/_src/jax/ffi.py Outdated
Comment thread warp/_src/jax/ffi.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@warp/_src/jax_experimental/ffi.py`:
- Around line 1772-1777: In register_ffi_callback, the Host ffi capsule is
constructed from the wrong variable (ffi_capsule_host) causing a NameError;
change the construction to use the host ccall address value by calling
jax.ffi.pycapsule(ffi_ccall_address_host.value) and then register that capsule
with jax.ffi.register_ffi_target(name, ffi_capsule_host, platform="Host") so the
Host path mirrors the CUDA path (refer to ffi_ccall_address_host,
ffi_capsule_host, register_ffi_target).
- Around line 629-632: The code assigns ffi_ccall_address_host then creates
ffi_capsule_host but mistakenly uses ffi_capsule_host.value (self-referential
NameError); change the capsule creation to use the previously computed address
value (ffi_ccall_address_host.value) so the lines around callback_func_host,
ffi_ccall_address_host, ffi_capsule_host and the
jax.ffi.register_ffi_target(self.name, ffi_capsule_host, platform="Host") call
use ffi_ccall_address_host.value when constructing the pycapsule for the Host
callback that wraps FFI_CCALLFUNC and calls self.ffi_callback.
- Around line 226-229: The Host FFI registration references an undefined
variable: replace the erroneous creation of ffi_capsule_host (currently using
ffi_capsule_host.value) with a capsule built from the c_void_p address you just
made; specifically, after creating callback_func_host and
ffi_ccall_address_host, set ffi_capsule_host =
jax.ffi.pycapsule(ffi_ccall_address_host.value) and then call
jax.ffi.register_ffi_target(self.name, ffi_capsule_host, platform="Host") so the
capsule is created from the correct address (symbols: callback_func_host,
FFI_CCALLFUNC, ffi_ccall_address_host, ffi_capsule_host,
jax.ffi.register_ffi_target, self.name, self.ffi_callback).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Enterprise

Run ID: d2c93d0f-cbef-4891-b361-773ad8140f6c

📥 Commits

Reviewing files that changed from the base of the PR and between 54327e3 and 09569ba.

📒 Files selected for processing (1)
  • warp/_src/jax_experimental/ffi.py

Comment thread warp/_src/jax/ffi.py Outdated
Comment thread warp/_src/jax/ffi.py Outdated
Comment thread warp/_src/jax/ffi.py Outdated
Comment thread warp/_src/jax/ffi.py
@loney7

loney7 commented May 8, 2026

Copy link
Copy Markdown
Author

pre-commit.ci autofix

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@warp/tests/interop/test_jax.py`:
- Around line 2422-2450: The host-only test registrations for TestJax (the
add_function_test calls registering test_ffi_jax_kernel_host_add,
test_ffi_jax_kernel_host_sincos, test_ffi_jax_kernel_host_in_out,
test_ffi_jax_kernel_host_scale_vec_constant,
test_ffi_jax_callable_host_scale_constant, and
test_ffi_jax_callable_host_in_out) are currently inside the
jax_compatible_cuda_devices conditional; move these specific add_function_test
calls out of that CUDA-only if block so they are always registered on CPU-only
setups, keeping the existing device=None argument and leaving CUDA/GPU-specific
registrations inside the original conditional.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Enterprise

Run ID: fcfb2e10-e509-41b9-b675-c8960056b6be

📥 Commits

Reviewing files that changed from the base of the PR and between d39169c and f6048f4.

📒 Files selected for processing (1)
  • warp/tests/interop/test_jax.py

Comment thread warp/tests/interop/test_jax.py Outdated
@loney7

loney7 commented May 9, 2026

Copy link
Copy Markdown
Author

pre-commit.ci autofix

@shi-eric shi-eric requested a review from nvlukasz May 12, 2026 16:45
Comment thread warp/tests/interop/test_jax.py Outdated
Comment thread warp/_src/jax/ffi.py
@shi-eric

Copy link
Copy Markdown
Contributor

Please add a CHANGELOG.md entry under Unreleased for this JAX Host FFI behavior change.

@shi-eric

Copy link
Copy Markdown
Contributor

Please squash this PR down to a single coherent commit before merge.

@shi-eric

Copy link
Copy Markdown
Contributor

Please rebase onto current main and move the fix/tests to the promoted JAX code paths. Commit 604a8961df6d40ea64ff1e740b23581e4c72c96f promoted the JAX code from jax_experimental to jax after this PR was opened, so the final diff should target the current locations.

@nvlukasz nvlukasz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the contribution. Please address outstanding comments or let us know if you are unable to do so.

Comment thread warp/_src/jax/ffi.py Outdated
Comment thread warp/_src/jax_experimental/ffi.py Outdated
Comment thread warp/_src/jax_experimental/ffi.py Outdated
@loney7

loney7 commented May 19, 2026

Copy link
Copy Markdown
Author

Thank you for the contribution. Please address outstanding comments or let us know if you are unable to do so.

@nvlukasz , please allow till the end of this week to address the outstanding comments. I apologise for the delay.

@loney7 loney7 force-pushed the loney7/ffi-host-support branch 2 times, most recently from 766a819 to cc5aa99 Compare June 9, 2026 05:46
@loney7

loney7 commented Jun 9, 2026

Copy link
Copy Markdown
Author

pre-commit.ci autofix

@loney7 loney7 force-pushed the loney7/ffi-host-support branch from cc5aa99 to c06e8d4 Compare June 9, 2026 05:48
@loney7

loney7 commented Jun 9, 2026

Copy link
Copy Markdown
Author

pre-commit.ci autofix

@loney7 loney7 marked this pull request as ready for review June 9, 2026 07:27
@loney7 loney7 force-pushed the loney7/ffi-host-support branch from 420290b to 38f4e20 Compare June 9, 2026 07:37
@loney7

loney7 commented Jun 9, 2026

Copy link
Copy Markdown
Author

pre-commit.ci autofix

@btaba

btaba commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Hi @nvlukasz wondering if you could take another pass :)

@loney7 loney7 force-pushed the loney7/ffi-host-support branch from df57595 to f752ab9 Compare June 9, 2026 20:17
@loney7

loney7 commented Jun 9, 2026

Copy link
Copy Markdown
Author

pre-commit.ci autofix

1 similar comment
@loney7

loney7 commented Jun 9, 2026

Copy link
Copy Markdown
Author

pre-commit.ci autofix

@greptile-apps

greptile-apps Bot commented Jun 9, 2026

Copy link
Copy Markdown

Want your agent to iterate on Greptile's feedback? Try greploops.

@loney7 loney7 force-pushed the loney7/ffi-host-support branch from 5d52c5c to 865d5ef Compare June 24, 2026 07:21
@loney7

loney7 commented Jun 24, 2026

Copy link
Copy Markdown
Author

/ok to test 865d5ef

@loney7 loney7 force-pushed the loney7/ffi-host-support branch from d6973f2 to 62590b8 Compare June 24, 2026 09:46

Copy link
Copy Markdown
Contributor

Could you also fix this Host jax_kernel placement case? It fails when jax_kernel is traced with CUDA as the effective JAX default device, but the actual call executes on CPU.

Minimal repro:

import jax
import jax.numpy as jnp
import numpy as np
import warp as wp


@wp.kernel
def add_kernel(
    a: wp.array[wp.float32],
    b: wp.array[wp.float32],
    out: wp.array[wp.float32],
):
    tid = wp.tid()
    out[tid] = a[tid] + b[tid]


jax_add = wp.jax_kernel(add_kernel)


@jax.jit
def f(a, b):
    (out,) = jax_add(a, b)
    return out


cpu = jax.devices("cpu")[0]

# Leave the effective JAX default device as CUDA, but place inputs on CPU.
a = jax.device_put(jnp.arange(16, dtype=jnp.float32), cpu)
b = jax.device_put(jnp.ones(16, dtype=jnp.float32), cpu)

out = f(a, b)
jax.block_until_ready(out)

np.testing.assert_allclose(np.asarray(out), np.arange(1, 17, dtype=np.float32))

On this branch, __call__ preloads the module for the default CUDA device. Execution then enters the Host callback, selects wp.get_device("cpu"), and fails when fetching CPU hooks:

RuntimeError: Module is not loaded on device cpu

The Host callback should ensure the kernel module is loaded for CPU before calling get_kernel_hooks(..., device). This would be good to cover with a regression test that leaves the effective JAX default device on CUDA while passing CPU-placed inputs.

Copy link
Copy Markdown
Contributor

Could you also fix the generic Host FFI callback buffer path? The Host target is registered, but the buffers passed to the user callback are still FfiBuffer objects that only expose __cuda_array_interface__. On CPU, passing those buffers directly to wp.launch(..., device="cpu") fails because Warp looks for __array_interface__.

Minimal repro:

import warnings

import jax
import jax.numpy as jnp
import warp as wp

with warnings.catch_warnings():
    warnings.simplefilter("ignore", DeprecationWarning)
    from warp.jax_experimental import register_ffi_callback


@wp.kernel
def double_kernel(inp: wp.array[wp.float32], out: wp.array[wp.float32]):
    tid = wp.tid()
    out[tid] = 2.0 * inp[tid]


def warp_callback(inputs, outputs, attrs, ctx):
    inp = inputs[0]
    out = outputs[0]

    assert ctx.stream is None
    assert hasattr(inp, "__cuda_array_interface__")
    assert not hasattr(inp, "__array_interface__")

    wp.launch(double_kernel, dim=inp.shape[0], inputs=[inp], outputs=[out], device="cpu")


register_ffi_callback("warp_generic_host_buffer_repro", warp_callback)


@jax.jit
def f(x):
    out_type = jax.ShapeDtypeStruct(x.shape, x.dtype)
    call = jax.ffi.ffi_call("warp_generic_host_buffer_repro", out_type)
    return call(x)


with jax.default_device(jax.devices("cpu")[0]):
    x = jnp.arange(16, dtype=jnp.float32)
    out = f(x)

jax.block_until_ready(out)

On this branch, the Host callback is reached, but the CPU launch fails while packing the FfiBuffer:

RuntimeError: Error launching kernel 'double_kernel', argument 'inp' expects an array of type array(ndim=1, dtype=float32), but passed value has type FfiBuffer.

Can the Host generic callback path expose CPU-compatible buffers, e.g. via __array_interface__, so the same callback pattern supported on CUDA also works with wp.launch(..., device="cpu")?

loney7 pushed a commit to loney7/warp that referenced this pull request Jun 25, 2026
Add support for running JAX FFI callbacks on the CPU (Host) platform
in addition to CUDA.

Changes:
- Register FFI targets for both "CUDA" and "Host" platforms in
  FfiKernel, FfiCallable, and register_ffi_callback.
- Handle the "Host" platform case in ffi_callback by using the CPU
  device and bypassing CUDA-specific features like streams and graphs.
- Update FfiCallable to reconstruct arguments and execute the function
  on the CPU when running on the Host platform.
- Make FfiBuffer platform-aware: expose __array_interface__ for Host
  and __cuda_array_interface__ for CUDA.
- Ensure kernel module is loaded on the target device before getting
  hooks, fixing the case where JAX default device is CUDA but the
  callback runs on Host.
- Guard COMMAND_BUFFER_COMPATIBLE traits and graph_mode setup behind
  platform == "CUDA" checks.
- Add CPU/Host FFI tests covering kernels, callables, and callbacks.

(NVIDIAGH-1446)

Signed-off-by: Ankit Jain <kitsrish@google.com>
@loney7 loney7 force-pushed the loney7/ffi-host-support branch from 62590b8 to c9d6224 Compare June 25, 2026 14:28
@loney7 loney7 force-pushed the loney7/ffi-host-support branch from c9d6224 to fbca388 Compare July 3, 2026 23:22
loney7 pushed a commit to loney7/warp that referenced this pull request Jul 3, 2026
Add support for running JAX FFI callbacks on the CPU (Host) platform
in addition to CUDA.

Changes:
- Register FFI targets for both "CUDA" and "Host" platforms in
  FfiKernel, FfiCallable, and register_ffi_callback.
- Handle the "Host" platform case in ffi_callback by using the CPU
  device and bypassing CUDA-specific features like streams and graphs.
- Update FfiCallable to reconstruct arguments and execute the function
  on the CPU when running on the Host platform.
- Make FfiBuffer platform-aware: expose __array_interface__ for Host
  and __cuda_array_interface__ for CUDA.
- Ensure kernel module is loaded on the target device before getting
  hooks, fixing the case where JAX default device is CUDA but the
  callback runs on Host.
- Guard COMMAND_BUFFER_COMPATIBLE traits and graph_mode setup behind
  platform == "CUDA" checks.
- Add CPU/Host FFI tests covering kernels, callables, and callbacks.

(NVIDIAGH-1446)

Signed-off-by: Ankit Jain <kitsrish@google.com>
@loney7

loney7 commented Jul 4, 2026

Copy link
Copy Markdown
Author

@shi-eric , can you please have a look

@shi-eric

shi-eric commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Could you hard-code block_dim=1 for Host callbacks while keeping CUDA at 256?

jax_kernel() does not expose block_dim to users, and Warp defines CPU tile execution as single-lane. Loading the CPU module with the default value of 256 compiles WP_TILE_BLOCK_DIM incorrectly, so explicitly shaped tile operations can process only lane 0's portion of the tile.

Something along these lines:

block_dim = 256 if device.is_cuda else 1
module_exec = self.kernel.module.load(device, block_dim)
hooks = module_exec.get_kernel_hooks(self.kernel)

Could you also add a Host regression test that stores an explicitly shaped tile, such as tile_ones(shape=64), and verifies that all elements are written?

@loney7 loney7 force-pushed the loney7/ffi-host-support branch from fbca388 to 7253c19 Compare July 5, 2026 11:46
loney7 pushed a commit to loney7/warp that referenced this pull request Jul 5, 2026
Add support for running JAX FFI callbacks on the CPU (Host) platform
in addition to CUDA.

Changes:
- Register FFI targets for both "CUDA" and "Host" platforms in
  FfiKernel, FfiCallable, and register_ffi_callback.
- Handle the "Host" platform case in ffi_callback by using the CPU
  device and bypassing CUDA-specific features like streams and graphs.
- Update FfiCallable to reconstruct arguments and execute the function
  on the CPU when running on the Host platform.
- Make FfiBuffer platform-aware: expose __array_interface__ for Host
  and __cuda_array_interface__ for CUDA.
- Ensure kernel module is loaded on the target device before getting
  hooks, fixing the case where JAX default device is CUDA but the
  callback runs on Host.
- Guard COMMAND_BUFFER_COMPATIBLE traits and graph_mode setup behind
  platform == "CUDA" checks.
- Add CPU/Host FFI tests covering kernels, callables, and callbacks.

(NVIDIAGH-1446)

Signed-off-by: Ankit Jain <kitsrish@google.com>
@loney7 loney7 force-pushed the loney7/ffi-host-support branch from 7253c19 to caaffcd Compare July 5, 2026 13:30
loney7 pushed a commit to loney7/warp that referenced this pull request Jul 5, 2026
Add support for running JAX FFI callbacks on the CPU (Host) platform
in addition to CUDA.

Changes:
- Register FFI targets for both "CUDA" and "Host" platforms in
  FfiKernel, FfiCallable, and register_ffi_callback.
- Handle the "Host" platform case in ffi_callback by using the CPU
  device and bypassing CUDA-specific features like streams and graphs.
- Update FfiCallable to reconstruct arguments and execute the function
  on the CPU when running on the Host platform.
- Make FfiBuffer platform-aware: expose __array_interface__ for Host
  and __cuda_array_interface__ for CUDA.
- Ensure kernel module is loaded on the target device before getting
  hooks, fixing the case where JAX default device is CUDA but the
  callback runs on Host.
- Guard COMMAND_BUFFER_COMPATIBLE traits and graph_mode setup behind
  platform == "CUDA" checks.
- Add CPU/Host FFI tests covering kernels, callables, and callbacks.

(NVIDIAGH-1446)

Signed-off-by: Ankit Jain <kitsrish@google.com>
Comment thread warp/tests/interop/test_jax.py Outdated
Comment thread warp/_src/jax/ffi.py
loney7 pushed a commit to loney7/warp that referenced this pull request Jul 5, 2026
Add support for running JAX FFI callbacks on the CPU (Host) platform
in addition to CUDA.

Changes:
- Register FFI targets for both "CUDA" and "Host" platforms in
  FfiKernel, FfiCallable, and register_ffi_callback.
- Handle the "Host" platform case in ffi_callback by using the CPU
  device and bypassing CUDA-specific features like streams and graphs.
- Update FfiCallable to reconstruct arguments and execute the function
  on the CPU when running on the Host platform.
- Make FfiBuffer platform-aware: expose __array_interface__ for Host
  and __cuda_array_interface__ for CUDA.
- Ensure kernel module is loaded on the target device before getting
  hooks, fixing the case where JAX default device is CUDA but the
  callback runs on Host.
- Guard COMMAND_BUFFER_COMPATIBLE traits and graph_mode setup behind
  platform == "CUDA" checks.
- Add CPU/Host FFI tests covering kernels, callables, and callbacks.

(NVIDIAGH-1446)

Signed-off-by: Ankit Jain <kitsrish@google.com>
@loney7 loney7 force-pushed the loney7/ffi-host-support branch from caaffcd to 47b367c Compare July 5, 2026 13:49
Comment thread warp/_src/jax/xla_ffi.py
@loney7 loney7 force-pushed the loney7/ffi-host-support branch 2 times, most recently from ca5e33a to 90b7047 Compare July 6, 2026 20:02
loney7 pushed a commit to loney7/warp that referenced this pull request Jul 6, 2026
Add support for running JAX FFI callbacks on the CPU (Host) platform
in addition to CUDA.

Changes:
- Register FFI targets for both "CUDA" and "Host" platforms in
  FfiKernel, FfiCallable, and register_ffi_callback.
- Handle the "Host" platform case in ffi_callback by using the CPU
  device and bypassing CUDA-specific features like streams and graphs.
- Update FfiCallable to reconstruct arguments and execute the function
  on the CPU when running on the Host platform.
- Make FfiBuffer platform-aware: expose __array_interface__ for Host
  and __cuda_array_interface__ for CUDA.
- Ensure kernel module is loaded on the target device before getting
  hooks, fixing the case where JAX default device is CUDA but the
  callback runs on Host.
- Guard COMMAND_BUFFER_COMPATIBLE traits and graph_mode setup behind
  platform == "CUDA" checks.
- Add CPU/Host FFI tests covering kernels, callables, and callbacks.

(NVIDIAGH-1446)

Signed-off-by: Ankit Jain <kitsrish@google.com>
Comment thread warp/_src/jax/xla_ffi.py
loney7 pushed a commit to loney7/warp that referenced this pull request Jul 6, 2026
Add support for running JAX FFI callbacks on the CPU (Host) platform
in addition to CUDA.

Changes:
- Register FFI targets for both "CUDA" and "Host" platforms in
  FfiKernel, FfiCallable, and register_ffi_callback.
- Handle the "Host" platform case in ffi_callback by using the CPU
  device and bypassing CUDA-specific features like streams and graphs.
- Update FfiCallable to reconstruct arguments and execute the function
  on the CPU when running on the Host platform.
- Make FfiBuffer platform-aware: expose __array_interface__ for Host
  and __cuda_array_interface__ for CUDA.
- Ensure kernel module is loaded on the target device before getting
  hooks, fixing the case where JAX default device is CUDA but the
  callback runs on Host.
- Guard COMMAND_BUFFER_COMPATIBLE traits and graph_mode setup behind
  platform == "CUDA" checks.
- Add CPU/Host FFI tests covering kernels, callables, and callbacks.

(NVIDIAGH-1446)

Signed-off-by: Ankit Jain <kitsrish@google.com>
@loney7 loney7 force-pushed the loney7/ffi-host-support branch from 90b7047 to 74bd896 Compare July 6, 2026 20:17
Comment thread warp/_src/jax/xla_ffi.py
Comment thread warp/_src/jax/ffi.py
Add support for running JAX FFI callbacks on the CPU (Host) platform
in addition to CUDA.

Changes:
- Register FFI targets for both "CUDA" and "Host" platforms in
  FfiKernel, FfiCallable, and register_ffi_callback.
- Handle the "Host" platform case in ffi_callback by using the CPU
  device and bypassing CUDA-specific features like streams and graphs.
- Update FfiCallable to reconstruct arguments and execute the function
  on the CPU when running on the Host platform.
- Make FfiBuffer platform-aware: expose __array_interface__ for Host
  and __cuda_array_interface__ for CUDA.
- Ensure kernel module is loaded on the target device before getting
  hooks, fixing the case where JAX default device is CUDA but the
  callback runs on Host.
- Guard COMMAND_BUFFER_COMPATIBLE traits and graph_mode setup behind
  platform == "CUDA" checks.
- Add CPU/Host FFI tests covering kernels, callables, and callbacks.

(NVIDIAGH-1446)

Signed-off-by: Ankit Jain <kitsrish@google.com>
@loney7 loney7 force-pushed the loney7/ffi-host-support branch from 74bd896 to 2617342 Compare July 6, 2026 21:16
@shi-eric

shi-eric commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

/ok to test 2617342

Comment thread warp/_src/jax/ffi.py
@@ -906,7 +963,7 @@ def ffi_callback(self, call_frame):

# call the Python function with reconstructed arguments
with wp.ScopedStream(stream, sync_enter=False):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the Host platform stream is None here, and ScopedStream(None) is a no-op (its
__enter__/__exit__ do nothing when the stream is None), so no device scope is
established around the callback. Device-less wp.launch() calls inside the callback then
resolve to Warp's default device, which is cuda:0 on any machine with a visible GPU. The
kernels launch asynchronously on CUDA with CPU buffer pointers, producing silently wrong
output (or an async CUDA error 700: illegal memory access, depending on whether the GPU
can access pageable host memory).

This is the failure on the GPU runners in
test-warp-gpu-linux
and
test-warp-gpu-linux-mgpu-python310:
test_ffi_jax_callable_host_in_out fails with stale/partially written output (8 of 1M
elements stale in one job, 100% in the other). The other host jax_callable tests only
pass by timing luck; the same pattern as scale_func produced wrong results on my machine.

Suggested change
with wp.ScopedStream(stream, sync_enter=False):
# ScopedStream makes the stream's device current, but there is
# no stream on the Host platform, so use ScopedDevice to route
# device-less launches in the callback to the CPU
if stream is not None:
exec_scope = wp.ScopedStream(stream, sync_enter=False)
else:
exec_scope = wp.ScopedDevice(device)
with exec_scope:

Verified locally on 2x RTX 3090 (CPU jaxlib and jax[cuda13]): before this change, the
host jax_callable tests fail with GPUs visible and pass with CUDA_VISIBLE_DEVICES=;
after it, all host and CUDA FFI tests pass. The CUDA branch is unchanged.

A regression test can pin this down deterministically by asserting on the resolved device
instead of comparing outputs (output comparisons can pass by timing luck on drivers where
the GPU can dereference pageable host memory):

@unittest.skipUnless(_jax_version() >= (0, 5, 0), "Jax version too old")
@unittest.skipUnless(wp.is_cpu_available(), "Requires CPU backend")
def test_ffi_jax_callable_host_default_device(test, device):
    """Verify that Host callbacks route device-less launches to the CPU.

    Regression test for the Host (CPU) FFI path: the callback must run with
    the CPU device current so that ``wp.launch()`` calls without an explicit
    device run on the CPU even when Warp's default device is a CUDA device.
    """
    import jax.numpy as jp  # noqa: PLC0415

    jax_callable = wp.jax_callable

    # devices resolved by device-less launches inside the callback
    resolved_devices = []

    def record_device_func(a: wp.array[float], b: wp.array[float]):
        resolved_devices.append(wp.get_device())
        wp.launch(scale_kernel, dim=a.shape, inputs=[a, 2.0], outputs=[b])

    jax_func = jax_callable(record_device_func)

    @jax.jit
    def f():
        a = jp.arange(ARRAY_SIZE, dtype=jp.float32)
        return jax_func(a, output_dims={"b": a.shape})

    # force a non-CPU default device to expose launches that don't inherit the
    # callback's device scope
    device_scope = wp.ScopedDevice("cuda:0") if wp.is_cuda_available() else contextlib.nullcontext()

    with device_scope, jax.default_device(jax.devices("cpu")[0]):
        (b,) = f()
        jax.block_until_ready(b)

    test.assertGreater(len(resolved_devices), 0)
    for d in resolved_devices:
        test.assertEqual(d, wp.get_device("cpu"))

    assert_np_equal(np.asarray(b), 2 * np.arange(ARRAY_SIZE, dtype=np.float32))

(plus an add_function_test(TestJax, "test_ffi_jax_callable_host_default_device", ..., devices=None)
registration next to the other host tests)

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.

4 participants