Import Warp v1.16.0

PiperOrigin-RevId: 966641228
Change-Id: I3b55b6c607a4a09eb714304db27e62ed391c7f56
This commit is contained in:
Google DeepMind
2026-08-18 09:35:21 -07:00
committed by Copybara-Service
parent 021c804bf8
commit a4f0afc012
7 changed files with 289 additions and 183 deletions
+5 -5
View File
@@ -20,8 +20,8 @@ jax-cuda12-pjrt==0.8.3; python_version >= '3.13' \
jax-cuda12-pjrt==0.5.3; python_version < '3.13' \ jax-cuda12-pjrt==0.5.3; python_version < '3.13' \
--hash=sha256:04ee111eaf5fc2692978ad4a5c84d5925e42eb05c1701849ba3a53f6515400cc \ --hash=sha256:04ee111eaf5fc2692978ad4a5c84d5925e42eb05c1701849ba3a53f6515400cc \
--hash=sha256:c5378306568ba0c81b230a779dd3194c9dd10339ab6360ae80928108d37e7f75 --hash=sha256:c5378306568ba0c81b230a779dd3194c9dd10339ab6360ae80928108d37e7f75
warp-lang==1.15.0 \ warp-lang==1.16.0 \
--hash=sha256:06ef42c3ce522749268056653ea253645eb4a96ca164af74ae5113f0d55a6970 \ --hash=sha256:19a7590c4484e8250ab19165311d2a1774138663b361c41e8be2865c7515c4ae \
--hash=sha256:3c0711b39ad98ff924d29654b028ec4eaa02330e207ae3efc72445e9cde05b34 \ --hash=sha256:8690c9096e0a271985339d4aa4b37675e7d62852620ca861ac032dc85b124542 \
--hash=sha256:95c169f28bd7d6c78ac4ad62e2df1e61a096033748f757157fa4551aed80d010 \ --hash=sha256:96449fc1e3b354185e2f09434fb794b5953ab2e8673b104d7e7f48d5d418bb35 \
--hash=sha256:ee06e2e76bff84088a09e64315e71ed0e55e94c936ec664d7b05ebd56f66a363 --hash=sha256:d4715171bd6821436b82293d774c9a082ace08215c189572b4348d1e48fb8ee8
+5 -1
View File
@@ -568,7 +568,11 @@ def put_model(
return _put_model_jax(m, device) return _put_model_jax(m, device)
elif impl == types.Impl.WARP: elif impl == types.Impl.WARP:
_check_warp_installed() _check_warp_installed()
graph_mode = graph_mode or getattr(mjxw.types.GraphMode, 'WARP') if graph_mode is None:
if device.platform == 'cpu':
graph_mode = getattr(mjxw.types.GraphMode, 'JAX')
else:
graph_mode = getattr(mjxw.types.GraphMode, 'WARP')
return _put_model_warp(m, graph_mode, device, batch_sizes=batch_sizes) return _put_model_warp(m, graph_mode, device, batch_sizes=batch_sizes)
elif impl == types.Impl.CPP: elif impl == types.Impl.CPP:
return _put_model_cpp(m, device, keepalive_refs=keepalive_refs) return _put_model_cpp(m, device, keepalive_refs=keepalive_refs)
-2
View File
@@ -3,8 +3,6 @@
import warp import warp
_wp_module_name_ = "warp.jax"
def device_to_jax(warp_device: warp.DeviceLike): def device_to_jax(warp_device: warp.DeviceLike):
"""Return the Jax device corresponding to a Warp device. """Return the Jax device corresponding to a Warp device.
@@ -10,8 +10,6 @@ from mujoco.mjx.third_party.warp._src.jax import get_jax_device
from warp._src.logger import log_warning from warp._src.logger import log_warning
from warp._src.types import array_t, matches_array_class, strides_from_shape from warp._src.types import array_t, matches_array_class, strides_from_shape
_wp_module_name_ = "warp.jax.custom_call"
_jax_warp_p = None _jax_warp_p = None
# Holder for the custom callback to keep it alive. # Holder for the custom callback to keep it alive.
+278 -170
View File
@@ -6,6 +6,7 @@ from __future__ import annotations
import collections import collections
import ctypes import ctypes
import inspect import inspect
import operator
import threading import threading
import traceback import traceback
from collections.abc import Callable from collections.abc import Callable
@@ -13,7 +14,13 @@ from enum import IntEnum
import warp as wp import warp as wp
from warp._src.codegen import get_full_arg_spec, make_full_qualified_name from warp._src.codegen import get_full_arg_spec, make_full_qualified_name
from warp._src.context import CudaMemcpyKind, _build_launch_bounds from warp._src.context import (
CudaMemcpyKind,
_build_launch_bounds,
_raise_cuda_launch_error,
_validate_cluster_launch,
invoke,
)
from mujoco.mjx.third_party.warp._src.jax import get_jax_device from mujoco.mjx.third_party.warp._src.jax import get_jax_device
from warp._src.logger import log_warning from warp._src.logger import log_warning
from warp._src.types import ( from warp._src.types import (
@@ -26,8 +33,6 @@ from warp._src.types import (
from .xla_ffi import * from .xla_ffi import *
_wp_module_name_ = "warp.jax.ffi"
# Holders for the custom callbacks to keep them alive. # Holders for the custom callbacks to keep them alive.
_FFI_KERNEL_REGISTRY: dict[tuple, FfiKernel] = {} _FFI_KERNEL_REGISTRY: dict[tuple, FfiKernel] = {}
_FFI_DIFF_KERNEL_REGISTRY: dict[tuple, Callable] = {} _FFI_DIFF_KERNEL_REGISTRY: dict[tuple, Callable] = {}
@@ -63,6 +68,31 @@ def check_jax_version():
raise RuntimeError(msg) raise RuntimeError(msg)
# JAX platform identifiers are case-sensitive and intentionally use different casing.
_FFI_PLATFORM_CPU = "cpu"
_FFI_PLATFORM_CUDA = "CUDA"
def _register_ffi_targets(name, callback):
"""Register one FFI callback for CPU and CUDA without initializing either backend."""
jax = _get_jax()
callback_type = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.POINTER(XLA_FFI_CallFrame))
callback_funcs = {}
for platform in (_FFI_PLATFORM_CPU, _FFI_PLATFORM_CUDA):
def platform_callback(call_frame, platform=platform):
return callback(call_frame, platform)
callback_func = callback_type(platform_callback)
callback_address = ctypes.cast(callback_func, ctypes.c_void_p)
callback_capsule = jax.ffi.pycapsule(callback_address.value)
jax.ffi.register_ffi_target(name, callback_capsule, platform=platform)
callback_funcs[platform] = callback_func
return callback_funcs
def collapse_batch_dims(shape, desired_ndim): def collapse_batch_dims(shape, desired_ndim):
# roll leading batch dims into one # roll leading batch dims into one
while len(shape) > desired_ndim: while len(shape) > desired_ndim:
@@ -111,6 +141,65 @@ class JaxModulePreloadMode(IntEnum):
ModulePreloadMode = JaxModulePreloadMode ModulePreloadMode = JaxModulePreloadMode
def _get_ffi_block_dim(device, block_dim=None):
"""Resolve the FFI block dimension for ``device``."""
if device.is_cpu:
# Remove this override if CPU launches gain configurable block dimensions.
return 1
return 256 if block_dim is None else block_dim
def _load_ffi_module(module, device, block_dim=None):
module_exec = module.load(device, _get_ffi_block_dim(device, block_dim))
if module_exec is None:
raise RuntimeError(
f"Failed to load Warp module '{module.name}' on device '{device}' after a previous build failure"
)
return module_exec
def _preload_ffi_module(module, mode, block_dim=None):
if mode == JaxModulePreloadMode.NONE:
return
if mode == JaxModulePreloadMode.CURRENT_DEVICE:
jax_device = get_jax_device()
try:
device = wp.device_from_jax(jax_device)
except (IndexError, RuntimeError):
return
_load_ffi_module(module, device, block_dim)
return
if mode == JaxModulePreloadMode.ALL_DEVICES:
jax = _get_jax()
devices = []
mapped_device_ids = set()
for backend in ("cpu", "cuda"):
try:
jax_devices = jax.local_devices(backend=backend)
except RuntimeError:
continue
for jax_device in jax_devices:
try:
device = wp.device_from_jax(jax_device)
except (IndexError, RuntimeError):
continue
device_id = id(device)
if device_id in mapped_device_ids:
continue
mapped_device_ids.add(device_id)
devices.append(device)
for device in devices:
_load_ffi_module(module, device, block_dim)
return
raise ValueError(f"Unsupported JAX module preload mode '{mode}'")
class FfiArg: class FfiArg:
def __init__(self, name, type, in_out=False): def __init__(self, name, type, in_out=False):
self.name = name self.name = name
@@ -155,6 +244,7 @@ class FfiKernel:
num_outputs, num_outputs,
vmap_method, vmap_method,
launch_dims, launch_dims,
block_dim,
output_dims, output_dims,
in_out_argnames, in_out_argnames,
module_preload_mode, module_preload_mode,
@@ -165,6 +255,7 @@ class FfiKernel:
self.num_outputs = num_outputs self.num_outputs = num_outputs
self.vmap_method = vmap_method self.vmap_method = vmap_method
self.launch_dims = launch_dims self.launch_dims = launch_dims
self.block_dim = block_dim
self.output_dims = output_dims self.output_dims = output_dims
self.module_preload_mode = module_preload_mode self.module_preload_mode = module_preload_mode
self.has_side_effect = has_side_effect self.has_side_effect = has_side_effect
@@ -228,20 +319,7 @@ class FfiKernel:
self.input_output_aliases = input_output_aliases self.input_output_aliases = input_output_aliases
# register the callback # register the callback
jax = _get_jax() self.callback_funcs = _register_ffi_targets(self.name, self.ffi_callback)
FFI_CCALLFUNC = ctypes.CFUNCTYPE(
ctypes.c_void_p, ctypes.POINTER(XLA_FFI_CallFrame)
)
self.callback_func_cuda = FFI_CCALLFUNC(lambda call_frame: self.ffi_callback(call_frame, platform="CUDA"))
ffi_ccall_address_cuda = ctypes.cast(self.callback_func_cuda, ctypes.c_void_p)
ffi_capsule_cuda = jax.ffi.pycapsule(ffi_ccall_address_cuda.value)
jax.ffi.register_ffi_target(self.name, ffi_capsule_cuda, platform="CUDA")
self.callback_func_host = FFI_CCALLFUNC(lambda call_frame: self.ffi_callback(call_frame, platform="Host"))
ffi_ccall_address_host = ctypes.cast(self.callback_func_host, ctypes.c_void_p)
ffi_capsule_host = jax.ffi.pycapsule(ffi_ccall_address_host.value)
jax.ffi.register_ffi_target(self.name, ffi_capsule_host, platform="Host")
def __call__(self, *args, output_dims=None, launch_dims=None, vmap_method=None): def __call__(self, *args, output_dims=None, launch_dims=None, vmap_method=None):
jax = _get_jax() jax = _get_jax()
@@ -332,20 +410,7 @@ class FfiKernel:
has_side_effect=self.has_side_effect, has_side_effect=self.has_side_effect,
) )
# preload on the specified devices _preload_ffi_module(self.kernel.module, self.module_preload_mode, self.block_dim)
if self.module_preload_mode == JaxModulePreloadMode.CURRENT_DEVICE:
device = wp.device_from_jax(get_jax_device())
self.kernel.module.load(device)
elif self.module_preload_mode == JaxModulePreloadMode.ALL_DEVICES:
for d in jax.local_devices():
try:
dev = wp.device_from_jax(d)
# we only support CUDA devices for now
if dev.is_cuda:
self.kernel.module.load(dev)
except Exception:
# ignore unsupported devices like TPUs
pass
# save launch data to be retrieved by callback # save launch data to be retrieved by callback
launch_id = self.launch_id launch_id = self.launch_id
@@ -356,7 +421,7 @@ class FfiKernel:
return call(*args, launch_id=launch_id) return call(*args, launch_id=launch_id)
def ffi_callback(self, call_frame, platform="CUDA"): def ffi_callback(self, call_frame, platform):
try: try:
# On the first call, XLA runtime will query the API version and traits # On the first call, XLA runtime will query the API version and traits
# metadata using the |extension| field. Let us respond to that query # metadata using the |extension| field. Let us respond to that query
@@ -368,8 +433,8 @@ class FfiKernel:
metadata_ext = ctypes.cast(extension, ctypes.POINTER(XLA_FFI_Metadata_Extension)) metadata_ext = ctypes.cast(extension, ctypes.POINTER(XLA_FFI_Metadata_Extension))
metadata_ext.contents.metadata.contents.api_version.major_version = 0 metadata_ext.contents.metadata.contents.api_version.major_version = 0
metadata_ext.contents.metadata.contents.api_version.minor_version = 1 metadata_ext.contents.metadata.contents.api_version.minor_version = 1
# Turn on CUDA graphs for this handler if on CUDA platform. if platform == _FFI_PLATFORM_CUDA:
if platform == "CUDA": # Turn on CUDA graphs for this handler.
metadata_ext.contents.metadata.contents.traits = ( metadata_ext.contents.metadata.contents.traits = (
XLA_FFI_Handler_TraitsBits.COMMAND_BUFFER_COMPATIBLE XLA_FFI_Handler_TraitsBits.COMMAND_BUFFER_COMPATIBLE
) )
@@ -392,8 +457,6 @@ class FfiKernel:
assert num_inputs == self.num_inputs assert num_inputs == self.num_inputs
assert num_outputs == self.num_outputs assert num_outputs == self.num_outputs
# first kernel param is the launch bounds
kernel_params = (ctypes.c_void_p * (1 + self.num_kernel_args))()
arg_refs = [] arg_refs = []
batch_size = None batch_size = None
@@ -411,13 +474,11 @@ class FfiKernel:
) )
strides = strides_from_shape(shape, input_arg.type.dtype) strides = strides_from_shape(shape, input_arg.type.dtype)
arg = array_t(buffer.data, 0, input_arg.type.ndim, shape, strides) arg = array_t(buffer.data, 0, input_arg.type.ndim, shape, strides)
kernel_params[i + 1] = ctypes.addressof(arg)
arg_refs.append(arg) # keep a reference arg_refs.append(arg) # keep a reference
else: else:
# scalar argument, get stashed value # scalar argument, get stashed value
value = launch_desc.static_inputs[input_arg.name] value = launch_desc.static_inputs[input_arg.name]
arg = input_arg.type._type_(value) arg = input_arg.type._type_(value)
kernel_params[i + 1] = ctypes.addressof(arg)
arg_refs.append(arg) # keep a reference arg_refs.append(arg) # keep a reference
# pure output args (skip in-out FFI buffers) # pure output args (skip in-out FFI buffers)
@@ -433,9 +494,40 @@ class FfiKernel:
) )
strides = strides_from_shape(shape, output_arg.type.dtype) strides = strides_from_shape(shape, output_arg.type.dtype)
arg = array_t(buffer.data, 0, output_arg.type.ndim, shape, strides) arg = array_t(buffer.data, 0, output_arg.type.ndim, shape, strides)
kernel_params[num_inputs + i + 1] = ctypes.addressof(arg)
arg_refs.append(arg) # keep a reference arg_refs.append(arg) # keep a reference
if platform == _FFI_PLATFORM_CPU:
if not wp.is_cpu_available():
return create_ffi_error(
call_frame.contents.api,
XLA_FFI_Error_Code.FAILED_PRECONDITION,
"This Warp build does not include CPU support",
)
device = wp.get_device("cpu")
stream = None
elif platform == _FFI_PLATFORM_CUDA:
if wp._src.context.runtime is None:
wp.init()
if not wp._src.context.runtime.is_cuda_enabled:
return create_ffi_error(
call_frame.contents.api,
XLA_FFI_Error_Code.FAILED_PRECONDITION,
"This Warp build does not include CUDA support",
)
device = wp.get_cuda_device(get_device_ordinal_from_callframe(call_frame.contents))
stream = get_stream_from_callframe(call_frame.contents)
else:
return create_invalid_argument_ffi_error(
call_frame.contents.api,
f"Unsupported JAX FFI platform '{platform}'",
)
# Preloading is best-effort, so the callback's actual device must
# load the module before reading code-generation metadata.
module_exec = _load_ffi_module(self.kernel.module, device, self.block_dim)
block_dim = module_exec.block_dim
# determine launch bounds # determine launch bounds
if launch_desc.launch_dims is None: if launch_desc.launch_dims is None:
# infer launch dims from argument shape, works with vmap # infer launch dims from argument shape, works with vmap
@@ -449,42 +541,43 @@ class FfiKernel:
launch_dims = (batch_size * launch_dims[0], *launch_dims[1:]) launch_dims = (batch_size * launch_dims[0], *launch_dims[1:])
launch_bounds = _build_launch_bounds(launch_dims, self.kernel.adj.kernel_dim) launch_bounds = _build_launch_bounds(launch_dims, self.kernel.adj.kernel_dim)
kernel_params[0] = ctypes.addressof(launch_bounds)
# get device and stream if platform == _FFI_PLATFORM_CPU:
if platform == "CUDA": hooks = module_exec.get_kernel_hooks(self.kernel)
device = wp.get_cuda_device(get_device_ordinal_from_callframe(call_frame.contents)) if hooks.forward is None:
stream = get_stream_from_callframe(call_frame.contents) raise RuntimeError("Failed to find CPU kernel entry point")
else: invoke(self.kernel, hooks, [launch_bounds, *arg_refs], adjoint=False)
device = wp.get_device("cpu") return None
stream = None
kernel_params = (ctypes.c_void_p * (1 + self.num_kernel_args))(
ctypes.addressof(launch_bounds),
*(ctypes.addressof(arg) for arg in arg_refs),
)
# get kernel hooks # get kernel hooks
hooks = self.kernel.module.get_kernel_hooks(self.kernel, device) hooks = module_exec.get_kernel_hooks(self.kernel)
assert hooks.forward, "Failed to find kernel entry point" if hooks.forward is None:
raise RuntimeError("Failed to find CUDA kernel entry point")
# launch the kernel # reject non-cluster-aligned grids with a clear Python error instead
if device.is_cuda: # of a cryptic native CUDA error (configured block_dim, max_blocks=0 below)
wp._src.context.runtime.core.wp_cuda_launch_kernel( _validate_cluster_launch(hooks.cluster_dim, launch_bounds.size, block_dim, 0)
device.context,
hooks.forward, # launch the kernel (cluster_dim is cached on the hooks at load time)
launch_bounds.size, if wp._src.context.runtime.core.wp_cuda_launch_kernel(
0, device.context,
256, hooks.forward,
int(self.kernel.grid_stride), launch_bounds.size,
hooks.cluster_dim, 0,
hooks.forward_smem_bytes, block_dim,
kernel_params, int(self.kernel.grid_stride),
stream, hooks.cluster_dim,
None, # apic_info hooks.forward_smem_bytes,
) kernel_params,
else: stream,
wp._src.context.runtime.core.wp_cpu_launch_kernel( None, # apic_info
device.context, ):
hooks.forward, _raise_cuda_launch_error(self.kernel, device)
launch_bounds.size,
kernel_params,
)
except Exception as e: except Exception as e:
print(traceback.format_exc()) print(traceback.format_exc())
@@ -637,20 +730,7 @@ class FfiCallable:
self.input_output_aliases = input_output_aliases self.input_output_aliases = input_output_aliases
# register the callback # register the callback
jax = _get_jax() self.callback_funcs = _register_ffi_targets(self.name, self.ffi_callback)
FFI_CCALLFUNC = ctypes.CFUNCTYPE(
ctypes.c_void_p, ctypes.POINTER(XLA_FFI_CallFrame)
)
self.callback_func_cuda = FFI_CCALLFUNC(lambda call_frame: self.ffi_callback(call_frame, platform="CUDA"))
ffi_ccall_address_cuda = ctypes.cast(self.callback_func_cuda, ctypes.c_void_p)
ffi_capsule_cuda = jax.ffi.pycapsule(ffi_ccall_address_cuda.value)
jax.ffi.register_ffi_target(self.name, ffi_capsule_cuda, platform="CUDA")
self.callback_func_host = FFI_CCALLFUNC(lambda call_frame: self.ffi_callback(call_frame, platform="Host"))
ffi_ccall_address_host = ctypes.cast(self.callback_func_host, ctypes.c_void_p)
ffi_capsule_host = jax.ffi.pycapsule(ffi_ccall_address_host.value)
jax.ffi.register_ffi_target(self.name, ffi_capsule_host, platform="Host")
def __call__(self, *args, output_dims=None, vmap_method=None): def __call__(self, *args, output_dims=None, vmap_method=None):
jax = _get_jax() jax = _get_jax()
@@ -729,21 +809,9 @@ class FfiCallable:
has_side_effect=self.has_side_effect, has_side_effect=self.has_side_effect,
) )
# preload on the specified devices
# NOTE: if the target function uses kernels from different modules, they will not be loaded here # NOTE: if the target function uses kernels from different modules, they will not be loaded here
module = wp.get_module(self.func.__module__) module = wp.get_module(self.func.__module__)
if self.module_preload_mode == JaxModulePreloadMode.CURRENT_DEVICE: _preload_ffi_module(module, self.module_preload_mode)
device = wp.device_from_jax(get_jax_device())
module.load(device)
elif self.module_preload_mode == JaxModulePreloadMode.ALL_DEVICES:
for d in jax.local_devices():
try:
dev = wp.device_from_jax(d)
if dev.is_cuda or dev.is_cpu:
module.load(dev)
except Exception:
# ignore unsupported devices like TPUs
pass
# save call data to be retrieved by callback # save call data to be retrieved by callback
call_id = self.call_id call_id = self.call_id
@@ -751,7 +819,25 @@ class FfiCallable:
self.call_id += 1 self.call_id += 1
return call(*args, call_id=call_id) return call(*args, call_id=call_id)
def ffi_callback(self, call_frame, platform="CUDA"): def _build_arg_list(self, inputs, outputs, call_desc, device):
arg_list = []
for i, arg in enumerate(self.input_args):
if arg.is_array:
buffer = inputs[i].contents
shape = collapse_batch_dims(buffer.dims[: buffer.rank - arg.dtype_ndim], arg.type.ndim)
arg_list.append(wp.array(ptr=buffer.data, dtype=arg.type.dtype, shape=shape, device=device))
else:
arg_list.append(call_desc.static_inputs[arg.name])
for i, arg in enumerate(self.output_args):
buffer = outputs[i + self.num_in_out].contents
shape = collapse_batch_dims(buffer.dims[: buffer.rank - arg.dtype_ndim], arg.type.ndim)
arg_list.append(wp.array(ptr=buffer.data, dtype=arg.type.dtype, shape=shape, device=device))
return arg_list
def ffi_callback(self, call_frame, platform):
try: try:
# On the first call, XLA runtime will query the API version and traits # On the first call, XLA runtime will query the API version and traits
# metadata using the |extension| field. Let us respond to that query # metadata using the |extension| field. Let us respond to that query
@@ -763,8 +849,8 @@ class FfiCallable:
metadata_ext = ctypes.cast(extension, ctypes.POINTER(XLA_FFI_Metadata_Extension)) metadata_ext = ctypes.cast(extension, ctypes.POINTER(XLA_FFI_Metadata_Extension))
metadata_ext.contents.metadata.contents.api_version.major_version = 0 metadata_ext.contents.metadata.contents.api_version.major_version = 0
metadata_ext.contents.metadata.contents.api_version.minor_version = 1 metadata_ext.contents.metadata.contents.api_version.minor_version = 1
# Turn on CUDA graphs for this handler if on CUDA platform. # Turn on CUDA graphs for this handler.
if self.graph_mode is JaxCallableGraphMode.JAX and platform == "CUDA": if platform == _FFI_PLATFORM_CUDA and self.graph_mode is JaxCallableGraphMode.JAX:
metadata_ext.contents.metadata.contents.traits = ( metadata_ext.contents.metadata.contents.traits = (
XLA_FFI_Handler_TraitsBits.COMMAND_BUFFER_COMPATIBLE XLA_FFI_Handler_TraitsBits.COMMAND_BUFFER_COMPATIBLE
) )
@@ -791,37 +877,45 @@ class FfiCallable:
assert num_inputs == self.num_inputs assert num_inputs == self.num_inputs
assert num_outputs == self.num_outputs assert num_outputs == self.num_outputs
if platform == "Host": if platform == _FFI_PLATFORM_CPU:
if self.graph_mode not in (JaxCallableGraphMode.NONE, JaxCallableGraphMode.JAX):
return create_invalid_argument_ffi_error(
call_frame.contents.api,
f"JaxCallableGraphMode.{self.graph_mode.name} is not supported for JAX CPU FFI calls",
)
if not wp.is_cpu_available():
return create_ffi_error(
call_frame.contents.api,
XLA_FFI_Error_Code.FAILED_PRECONDITION,
"This Warp build does not include CPU support",
)
device = wp.get_device("cpu") device = wp.get_device("cpu")
# reconstruct the argument list module = wp.get_module(self.func.__module__)
arg_list = [] _load_ffi_module(module, device)
arg_list = self._build_arg_list(inputs, outputs, call_desc, device)
# input and in-out args
for i, arg in enumerate(self.input_args):
if arg.is_array:
buffer = inputs[i].contents
shape = collapse_batch_dims(buffer.dims[: buffer.rank - arg.dtype_ndim], arg.type.ndim)
arr = wp.array(ptr=buffer.data, dtype=arg.type.dtype, shape=shape, device=device)
arg_list.append(arr)
else:
# scalar argument, get stashed value
value = call_desc.static_inputs[arg.name]
arg_list.append(value)
# pure output args (skip in-out FFI buffers)
for i, arg in enumerate(self.output_args):
buffer = outputs[i + self.num_in_out].contents
shape = collapse_batch_dims(buffer.dims[: buffer.rank - arg.dtype_ndim], arg.type.ndim)
arr = wp.array(ptr=buffer.data, dtype=arg.type.dtype, shape=shape, device=device)
arg_list.append(arr)
# call the Python function with reconstructed arguments
with wp.ScopedDevice(device): with wp.ScopedDevice(device):
self.func(*arg_list) self.func(*arg_list)
return return None
if platform != _FFI_PLATFORM_CUDA:
return create_invalid_argument_ffi_error(
call_frame.contents.api,
f"Unsupported JAX FFI platform '{platform}'",
)
if wp._src.context.runtime is None:
wp.init()
if not wp._src.context.runtime.is_cuda_enabled:
return create_ffi_error(
call_frame.contents.api,
XLA_FFI_Error_Code.FAILED_PRECONDITION,
"This Warp build does not include CUDA support",
)
cuda_stream = get_stream_from_callframe(call_frame.contents) cuda_stream = get_stream_from_callframe(call_frame.contents)
device_ordinal = get_device_ordinal_from_callframe(call_frame.contents) device_ordinal = get_device_ordinal_from_callframe(call_frame.contents)
device = wp.get_cuda_device(device_ordinal)
if self.graph_mode == JaxCallableGraphMode.WARP: if self.graph_mode == JaxCallableGraphMode.WARP:
# check if we already captured an identical call # check if we already captured an identical call
@@ -925,35 +1019,14 @@ class FfiCallable:
# early out # early out
return return
device_ordinal = get_device_ordinal_from_callframe(call_frame.contents) _load_ffi_module(wp.get_module(self.func.__module__), device)
device = wp.get_cuda_device(device_ordinal)
stream = wp.Stream(device, cuda_stream=cuda_stream) stream = wp.Stream(device, cuda_stream=cuda_stream)
# reconstruct the argument list arg_list = self._build_arg_list(inputs, outputs, call_desc, device)
arg_list = []
# input and in-out args
for i, arg in enumerate(self.input_args):
if arg.is_array:
buffer = inputs[i].contents
shape = collapse_batch_dims(buffer.dims[: buffer.rank - arg.dtype_ndim], arg.type.ndim)
arr = wp.array(ptr=buffer.data, dtype=arg.type.dtype, shape=shape, device=device)
arg_list.append(arr)
else:
# scalar argument, get stashed value
value = call_desc.static_inputs[arg.name]
arg_list.append(value)
# pure output args (skip in-out FFI buffers)
for i, arg in enumerate(self.output_args):
buffer = outputs[i + self.num_in_out].contents
shape = collapse_batch_dims(buffer.dims[: buffer.rank - arg.dtype_ndim], arg.type.ndim)
arr = wp.array(ptr=buffer.data, dtype=arg.type.dtype, shape=shape, device=device)
arg_list.append(arr)
# call the Python function with reconstructed arguments # call the Python function with reconstructed arguments
with wp.ScopedStream(stream, sync_enter=False) if stream else wp.ScopedDevice(device): with wp.ScopedStream(stream, sync_enter=False):
if stream and stream.is_capturing: if stream.is_capturing:
# capturing with JAX # capturing with JAX
with wp.ScopedCapture(external=True) as capture: with wp.ScopedCapture(external=True) as capture:
self.func(*arg_list) self.func(*arg_list)
@@ -961,7 +1034,7 @@ class FfiCallable:
# keep a reference to the capture object to prevent required modules getting unloaded # keep a reference to the capture object to prevent required modules getting unloaded
call_desc.capture = capture call_desc.capture = capture
elif self.graph_mode == JaxCallableGraphMode.WARP and device.is_cuda: elif self.graph_mode == JaxCallableGraphMode.WARP:
# capturing with WARP # capturing with WARP
with wp.ScopedCapture() as capture: with wp.ScopedCapture() as capture:
self.func(*arg_list) self.func(*arg_list)
@@ -974,7 +1047,7 @@ class FfiCallable:
if self._graph_cache_max is not None and len(self.captures) > self._graph_cache_max: if self._graph_cache_max is not None and len(self.captures) > self._graph_cache_max:
self.captures.popitem(last=False) self.captures.popitem(last=False)
elif self.graph_mode == JaxCallableGraphMode.WARP_STAGED_EX and device.is_cuda: elif self.graph_mode == JaxCallableGraphMode.WARP_STAGED_EX:
# capturing with WARP using staging buffers and memcopies done outside of the graph # capturing with WARP using staging buffers and memcopies done outside of the graph
wp_memcpy_batch = wp._src.context.runtime.core.wp_memcpy_batch wp_memcpy_batch = wp._src.context.runtime.core.wp_memcpy_batch
@@ -1017,7 +1090,7 @@ class FfiCallable:
# TODO: we should have a way of freeing this # TODO: we should have a way of freeing this
call_desc.capture = capture call_desc.capture = capture
elif self.graph_mode == JaxCallableGraphMode.WARP_STAGED and device.is_cuda: elif self.graph_mode == JaxCallableGraphMode.WARP_STAGED:
# capturing with WARP using staging buffers and memcopies done inside of the graph # capturing with WARP using staging buffers and memcopies done inside of the graph
wp_cuda_graph_insert_memcpy_batch = ( wp_cuda_graph_insert_memcpy_batch = (
wp._src.context.runtime.core.wp_cuda_graph_insert_memcpy_batch wp._src.context.runtime.core.wp_cuda_graph_insert_memcpy_batch
@@ -1224,6 +1297,7 @@ def jax_kernel(
module_preload_mode=JaxModulePreloadMode.CURRENT_DEVICE, module_preload_mode=JaxModulePreloadMode.CURRENT_DEVICE,
enable_backward: bool = False, enable_backward: bool = False,
has_side_effect: bool = False, has_side_effect: bool = False,
block_dim: int | None = None,
): ):
"""Create a JAX callback from a Warp kernel. """Create a JAX callback from a Warp kernel.
@@ -1250,22 +1324,43 @@ def jax_kernel(
kernel signature. The number of in-out arguments is included in ``num_outputs``. kernel signature. The number of in-out arguments is included in ``num_outputs``.
Not supported when ``enable_backward=True``. Not supported when ``enable_backward=True``.
module_preload_mode: Specify the devices where the module should be preloaded. module_preload_mode: Specify the devices where the module should be preloaded.
``JaxModulePreloadMode.ALL_DEVICES`` includes the host CPU and all
supported local CUDA devices. Preloading is best-effort: JAX devices
that cannot be mapped to Warp are skipped, and execution loads the
module for the callback's actual device. Module loading errors are
not suppressed.
enable_backward: Enable automatic differentiation for this kernel. enable_backward: Enable automatic differentiation for this kernel.
has_side_effect: Whether the custom call has side effects. When True, has_side_effect: Whether the custom call has side effects. When True,
the FFI call will be executed even when the outputs are not used. the FFI call will be executed even when the outputs are not used.
block_dim: Specify the number of threads per block for CUDA execution.
When ``None``, CUDA uses 256 threads per block. CPU execution always
uses one thread per block. The value is fixed when the wrapper is
constructed and is shared by forward and adjoint launches when
``enable_backward=True``.
Limitations: Limitations:
- All kernel arguments must be contiguous arrays or scalars. - All kernel arguments must be contiguous arrays or scalars.
- Scalars must be static arguments in JAX. - Scalars must be static arguments in JAX.
- Input and input-output arguments must precede the output arguments in the ``kernel`` definition. - Input and input-output arguments must precede the output arguments in the ``kernel`` definition.
- There must be at least one output or input-output argument. - There must be at least one output or input-output argument.
- Only the CUDA backend is supported. - The CPU and CUDA backends are supported. JAX selects the backend from the
lowered computation's device.
- ``output_dims`` and ``in_out_argnames`` are not supported when ``enable_backward=True``. - ``output_dims`` and ``in_out_argnames`` are not supported when ``enable_backward=True``.
""" """
check_jax_version() check_jax_version()
jax = _get_jax() jax = _get_jax()
if block_dim is not None:
if isinstance(block_dim, bool):
raise TypeError("jax_kernel(): block_dim must be an integer or None.")
try:
block_dim = operator.index(block_dim)
except TypeError:
raise TypeError("jax_kernel(): block_dim must be an integer or None.") from None
if block_dim <= 0:
raise ValueError("jax_kernel(): block_dim must be positive.")
if isinstance(output_dims, dict): if isinstance(output_dims, dict):
hashable_output_dims = tuple(sorted(output_dims.items())) hashable_output_dims = tuple(sorted(output_dims.items()))
elif hasattr(output_dims, "__len__"): elif hasattr(output_dims, "__len__"):
@@ -1288,6 +1383,7 @@ def jax_kernel(
hashable_output_dims, hashable_output_dims,
module_preload_mode, module_preload_mode,
has_side_effect, has_side_effect,
block_dim,
) )
with _FFI_REGISTRY_LOCK: with _FFI_REGISTRY_LOCK:
@@ -1297,6 +1393,7 @@ def jax_kernel(
num_outputs, num_outputs,
vmap_method, vmap_method,
launch_dims, launch_dims,
block_dim,
output_dims, output_dims,
in_out_argnames, in_out_argnames,
module_preload_mode, module_preload_mode,
@@ -1342,6 +1439,7 @@ def jax_kernel(
# Reuse `hashable_launch_dims` (computed above for the cache key path) # Reuse `hashable_launch_dims` (computed above for the cache key path)
# so 1-D integer and sequence forms are normalized identically. # so 1-D integer and sequence forms are normalized identically.
_user_launch_dims = hashable_launch_dims if launch_dims is not None else None _user_launch_dims = hashable_launch_dims if launch_dims is not None else None
_launch_block_dim = 256 if block_dim is None else block_dim
def _resolve_launch_dims(call_args): def _resolve_launch_dims(call_args):
if _user_launch_dims is not None: if _user_launch_dims is not None:
@@ -1362,7 +1460,13 @@ def jax_kernel(
# Forward kernel wrapper: simply launches the kernel # Forward kernel wrapper: simply launches the kernel
def fwd_kernel_wrapper(*args): def fwd_kernel_wrapper(*args):
wp.launch(kernel, dim=_resolve_launch_dims(args), inputs=args[:num_inputs], outputs=args[num_inputs:]) wp.launch(
kernel,
dim=_resolve_launch_dims(args),
inputs=args[:num_inputs],
outputs=args[num_inputs:],
block_dim=_launch_block_dim,
)
# update forward signature and annotations so jax_callable() sees a fully annotated function # update forward signature and annotations so jax_callable() sees a fully annotated function
fwd_kernel_wrapper.__signature__ = signature fwd_kernel_wrapper.__signature__ = signature
@@ -1415,6 +1519,7 @@ def jax_kernel(
adj_inputs=grad_in, adj_inputs=grad_in,
adj_outputs=grad_out, adj_outputs=grad_out,
adjoint=True, adjoint=True,
block_dim=_launch_block_dim,
) )
# Build the backward wrapper signature expected by jax_callable # Build the backward wrapper signature expected by jax_callable
@@ -1545,6 +1650,7 @@ def jax_kernel(
# Reusing _user_launch_dims ensures int and 1-tuple forms of the same # Reusing _user_launch_dims ensures int and 1-tuple forms of the same
# value map to the same key. # value map to the same key.
_user_launch_dims, _user_launch_dims,
block_dim,
) )
if static_args: if static_args:
@@ -1585,6 +1691,7 @@ def jax_kernel(
) )
return cached(*args) return cached(*args)
_checked_wrapper.block_dim = block_dim
return _checked_wrapper return _checked_wrapper
@@ -1617,6 +1724,9 @@ def jax_callable(
such as when the callable uses conditional graph nodes. such as when the callable uses conditional graph nodes.
``JaxCallableGraphMode.NONE``: Disable graph capture. Use when the callable performs operations that are not legal in a graph, ``JaxCallableGraphMode.NONE``: Disable graph capture. Use when the callable performs operations that are not legal in a graph,
such as host synchronization. such as host synchronization.
On CPU, ``JaxCallableGraphMode.NONE`` and ``JaxCallableGraphMode.JAX``
both execute without graph capture. The ``WARP``, ``WARP_STAGED``, and
``WARP_STAGED_EX`` modes require CUDA.
vmap_method: String specifying how the callback transforms under ``vmap()``. vmap_method: String specifying how the callback transforms under ``vmap()``.
This argument can also be specified for individual calls. This argument can also be specified for individual calls.
output_dims: Specify the default dimensions of output arrays. output_dims: Specify the default dimensions of output arrays.
@@ -1632,6 +1742,11 @@ def jax_callable(
graph_cache_max: Maximum number of cached graphs captured using ``JaxCallableGraphMode.WARP``. graph_cache_max: Maximum number of cached graphs captured using ``JaxCallableGraphMode.WARP``.
If ``None``, the graph cache is unlimited. If ``None``, the graph cache is unlimited.
module_preload_mode: Specify the devices where the module should be preloaded. module_preload_mode: Specify the devices where the module should be preloaded.
``JaxModulePreloadMode.ALL_DEVICES`` includes the host CPU and all
supported local CUDA devices. Preloading is best-effort: JAX devices
that cannot be mapped to Warp are skipped, and execution loads the
module for the callback's actual device. Module loading errors are
not suppressed.
has_side_effect: Whether the custom call has side effects. When True, has_side_effect: Whether the custom call has side effects. When True,
the FFI call will be executed even when the outputs are not used. the FFI call will be executed even when the outputs are not used.
@@ -1640,10 +1755,12 @@ def jax_callable(
- Scalars must be static arguments in JAX. - Scalars must be static arguments in JAX.
- Input and input-output arguments must precede the output arguments in the ``func`` definition. - Input and input-output arguments must precede the output arguments in the ``func`` definition.
- There must be at least one output or input-output argument. - There must be at least one output or input-output argument.
- Only the CUDA backend is supported. - The CPU and CUDA backends are supported. JAX selects the backend from the
lowered computation's device.
""" """
check_jax_version() check_jax_version()
graph_mode = JaxCallableGraphMode(graph_mode)
if isinstance(output_dims, dict): if isinstance(output_dims, dict):
hashable_output_dims = tuple(sorted(output_dims.items())) hashable_output_dims = tuple(sorted(output_dims.items()))
@@ -1739,7 +1856,7 @@ def register_ffi_callback(name: str, func: Callable, graph_compatible: bool = Tr
# TODO check that the name is not already registered # TODO check that the name is not already registered
def ffi_callback(call_frame, platform="CUDA"): def ffi_callback(call_frame, platform):
try: try:
extension = call_frame.contents.extension_start extension = call_frame.contents.extension_start
# On the first call, XLA runtime will query the API version and traits # On the first call, XLA runtime will query the API version and traits
@@ -1751,7 +1868,7 @@ def register_ffi_callback(name: str, func: Callable, graph_compatible: bool = Tr
metadata_ext = ctypes.cast(extension, ctypes.POINTER(XLA_FFI_Metadata_Extension)) metadata_ext = ctypes.cast(extension, ctypes.POINTER(XLA_FFI_Metadata_Extension))
metadata_ext.contents.metadata.contents.api_version.major_version = 0 metadata_ext.contents.metadata.contents.api_version.major_version = 0
metadata_ext.contents.metadata.contents.api_version.minor_version = 1 metadata_ext.contents.metadata.contents.api_version.minor_version = 1
if graph_compatible and platform == "CUDA": if graph_compatible and platform == _FFI_PLATFORM_CUDA:
# Turn on CUDA graphs for this handler. # Turn on CUDA graphs for this handler.
metadata_ext.contents.metadata.contents.traits = ( metadata_ext.contents.metadata.contents.traits = (
XLA_FFI_Handler_TraitsBits.COMMAND_BUFFER_COMPATIBLE XLA_FFI_Handler_TraitsBits.COMMAND_BUFFER_COMPATIBLE
@@ -1783,18 +1900,9 @@ def register_ffi_callback(name: str, func: Callable, graph_compatible: bool = Tr
return None return None
FFI_CCALLFUNC = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.POINTER(XLA_FFI_CallFrame)) callback_funcs = _register_ffi_targets(name, ffi_callback)
callback_func_cuda = FFI_CCALLFUNC(lambda call_frame: ffi_callback(call_frame, platform="CUDA"))
callback_func_host = FFI_CCALLFUNC(lambda call_frame: ffi_callback(call_frame, platform="Host"))
with _FFI_REGISTRY_LOCK: with _FFI_REGISTRY_LOCK:
_FFI_CALLBACK_REGISTRY[f"{name}_cuda"] = callback_func_cuda _FFI_CALLBACK_REGISTRY[name] = callback_funcs
_FFI_CALLBACK_REGISTRY[f"{name}_host"] = callback_func_host
ffi_ccall_address_cuda = ctypes.cast(callback_func_cuda, ctypes.c_void_p)
ffi_capsule_cuda = jax.ffi.pycapsule(ffi_ccall_address_cuda.value)
jax.ffi.register_ffi_target(name, ffi_capsule_cuda, platform="CUDA")
ffi_ccall_address_host = ctypes.cast(callback_func_host, ctypes.c_void_p)
ffi_capsule_host = jax.ffi.pycapsule(ffi_ccall_address_host.value)
jax.ffi.register_ffi_target(name, ffi_capsule_host, platform="Host")
############################################################################### ###############################################################################
-2
View File
@@ -9,8 +9,6 @@ import numpy as np
import warp as wp import warp as wp
_wp_module_name_ = "warp.jax.xla_ffi"
_xla_data_type_to_constructor = None _xla_data_type_to_constructor = None
_XLA_DATA_TYPE_TO_CONSTRUCTOR_LOCK = threading.Lock() _XLA_DATA_TYPE_TO_CONSTRUCTOR_LOCK = threading.Lock()
+1 -1
View File
@@ -37,7 +37,7 @@ dependencies = [
[project.optional-dependencies] [project.optional-dependencies]
warp = [ warp = [
"warp-lang==1.15.0", "warp-lang==1.16.0",
] ]
dev = [ dev = [
"isort", "isort",