Import NVIDIA/warp from GitHub.

PiperOrigin-RevId: 814423562
Change-Id: Ibf441f161e25c51c494de84b0d263b32af76545c
This commit is contained in:
Baruch Tabanpour
2025-10-02 16:07:03 -07:00
committed by Copybara-Service
parent 32e08f9507
commit 8eacacfd13
3 changed files with 150 additions and 33 deletions
@@ -19,6 +19,7 @@ import warp as wp
from warp.context import type_str from warp.context import type_str
from warp.jax import get_jax_device from warp.jax import get_jax_device
from warp.types import array_t, launch_bounds_t, strides_from_shape from warp.types import array_t, launch_bounds_t, strides_from_shape
from warp.utils import warn
_jax_warp_p = None _jax_warp_p = None
@@ -28,7 +29,7 @@ _registered_kernels = [None]
_registered_kernel_to_id = {} _registered_kernel_to_id = {}
def jax_kernel(kernel, launch_dims=None): def jax_kernel(kernel, launch_dims=None, quiet=False):
"""Create a Jax primitive from a Warp kernel. """Create a Jax primitive from a Warp kernel.
NOTE: This is an experimental feature under development. NOTE: This is an experimental feature under development.
@@ -38,6 +39,7 @@ def jax_kernel(kernel, launch_dims=None):
launch_dims: Optional. Specify the kernel launch dimensions. If None, launch_dims: Optional. Specify the kernel launch dimensions. If None,
dimensions are inferred from the shape of the first argument. dimensions are inferred from the shape of the first argument.
This option when set will specify the output dimensions. This option when set will specify the output dimensions.
quiet: Optional. If True, suppress deprecation warnings with newer JAX versions.
Limitations: Limitations:
- All kernel arguments must be contiguous arrays. - All kernel arguments must be contiguous arrays.
@@ -46,6 +48,27 @@ def jax_kernel(kernel, launch_dims=None):
- Only the CUDA backend is supported. - Only the CUDA backend is supported.
""" """
import jax
# check if JAX version supports this
if jax.__version_info__ < (0, 4, 25) or jax.__version_info__ >= (0, 8, 0):
msg = (
"This version of jax_kernel() requires JAX version 0.4.25 - 0.7.x, "
f"but installed JAX version is {jax.__version_info__}."
)
if jax.__version_info__ >= (0, 8, 0):
msg += " Please use warp.jax_experimental.ffi.jax_kernel instead."
raise RuntimeError(msg)
# deprecation warning
if jax.__version_info__ >= (0, 5, 0) and not quiet:
warn(
"This version of jax_kernel() is deprecated and will not be supported with newer JAX versions. "
"Please use the newer FFI version instead (warp.jax_experimental.ffi.jax_kernel). "
"In Warp release 1.10, the FFI version will become the default implementation of jax_kernel().",
DeprecationWarning,
)
if _jax_warp_p is None: if _jax_warp_p is None:
# Create and register the primitive # Create and register the primitive
_create_jax_warp_primitive() _create_jax_warp_primitive()
+110 -25
View File
@@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import collections
import ctypes import ctypes
import threading import threading
import traceback import traceback
@@ -28,6 +29,24 @@ from warp.types import array_t, launch_bounds_t, strides_from_shape, type_to_war
from .xla_ffi import * from .xla_ffi import *
jax_callable_default_graph_cache_max: int | None = 32
"""
Maximum size of the graph cache for graphs captured using ``GraphMode.WARP``, unlimited if ``None``.
Example usage: ``warp.jax_experimental.ffi.jax_callable_default_graph_cache_max = 42``.
"""
def check_jax_version():
# check if JAX version supports this
if jax.__version_info__ < (0, 5, 0):
msg = (
"This version of jax_kernel() requires JAX version 0.5.0 or higher, "
f"but installed JAX version is {jax.__version_info__}."
)
if jax.__version_info__ >= (0, 4, 25):
msg += " Please use warp.jax_experimental.custom_call.jax_kernel instead."
raise RuntimeError(msg)
class GraphMode(IntEnum): class GraphMode(IntEnum):
NONE = 0 # don't capture a graph NONE = 0 # don't capture a graph
@@ -338,11 +357,10 @@ class FfiKernel:
class FfiCallDesc: class FfiCallDesc:
def __init__(self, static_inputs): def __init__(self, static_inputs):
self.static_inputs = static_inputs self.static_inputs = static_inputs
self.captures = {}
class FfiCallable: class FfiCallable:
def __init__(self, func, num_outputs, graph_mode, vmap_method, output_dims, in_out_argnames): def __init__(self, func, num_outputs, graph_mode, vmap_method, output_dims, in_out_argnames, graph_cache_max):
self.func = func self.func = func
self.name = generate_unique_name(func) self.name = generate_unique_name(func)
self.num_outputs = num_outputs self.num_outputs = num_outputs
@@ -353,6 +371,10 @@ class FfiCallable:
self.call_id = 0 self.call_id = 0
self.call_descriptors = {} self.call_descriptors = {}
# LRU cache of graphs captured by Warp
self._graph_cache_max = graph_cache_max
self.captures = collections.OrderedDict()
in_out_argnames_list = in_out_argnames or [] in_out_argnames_list = in_out_argnames or []
in_out_argnames = set(in_out_argnames_list) in_out_argnames = set(in_out_argnames_list)
if len(in_out_argnames_list) != len(in_out_argnames): if len(in_out_argnames_list) != len(in_out_argnames):
@@ -559,8 +581,8 @@ class FfiCallable:
# check if we already captured an identical call # check if we already captured an identical call
ip = [inputs[i].contents.data for i in self.array_input_indices] ip = [inputs[i].contents.data for i in self.array_input_indices]
op = [outputs[i].contents.data for i in self.array_output_indices] op = [outputs[i].contents.data for i in self.array_output_indices]
buffer_hash = hash((*ip, *op)) capture_key = hash((call_id, *ip, *op))
capture = call_desc.captures.get(buffer_hash) capture = self.captures.get(capture_key)
# launch existing graph # launch existing graph
if capture is not None: if capture is not None:
@@ -578,6 +600,9 @@ class FfiCallable:
if not wp.context.runtime.core.wp_cuda_graph_launch(graph.graph_exec, cuda_stream): if not wp.context.runtime.core.wp_cuda_graph_launch(graph.graph_exec, cuda_stream):
raise RuntimeError(f"Graph launch error: {wp.context.runtime.get_error_string()}") raise RuntimeError(f"Graph launch error: {wp.context.runtime.get_error_string()}")
# update the graph cache to keep recently used graphs alive
self.captures.move_to_end(capture_key)
# early out # early out
return return
@@ -620,7 +645,10 @@ class FfiCallable:
self.func(*arg_list) self.func(*arg_list)
wp.capture_launch(capture.graph) wp.capture_launch(capture.graph)
# keep a reference to the capture object and reuse it with same buffers # keep a reference to the capture object and reuse it with same buffers
call_desc.captures[buffer_hash] = capture self.captures[capture_key] = capture
# respect the cache size limit if specified
if self._graph_cache_max is not None and len(self.captures) > self._graph_cache_max:
self.captures.popitem(last=False)
else: else:
# not capturing # not capturing
self.func(*arg_list) self.func(*arg_list)
@@ -633,10 +661,28 @@ class FfiCallable:
return None return None
@property
def graph_cache_max(self) -> int | None:
return self._graph_cache_max
@graph_cache_max.setter
def graph_cache_max(self, value: int | None):
if value != self._graph_cache_max:
if value is not None and (self._graph_cache_max is None or value < self._graph_cache_max):
# trim the cache if needed
while len(self.captures) > value:
self.captures.popitem(last=False)
self._graph_cache_max = value
@property
def graph_cache_size(self) -> int:
return len(self.captures)
# Holders for the custom callbacks to keep them alive. # Holders for the custom callbacks to keep them alive.
_FFI_CALLABLE_REGISTRY: dict[str, FfiCallable] = {}
_FFI_KERNEL_REGISTRY: dict[str, FfiKernel] = {} _FFI_KERNEL_REGISTRY: dict[str, FfiKernel] = {}
_FFI_CALLABLE_REGISTRY: dict[str, FfiCallable] = {}
_FFI_CALLBACK_REGISTRY: dict[str, ctypes.CFUNCTYPE] = {}
_FFI_REGISTRY_LOCK = threading.Lock() _FFI_REGISTRY_LOCK = threading.Lock()
@@ -649,17 +695,17 @@ def jax_kernel(
Args: Args:
kernel: The Warp kernel to launch. kernel: The Warp kernel to launch.
num_outputs: Optional. Specify the number of output arguments if greater than 1. num_outputs: Specify the number of output arguments if greater than 1.
This must include the number of ``in_out_arguments``. This must include the number of ``in_out_arguments``.
vmap_method: Optional. 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.
launch_dims: Optional. Specify the default kernel launch dimensions. If None, launch launch_dims: Specify the default kernel launch dimensions. If None, launch
dimensions are inferred from the shape of the first array argument. dimensions are inferred from the shape of the first array argument.
This argument can also be specified for individual calls. This argument can also be specified for individual calls.
output_dims: Optional. Specify the default dimensions of output arrays. If None, output output_dims: Specify the default dimensions of output arrays. If None, output
dimensions are inferred from the launch dimensions. dimensions are inferred from the launch dimensions.
This argument can also be specified for individual calls. This argument can also be specified for individual calls.
in_out_argnames: Optional. Names of input-output arguments. in_out_argnames: Names of input-output arguments.
Limitations: Limitations:
- All kernel arguments must be contiguous arrays or scalars. - All kernel arguments must be contiguous arrays or scalars.
@@ -668,8 +714,12 @@ def jax_kernel(
- 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. - Only the CUDA backend is supported.
""" """
check_jax_version()
key = ( key = (
kernel.func, kernel.func,
kernel.sig,
num_outputs, num_outputs,
vmap_method, vmap_method,
tuple(launch_dims) if launch_dims else launch_dims, tuple(launch_dims) if launch_dims else launch_dims,
@@ -692,6 +742,7 @@ def jax_callable(
vmap_method: Optional[str] = "broadcast_all", vmap_method: Optional[str] = "broadcast_all",
output_dims=None, output_dims=None,
in_out_argnames=None, in_out_argnames=None,
graph_cache_max: int | None = None,
): ):
"""Create a JAX callback from an annotated Python function. """Create a JAX callback from an annotated Python function.
@@ -701,22 +752,24 @@ def jax_callable(
Args: Args:
func: The Python function to call. func: The Python function to call.
num_outputs: Optional. Specify the number of output arguments if greater than 1. num_outputs: Specify the number of output arguments if greater than 1.
This must include the number of ``in_out_arguments``. This must include the number of ``in_out_arguments``.
graph_compatible: Optional. Whether the function can be called during CUDA graph capture. graph_compatible: Whether the function can be called during CUDA graph capture.
This argument is deprecated, use ``graph_mode`` instead. This argument is deprecated, use ``graph_mode`` instead.
graph_mode: Optional. CUDA graph capture mode. graph_mode: CUDA graph capture mode.
``GraphMode.JAX`` (default): Let JAX capture the graph, which may be used as a subgraph in an enclosing capture. ``GraphMode.JAX`` (default): Let JAX capture the graph, which may be used as a subgraph in an enclosing JAX capture.
``GraphMode.WARP``: Let Warp capture the graph. Use this mode when the callable cannot be used as a subraph, ``GraphMode.WARP``: Let Warp capture the graph. Use this mode when the callable cannot be used as a subgraph,
such as when the callable uses conditional graph nodes. such as when the callable uses conditional graph nodes.
``GraphMode.NONE``: Disable graph capture. Use when the callable performs operations that are not legal in a graph, ``GraphMode.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.
vmap_method: Optional. 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: Optional. Specify the default dimensions of output arrays. output_dims: Specify the default dimensions of output arrays.
If ``None``, output dimensions are inferred from the launch dimensions. If ``None``, output dimensions are inferred from the launch dimensions.
This argument can also be specified for individual calls. This argument can also be specified for individual calls.
in_out_argnames: Optional. Names of input-output arguments. in_out_argnames: Names of input-output arguments.
graph_cache_max: Maximum number of cached graphs captured using ``GraphMode.WARP``.
If ``None``, use ``warp.jax_experimental.ffi.jax_callable_default_graph_cache_max``.
Limitations: Limitations:
- All kernel arguments must be contiguous arrays or scalars. - All kernel arguments must be contiguous arrays or scalars.
@@ -726,6 +779,8 @@ def jax_callable(
- Only the CUDA backend is supported. - Only the CUDA backend is supported.
""" """
check_jax_version()
if graph_compatible is not None: if graph_compatible is not None:
wp.utils.warn( wp.utils.warn(
"The `graph_compatible` argument is deprecated, use `graph_mode` instead.", "The `graph_compatible` argument is deprecated, use `graph_mode` instead.",
@@ -735,6 +790,10 @@ def jax_callable(
if graph_compatible is False: if graph_compatible is False:
graph_mode = GraphMode.NONE graph_mode = GraphMode.NONE
if graph_cache_max is None:
graph_cache_max = jax_callable_default_graph_cache_max
# Note: we don't include graph_cache_max in the key, it is applied below.
key = ( key = (
func, func,
num_outputs, num_outputs,
@@ -744,11 +803,35 @@ def jax_callable(
) )
with _FFI_REGISTRY_LOCK: with _FFI_REGISTRY_LOCK:
if key not in _FFI_CALLABLE_REGISTRY: callable = _FFI_CALLABLE_REGISTRY.get(key)
new_callable = FfiCallable(func, num_outputs, graph_mode, vmap_method, output_dims, in_out_argnames) if callable is None:
_FFI_CALLABLE_REGISTRY[key] = new_callable callable = FfiCallable(
func,
num_outputs,
graph_mode,
vmap_method,
output_dims,
in_out_argnames,
graph_cache_max,
)
_FFI_CALLABLE_REGISTRY[key] = callable
else:
# make sure we're using the latest graph cache max
callable.graph_cache_max = graph_cache_max
return _FFI_CALLABLE_REGISTRY[key] return callable
def clear_jax_callable_graph_cache(callable: FfiCallable | None = None):
"""Clear the graph cache of the given callable or all callables if ``None``."""
if callable is not None:
callable.captures.clear()
else:
# apply to all callables
with _FFI_REGISTRY_LOCK:
for callable in _FFI_CALLABLE_REGISTRY.values():
callable.captures.clear()
############################################################################### ###############################################################################
@@ -769,9 +852,11 @@ def register_ffi_callback(name: str, func: Callable, graph_compatible: bool = Tr
Args: Args:
name: A unique FFI callback name. name: A unique FFI callback name.
func: The Python function to call. func: The Python function to call.
graph_compatible: Optional. Whether the function can be called during CUDA graph capture. graph_compatible: Whether the function can be called during CUDA graph capture.
""" """
check_jax_version()
# TODO check that the name is not already registered # TODO check that the name is not already registered
def ffi_callback(call_frame): def ffi_callback(call_frame):
@@ -817,7 +902,7 @@ def register_ffi_callback(name: str, func: Callable, graph_compatible: bool = Tr
FFI_CCALLFUNC = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.POINTER(XLA_FFI_CallFrame)) FFI_CCALLFUNC = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.POINTER(XLA_FFI_CallFrame))
callback_func = FFI_CCALLFUNC(ffi_callback) callback_func = FFI_CCALLFUNC(ffi_callback)
with _FFI_REGISTRY_LOCK: with _FFI_REGISTRY_LOCK:
_FFI_CALLABLE_REGISTRY[name] = callback_func _FFI_CALLBACK_REGISTRY[name] = callback_func
ffi_ccall_address = ctypes.cast(callback_func, ctypes.c_void_p) ffi_ccall_address = ctypes.cast(callback_func, ctypes.c_void_p)
ffi_capsule = jax.ffi.pycapsule(ffi_ccall_address.value) ffi_capsule = jax.ffi.pycapsule(ffi_ccall_address.value)
jax.ffi.register_ffi_target(name, ffi_capsule, platform="CUDA") jax.ffi.register_ffi_target(name, ffi_capsule, platform="CUDA")
+16 -7
View File
@@ -475,17 +475,26 @@ _xla_data_type_to_constructor = {
XLA_FFI_DataType.C64: jnp.complex64, XLA_FFI_DataType.C64: jnp.complex64,
XLA_FFI_DataType.C128: jnp.complex128, XLA_FFI_DataType.C128: jnp.complex128,
# XLA_FFI_DataType.TOKEN # XLA_FFI_DataType.TOKEN
XLA_FFI_DataType.F8E5M2: jnp.float8_e5m2,
XLA_FFI_DataType.F8E3M4: jnp.float8_e3m4,
XLA_FFI_DataType.F8E4M3: jnp.float8_e4m3,
XLA_FFI_DataType.F8E4M3FN: jnp.float8_e4m3fn,
XLA_FFI_DataType.F8E4M3B11FNUZ: jnp.float8_e4m3b11fnuz,
XLA_FFI_DataType.F8E5M2FNUZ: jnp.float8_e5m2fnuz,
XLA_FFI_DataType.F8E4M3FNUZ: jnp.float8_e4m3fnuz,
# XLA_FFI_DataType.F4E2M1FN: jnp.float4_e2m1fn.dtype, # XLA_FFI_DataType.F4E2M1FN: jnp.float4_e2m1fn.dtype,
# XLA_FFI_DataType.F8E8M0FNU: jnp.float8_e8m0fnu.dtype, # XLA_FFI_DataType.F8E8M0FNU: jnp.float8_e8m0fnu.dtype,
} }
# newer types not supported by older versions
if hasattr(jnp, "float8_e5m2"):
_xla_data_type_to_constructor[XLA_FFI_DataType.F8E5M2] = jnp.float8_e5m2
if hasattr(jnp, "float8_e3m4"):
_xla_data_type_to_constructor[XLA_FFI_DataType.F8E3M4] = jnp.float8_e3m4
if hasattr(jnp, "float8_e4m3"):
_xla_data_type_to_constructor[XLA_FFI_DataType.F8E4M3] = jnp.float8_e4m3
if hasattr(jnp, "float8_e4m3fn"):
_xla_data_type_to_constructor[XLA_FFI_DataType.F8E4M3FN] = jnp.float8_e4m3fn
if hasattr(jnp, "float8_e4m3b11fnuz"):
_xla_data_type_to_constructor[XLA_FFI_DataType.F8E4M3B11FNUZ] = jnp.float8_e4m3b11fnuz
if hasattr(jnp, "float8_e5m2fnuz"):
_xla_data_type_to_constructor[XLA_FFI_DataType.F8E5M2FNUZ] = jnp.float8_e5m2fnuz
if hasattr(jnp, "float8_e4m3fnuz"):
_xla_data_type_to_constructor[XLA_FFI_DataType.F8E4M3FNUZ] = jnp.float8_e4m3fnuz
######################################################################## ########################################################################
# Helpers for translating between ctypes and python types # Helpers for translating between ctypes and python types