Import NVIDIA/warp from GitHub.
PiperOrigin-RevId: 826571491 Change-Id: I294abc5aa3714345ab295636d12428293ea69285
This commit is contained in:
committed by
Copybara-Service
parent
4086261714
commit
fbb95d5e8c
@@ -13,4 +13,17 @@
|
|||||||
# 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.
|
||||||
|
|
||||||
from .custom_call import jax_kernel
|
# isort: skip_file
|
||||||
|
|
||||||
|
from warp._src.jax_experimental.ffi import GraphMode as GraphMode
|
||||||
|
from warp._src.jax_experimental.ffi import jax_kernel as jax_kernel
|
||||||
|
from warp._src.jax_experimental.ffi import jax_callable as jax_callable
|
||||||
|
from warp._src.jax_experimental.ffi import register_ffi_callback as register_ffi_callback
|
||||||
|
|
||||||
|
from warp._src.jax_experimental.ffi import (
|
||||||
|
get_jax_callable_default_graph_cache_max as get_jax_callable_default_graph_cache_max,
|
||||||
|
)
|
||||||
|
from warp._src.jax_experimental.ffi import (
|
||||||
|
set_jax_callable_default_graph_cache_max as set_jax_callable_default_graph_cache_max,
|
||||||
|
)
|
||||||
|
from warp._src.jax_experimental.ffi import clear_jax_callable_graph_cache as clear_jax_callable_graph_cache
|
||||||
|
|||||||
+8
-365
@@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
@@ -13,374 +13,17 @@
|
|||||||
# 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 ctypes
|
# isort: skip_file
|
||||||
|
|
||||||
import warp as wp
|
from warp._src.jax_experimental.custom_call import jax_kernel as jax_kernel
|
||||||
from warp.context import type_str
|
|
||||||
from warp.jax import get_jax_device
|
|
||||||
from warp.types import array_t, launch_bounds_t, strides_from_shape
|
|
||||||
from warp.utils import warn
|
|
||||||
|
|
||||||
_jax_warp_p = None
|
|
||||||
|
|
||||||
# Holder for the custom callback to keep it alive.
|
# TODO: Remove after cleaning up the public API.
|
||||||
_cc_callback = None
|
|
||||||
_registered_kernels = [None]
|
|
||||||
_registered_kernel_to_id = {}
|
|
||||||
|
|
||||||
|
from warp._src.jax_experimental import custom_call as _custom_call
|
||||||
|
|
||||||
def jax_kernel(kernel, launch_dims=None, quiet=False):
|
|
||||||
"""Create a Jax primitive from a Warp kernel.
|
|
||||||
|
|
||||||
NOTE: This is an experimental feature under development.
|
def __getattr__(name):
|
||||||
|
from warp._src.utils import get_deprecated_api
|
||||||
|
|
||||||
Args:
|
return get_deprecated_api(_custom_call, "wp.jax_experimental", name)
|
||||||
kernel: The Warp kernel to be wrapped.
|
|
||||||
launch_dims: Optional. Specify the kernel launch dimensions. If None,
|
|
||||||
dimensions are inferred from the shape of the first argument.
|
|
||||||
This option when set will specify the output dimensions.
|
|
||||||
quiet: Optional. If True, suppress deprecation warnings with newer JAX versions.
|
|
||||||
|
|
||||||
Limitations:
|
|
||||||
- All kernel arguments must be contiguous arrays.
|
|
||||||
- Input arguments are followed by output arguments in the Warp kernel definition.
|
|
||||||
- There must be at least one input argument and at least one output argument.
|
|
||||||
- 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:
|
|
||||||
# Create and register the primitive
|
|
||||||
_create_jax_warp_primitive()
|
|
||||||
if kernel not in _registered_kernel_to_id:
|
|
||||||
id = len(_registered_kernels)
|
|
||||||
_registered_kernels.append(kernel)
|
|
||||||
_registered_kernel_to_id[kernel] = id
|
|
||||||
else:
|
|
||||||
id = _registered_kernel_to_id[kernel]
|
|
||||||
|
|
||||||
def bind(*args):
|
|
||||||
return _jax_warp_p.bind(*args, kernel=id, launch_dims=launch_dims)
|
|
||||||
|
|
||||||
return bind
|
|
||||||
|
|
||||||
|
|
||||||
def _warp_custom_callback(stream, buffers, opaque, opaque_len):
|
|
||||||
# The descriptor is the form
|
|
||||||
# <kernel-id>|<launch-dims>|<arg-dims-list>
|
|
||||||
# Example: 42|16,32|16,32;100;16,32
|
|
||||||
kernel_id_str, dim_str, args_str = opaque.decode().split("|")
|
|
||||||
|
|
||||||
# Get the kernel from the registry.
|
|
||||||
kernel_id = int(kernel_id_str)
|
|
||||||
kernel = _registered_kernels[kernel_id]
|
|
||||||
|
|
||||||
# Parse launch dimensions.
|
|
||||||
dims = [int(d) for d in dim_str.split(",")]
|
|
||||||
bounds = launch_bounds_t(dims)
|
|
||||||
|
|
||||||
# Parse arguments.
|
|
||||||
arg_strings = args_str.split(";")
|
|
||||||
num_args = len(arg_strings)
|
|
||||||
assert num_args == len(kernel.adj.args), "Incorrect number of arguments"
|
|
||||||
|
|
||||||
# First param is the launch bounds.
|
|
||||||
kernel_params = (ctypes.c_void_p * (1 + num_args))()
|
|
||||||
kernel_params[0] = ctypes.addressof(bounds)
|
|
||||||
|
|
||||||
# Parse array descriptors.
|
|
||||||
args = []
|
|
||||||
for i in range(num_args):
|
|
||||||
dtype = kernel.adj.args[i].type.dtype
|
|
||||||
shape = [int(d) for d in arg_strings[i].split(",")]
|
|
||||||
strides = strides_from_shape(shape, dtype)
|
|
||||||
|
|
||||||
arr = array_t(buffers[i], 0, len(shape), shape, strides)
|
|
||||||
args.append(arr) # keep a reference
|
|
||||||
arg_ptr = ctypes.addressof(arr)
|
|
||||||
|
|
||||||
kernel_params[i + 1] = arg_ptr
|
|
||||||
|
|
||||||
# Get current device.
|
|
||||||
device = wp.device_from_jax(get_jax_device())
|
|
||||||
|
|
||||||
# Get kernel hooks.
|
|
||||||
# Note: module was loaded during jit lowering.
|
|
||||||
hooks = kernel.module.get_kernel_hooks(kernel, device)
|
|
||||||
assert hooks.forward, "Failed to find kernel entry point"
|
|
||||||
|
|
||||||
# Launch the kernel.
|
|
||||||
wp.context.runtime.core.wp_cuda_launch_kernel(
|
|
||||||
device.context, hooks.forward, bounds.size, 0, 256, hooks.forward_smem_bytes, kernel_params, stream
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _create_jax_warp_primitive():
|
|
||||||
from functools import reduce
|
|
||||||
|
|
||||||
import jax
|
|
||||||
from jax._src.interpreters import batching
|
|
||||||
from jax.interpreters import mlir
|
|
||||||
from jax.interpreters.mlir import ir
|
|
||||||
from jax.google.hlo_helpers import custom_call
|
|
||||||
|
|
||||||
global _jax_warp_p
|
|
||||||
global _cc_callback
|
|
||||||
|
|
||||||
# Create and register the primitive.
|
|
||||||
# TODO add default implementation that calls the kernel via warp.
|
|
||||||
try:
|
|
||||||
# newer JAX versions
|
|
||||||
import jax.extend
|
|
||||||
|
|
||||||
_jax_warp_p = jax.extend.core.Primitive("jax_warp")
|
|
||||||
except (ImportError, AttributeError):
|
|
||||||
# older JAX versions
|
|
||||||
_jax_warp_p = jax.core.Primitive("jax_warp")
|
|
||||||
_jax_warp_p.multiple_results = True
|
|
||||||
|
|
||||||
# TODO Just launch the kernel directly, but make sure the argument
|
|
||||||
# shapes are massaged the same way as below so that vmap works.
|
|
||||||
def impl(*args):
|
|
||||||
raise Exception("Not implemented")
|
|
||||||
|
|
||||||
_jax_warp_p.def_impl(impl)
|
|
||||||
|
|
||||||
# Auto-batching. Make sure all the arguments are fully broadcasted
|
|
||||||
# so that Warp is not confused about dimensions.
|
|
||||||
def vectorized_multi_batcher(args, dims, **params):
|
|
||||||
# Figure out the number of outputs.
|
|
||||||
wp_kernel = _registered_kernels[params["kernel"]]
|
|
||||||
output_count = len(wp_kernel.adj.args) - len(args)
|
|
||||||
shape, dim = next((a.shape, d) for a, d in zip(args, dims) if d is not None)
|
|
||||||
size = shape[dim]
|
|
||||||
args = [batching.bdim_at_front(a, d, size) if len(a.shape) else a for a, d in zip(args, dims)]
|
|
||||||
# Create the batched primitive.
|
|
||||||
return _jax_warp_p.bind(*args, **params), [dims[0]] * output_count
|
|
||||||
|
|
||||||
batching.primitive_batchers[_jax_warp_p] = vectorized_multi_batcher
|
|
||||||
|
|
||||||
def get_vecmat_shape(warp_type):
|
|
||||||
if hasattr(warp_type.dtype, "_shape_"):
|
|
||||||
return warp_type.dtype._shape_
|
|
||||||
return []
|
|
||||||
|
|
||||||
def strip_vecmat_dimensions(warp_arg, actual_shape):
|
|
||||||
shape = get_vecmat_shape(warp_arg.type)
|
|
||||||
for i, s in enumerate(reversed(shape)):
|
|
||||||
item = actual_shape[-i - 1]
|
|
||||||
if s != item:
|
|
||||||
raise Exception(f"The vector/matrix shape for argument {warp_arg.label} does not match")
|
|
||||||
return actual_shape[: len(actual_shape) - len(shape)]
|
|
||||||
|
|
||||||
def collapse_into_leading_dimension(warp_arg, actual_shape):
|
|
||||||
if len(actual_shape) < warp_arg.type.ndim:
|
|
||||||
raise Exception(f"Argument {warp_arg.label} has too few non-matrix/vector dimensions")
|
|
||||||
index_rest = len(actual_shape) - warp_arg.type.ndim + 1
|
|
||||||
leading_size = reduce(lambda x, y: x * y, actual_shape[:index_rest])
|
|
||||||
return [leading_size] + actual_shape[index_rest:]
|
|
||||||
|
|
||||||
# Infer array dimensions from input type.
|
|
||||||
def infer_dimensions(warp_arg, actual_shape):
|
|
||||||
actual_shape = strip_vecmat_dimensions(warp_arg, actual_shape)
|
|
||||||
return collapse_into_leading_dimension(warp_arg, actual_shape)
|
|
||||||
|
|
||||||
def base_type_to_jax(warp_dtype):
|
|
||||||
if hasattr(warp_dtype, "_wp_scalar_type_"):
|
|
||||||
return wp.dtype_to_jax(warp_dtype._wp_scalar_type_)
|
|
||||||
return wp.dtype_to_jax(warp_dtype)
|
|
||||||
|
|
||||||
def base_type_to_jax_ir(warp_dtype):
|
|
||||||
warp_to_jax_dict = {
|
|
||||||
wp.float16: ir.F16Type.get(),
|
|
||||||
wp.float32: ir.F32Type.get(),
|
|
||||||
wp.float64: ir.F64Type.get(),
|
|
||||||
wp.int8: ir.IntegerType.get_signless(8),
|
|
||||||
wp.int16: ir.IntegerType.get_signless(16),
|
|
||||||
wp.int32: ir.IntegerType.get_signless(32),
|
|
||||||
wp.int64: ir.IntegerType.get_signless(64),
|
|
||||||
wp.uint8: ir.IntegerType.get_unsigned(8),
|
|
||||||
wp.uint16: ir.IntegerType.get_unsigned(16),
|
|
||||||
wp.uint32: ir.IntegerType.get_unsigned(32),
|
|
||||||
wp.uint64: ir.IntegerType.get_unsigned(64),
|
|
||||||
}
|
|
||||||
if hasattr(warp_dtype, "_wp_scalar_type_"):
|
|
||||||
warp_dtype = warp_dtype._wp_scalar_type_
|
|
||||||
jax_dtype = warp_to_jax_dict.get(warp_dtype)
|
|
||||||
if jax_dtype is None:
|
|
||||||
raise TypeError(f"Invalid or unsupported data type: {warp_dtype}")
|
|
||||||
return jax_dtype
|
|
||||||
|
|
||||||
def base_type_is_compatible(warp_type, jax_ir_type):
|
|
||||||
jax_ir_to_warp = {
|
|
||||||
"f16": wp.float16,
|
|
||||||
"f32": wp.float32,
|
|
||||||
"f64": wp.float64,
|
|
||||||
"i8": wp.int8,
|
|
||||||
"i16": wp.int16,
|
|
||||||
"i32": wp.int32,
|
|
||||||
"i64": wp.int64,
|
|
||||||
"ui8": wp.uint8,
|
|
||||||
"ui16": wp.uint16,
|
|
||||||
"ui32": wp.uint32,
|
|
||||||
"ui64": wp.uint64,
|
|
||||||
}
|
|
||||||
expected_warp_type = jax_ir_to_warp.get(str(jax_ir_type))
|
|
||||||
if expected_warp_type is not None:
|
|
||||||
if hasattr(warp_type, "_wp_scalar_type_"):
|
|
||||||
return warp_type._wp_scalar_type_ == expected_warp_type
|
|
||||||
else:
|
|
||||||
return warp_type == expected_warp_type
|
|
||||||
else:
|
|
||||||
raise TypeError(f"Invalid or unsupported data type: {jax_ir_type}")
|
|
||||||
|
|
||||||
# Abstract evaluation.
|
|
||||||
def jax_warp_abstract(*args, kernel=None, launch_dims=None):
|
|
||||||
wp_kernel = _registered_kernels[kernel]
|
|
||||||
# All the extra arguments to the warp kernel are outputs.
|
|
||||||
warp_outputs = [o.type for o in wp_kernel.adj.args[len(args) :]]
|
|
||||||
|
|
||||||
if launch_dims is None:
|
|
||||||
# Use the first input dimension to infer the output's dimensions if launch_dims is not provided
|
|
||||||
dims = strip_vecmat_dimensions(wp_kernel.adj.args[0], list(args[0].shape))
|
|
||||||
else:
|
|
||||||
dims = launch_dims
|
|
||||||
|
|
||||||
jax_outputs = []
|
|
||||||
for o in warp_outputs:
|
|
||||||
shape = list(dims) + list(get_vecmat_shape(o))
|
|
||||||
dtype = base_type_to_jax(o.dtype)
|
|
||||||
jax_outputs.append(jax.core.ShapedArray(shape, dtype))
|
|
||||||
return jax_outputs
|
|
||||||
|
|
||||||
_jax_warp_p.def_abstract_eval(jax_warp_abstract)
|
|
||||||
|
|
||||||
# Lowering to MLIR.
|
|
||||||
|
|
||||||
# Create python-land custom call target.
|
|
||||||
CCALLFUNC = ctypes.CFUNCTYPE(
|
|
||||||
ctypes.c_voidp, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), ctypes.c_char_p, ctypes.c_size_t
|
|
||||||
)
|
|
||||||
_cc_callback = CCALLFUNC(_warp_custom_callback)
|
|
||||||
ccall_address = ctypes.cast(_cc_callback, ctypes.c_void_p)
|
|
||||||
|
|
||||||
# Put the custom call into a capsule, as required by XLA.
|
|
||||||
PyCapsule_Destructor = ctypes.CFUNCTYPE(None, ctypes.py_object)
|
|
||||||
PyCapsule_New = ctypes.pythonapi.PyCapsule_New
|
|
||||||
PyCapsule_New.restype = ctypes.py_object
|
|
||||||
PyCapsule_New.argtypes = (ctypes.c_void_p, ctypes.c_char_p, PyCapsule_Destructor)
|
|
||||||
capsule = PyCapsule_New(ccall_address.value, b"xla._CUSTOM_CALL_TARGET", PyCapsule_Destructor(0))
|
|
||||||
|
|
||||||
# Register the callback in XLA.
|
|
||||||
try:
|
|
||||||
# newer JAX versions
|
|
||||||
jax.ffi.register_ffi_target("warp_call", capsule, platform="gpu", api_version=0)
|
|
||||||
except AttributeError:
|
|
||||||
# older JAX versions
|
|
||||||
jax.lib.xla_client.register_custom_call_target("warp_call", capsule, platform="gpu")
|
|
||||||
|
|
||||||
def default_layout(shape):
|
|
||||||
return range(len(shape) - 1, -1, -1)
|
|
||||||
|
|
||||||
def warp_call_lowering(ctx, *args, kernel=None, launch_dims=None):
|
|
||||||
if not kernel:
|
|
||||||
raise Exception("Unknown kernel id " + str(kernel))
|
|
||||||
wp_kernel = _registered_kernels[kernel]
|
|
||||||
|
|
||||||
# TODO This may not be necessary, but it is perhaps better not to be
|
|
||||||
# mucking with kernel loading while already running the workload.
|
|
||||||
module = wp_kernel.module
|
|
||||||
device = wp.device_from_jax(get_jax_device())
|
|
||||||
if not module.load(device):
|
|
||||||
raise Exception("Could not load kernel on device")
|
|
||||||
|
|
||||||
if launch_dims is None:
|
|
||||||
# Infer dimensions from the first input.
|
|
||||||
warp_arg0 = wp_kernel.adj.args[0]
|
|
||||||
actual_shape0 = ir.RankedTensorType(args[0].type).shape
|
|
||||||
dims = strip_vecmat_dimensions(warp_arg0, actual_shape0)
|
|
||||||
warp_dims = collapse_into_leading_dimension(warp_arg0, dims)
|
|
||||||
else:
|
|
||||||
dims = launch_dims
|
|
||||||
warp_dims = launch_dims
|
|
||||||
# Figure out the types and shapes of the input arrays.
|
|
||||||
arg_strings = []
|
|
||||||
operand_layouts = []
|
|
||||||
for actual, warg in zip(args, wp_kernel.adj.args):
|
|
||||||
wtype = warg.type
|
|
||||||
rtt = ir.RankedTensorType(actual.type)
|
|
||||||
|
|
||||||
if not isinstance(wtype, wp.array):
|
|
||||||
raise Exception("Only contiguous arrays are supported for Jax kernel arguments")
|
|
||||||
|
|
||||||
if not base_type_is_compatible(wtype.dtype, rtt.element_type):
|
|
||||||
raise TypeError(
|
|
||||||
f"Incompatible data type for argument '{warg.label}', expected {type_str(wtype.dtype)}, got {rtt.element_type}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Infer array dimension (by removing the vector/matrix dimensions and
|
|
||||||
# collapsing the initial dimensions).
|
|
||||||
shape = infer_dimensions(warg, rtt.shape)
|
|
||||||
|
|
||||||
if len(shape) != wtype.ndim:
|
|
||||||
raise TypeError(f"Incompatible array dimensionality for argument '{warg.label}'")
|
|
||||||
|
|
||||||
arg_strings.append(",".join([str(d) for d in shape]))
|
|
||||||
operand_layouts.append(default_layout(rtt.shape))
|
|
||||||
|
|
||||||
# Figure out the types and shapes of the output arrays.
|
|
||||||
result_types = []
|
|
||||||
result_layouts = []
|
|
||||||
for warg in wp_kernel.adj.args[len(args) :]:
|
|
||||||
wtype = warg.type
|
|
||||||
|
|
||||||
if not isinstance(wtype, wp.array):
|
|
||||||
raise Exception("Only contiguous arrays are supported for Jax kernel arguments")
|
|
||||||
|
|
||||||
# Infer dimensions from the first input.
|
|
||||||
arg_strings.append(",".join([str(d) for d in warp_dims]))
|
|
||||||
|
|
||||||
result_shape = list(dims) + list(get_vecmat_shape(wtype))
|
|
||||||
result_types.append(ir.RankedTensorType.get(result_shape, base_type_to_jax_ir(wtype.dtype)))
|
|
||||||
result_layouts.append(default_layout(result_shape))
|
|
||||||
|
|
||||||
# Build opaque descriptor for callback.
|
|
||||||
shape_str = ",".join([str(d) for d in warp_dims])
|
|
||||||
args_str = ";".join(arg_strings)
|
|
||||||
descriptor = f"{kernel}|{shape_str}|{args_str}"
|
|
||||||
|
|
||||||
out = custom_call(
|
|
||||||
b"warp_call",
|
|
||||||
result_types=result_types,
|
|
||||||
operands=args,
|
|
||||||
backend_config=descriptor.encode("utf-8"),
|
|
||||||
operand_layouts=operand_layouts,
|
|
||||||
result_layouts=result_layouts,
|
|
||||||
).results
|
|
||||||
return out
|
|
||||||
|
|
||||||
mlir.register_lowering(
|
|
||||||
_jax_warp_p,
|
|
||||||
warp_call_lowering,
|
|
||||||
platform="gpu",
|
|
||||||
)
|
|
||||||
|
|||||||
+17
-938
@@ -13,948 +13,27 @@
|
|||||||
# 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
|
# isort: skip_file
|
||||||
import ctypes
|
|
||||||
import threading
|
|
||||||
import traceback
|
|
||||||
from enum import IntEnum
|
|
||||||
from typing import Callable, Optional
|
|
||||||
|
|
||||||
import jax
|
from warp._src.jax_experimental.ffi import GraphMode as GraphMode
|
||||||
|
from warp._src.jax_experimental.ffi import jax_kernel as jax_kernel
|
||||||
|
from warp._src.jax_experimental.ffi import jax_callable as jax_callable
|
||||||
|
from warp._src.jax_experimental.ffi import register_ffi_callback as register_ffi_callback
|
||||||
|
|
||||||
import warp as wp
|
from warp._src.jax_experimental.ffi import (
|
||||||
from warp.codegen import get_full_arg_spec, make_full_qualified_name
|
get_jax_callable_default_graph_cache_max as get_jax_callable_default_graph_cache_max,
|
||||||
from warp.jax import get_jax_device
|
)
|
||||||
from warp.types import array_t, launch_bounds_t, strides_from_shape, type_to_warp
|
from warp._src.jax_experimental.ffi import (
|
||||||
|
set_jax_callable_default_graph_cache_max as set_jax_callable_default_graph_cache_max,
|
||||||
|
)
|
||||||
|
from warp._src.jax_experimental.ffi import clear_jax_callable_graph_cache as clear_jax_callable_graph_cache
|
||||||
|
|
||||||
from .xla_ffi import *
|
# TODO: Remove after cleaning up the public API.
|
||||||
|
|
||||||
jax_callable_default_graph_cache_max: int | None = 32
|
from warp._src.jax_experimental import ffi as _ffi
|
||||||
"""
|
|
||||||
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():
|
def __getattr__(name):
|
||||||
# check if JAX version supports this
|
from warp._src.utils import get_deprecated_api
|
||||||
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)
|
|
||||||
|
|
||||||
|
return get_deprecated_api(_ffi, "wp.jax_experimental", name)
|
||||||
class GraphMode(IntEnum):
|
|
||||||
NONE = 0 # don't capture a graph
|
|
||||||
JAX = 1 # let JAX capture a graph
|
|
||||||
WARP = 2 # let Warp capture a graph
|
|
||||||
|
|
||||||
|
|
||||||
class FfiArg:
|
|
||||||
def __init__(self, name, type, in_out=False):
|
|
||||||
self.name = name
|
|
||||||
self.type = type
|
|
||||||
self.in_out = in_out
|
|
||||||
self.is_array = isinstance(type, wp.array)
|
|
||||||
|
|
||||||
if self.is_array:
|
|
||||||
if hasattr(type.dtype, "_wp_scalar_type_"):
|
|
||||||
self.dtype_shape = type.dtype._shape_
|
|
||||||
self.dtype_ndim = len(self.dtype_shape)
|
|
||||||
self.jax_scalar_type = wp.dtype_to_jax(type.dtype._wp_scalar_type_)
|
|
||||||
self.jax_ndim = type.ndim + self.dtype_ndim
|
|
||||||
elif type.dtype in wp.types.value_types:
|
|
||||||
self.dtype_ndim = 0
|
|
||||||
self.dtype_shape = ()
|
|
||||||
self.jax_scalar_type = wp.dtype_to_jax(type.dtype)
|
|
||||||
self.jax_ndim = type.ndim
|
|
||||||
else:
|
|
||||||
raise TypeError(f"Invalid data type for array argument '{name}', expected scalar, vector, or matrix")
|
|
||||||
self.warp_ndim = type.ndim
|
|
||||||
elif type in wp.types.value_types:
|
|
||||||
self.dtype_ndim = 0
|
|
||||||
self.dtype_shape = ()
|
|
||||||
self.jax_scalar_type = wp.dtype_to_jax(type_to_warp(type))
|
|
||||||
self.jax_ndim = 0
|
|
||||||
self.warp_ndim = 0
|
|
||||||
else:
|
|
||||||
raise TypeError(f"Invalid type for argument '{name}', expected array or scalar, got {type}")
|
|
||||||
|
|
||||||
|
|
||||||
class FfiLaunchDesc:
|
|
||||||
def __init__(self, static_inputs, launch_dims):
|
|
||||||
self.static_inputs = static_inputs
|
|
||||||
self.launch_dims = launch_dims
|
|
||||||
|
|
||||||
|
|
||||||
class FfiKernel:
|
|
||||||
def __init__(self, kernel, num_outputs, vmap_method, launch_dims, output_dims, in_out_argnames):
|
|
||||||
self.kernel = kernel
|
|
||||||
self.name = generate_unique_name(kernel.func)
|
|
||||||
self.num_outputs = num_outputs
|
|
||||||
self.vmap_method = vmap_method
|
|
||||||
self.launch_dims = launch_dims
|
|
||||||
self.output_dims = output_dims
|
|
||||||
self.first_array_arg = None
|
|
||||||
self.launch_id = 0
|
|
||||||
self.launch_descriptors = {}
|
|
||||||
|
|
||||||
in_out_argnames_list = in_out_argnames or []
|
|
||||||
in_out_argnames = set(in_out_argnames_list)
|
|
||||||
if len(in_out_argnames_list) != len(in_out_argnames):
|
|
||||||
raise AssertionError("in_out_argnames must not contain duplicate names")
|
|
||||||
|
|
||||||
self.num_kernel_args = len(kernel.adj.args)
|
|
||||||
self.num_in_out = len(in_out_argnames)
|
|
||||||
self.num_inputs = self.num_kernel_args - num_outputs + self.num_in_out
|
|
||||||
if self.num_outputs < 1:
|
|
||||||
raise ValueError("At least one output is required")
|
|
||||||
if self.num_outputs > self.num_kernel_args:
|
|
||||||
raise ValueError("Number of outputs cannot be greater than the number of kernel arguments")
|
|
||||||
if self.num_outputs < self.num_in_out:
|
|
||||||
raise ValueError("Number of outputs cannot be smaller than the number of in_out_argnames")
|
|
||||||
|
|
||||||
# process input args
|
|
||||||
self.input_args = []
|
|
||||||
for i in range(self.num_inputs):
|
|
||||||
arg_name = kernel.adj.args[i].label
|
|
||||||
arg = FfiArg(arg_name, kernel.adj.args[i].type, arg_name in in_out_argnames)
|
|
||||||
if arg_name in in_out_argnames:
|
|
||||||
in_out_argnames.remove(arg_name)
|
|
||||||
if arg.is_array:
|
|
||||||
# keep track of the first input array argument
|
|
||||||
if self.first_array_arg is None:
|
|
||||||
self.first_array_arg = i
|
|
||||||
self.input_args.append(arg)
|
|
||||||
|
|
||||||
# process output args
|
|
||||||
self.output_args = []
|
|
||||||
for i in range(self.num_inputs, self.num_kernel_args):
|
|
||||||
arg_name = kernel.adj.args[i].label
|
|
||||||
if arg_name in in_out_argnames:
|
|
||||||
raise AssertionError(
|
|
||||||
f"Expected an output-only argument for argument {arg_name}."
|
|
||||||
" in_out arguments should be placed before output-only arguments."
|
|
||||||
)
|
|
||||||
arg = FfiArg(arg_name, kernel.adj.args[i].type, False)
|
|
||||||
if not arg.is_array:
|
|
||||||
raise TypeError("All output arguments must be arrays")
|
|
||||||
self.output_args.append(arg)
|
|
||||||
|
|
||||||
if in_out_argnames:
|
|
||||||
raise ValueError(f"in_out_argnames: '{in_out_argnames}' did not match any function argument names.")
|
|
||||||
|
|
||||||
# Build input output aliases.
|
|
||||||
out_id = 0
|
|
||||||
input_output_aliases = {}
|
|
||||||
for in_id, arg in enumerate(self.input_args):
|
|
||||||
if not arg.in_out:
|
|
||||||
continue
|
|
||||||
input_output_aliases[in_id] = out_id
|
|
||||||
out_id += 1
|
|
||||||
self.input_output_aliases = input_output_aliases
|
|
||||||
|
|
||||||
# register the callback
|
|
||||||
FFI_CCALLFUNC = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.POINTER(XLA_FFI_CallFrame))
|
|
||||||
self.callback_func = FFI_CCALLFUNC(lambda call_frame: self.ffi_callback(call_frame))
|
|
||||||
ffi_ccall_address = ctypes.cast(self.callback_func, ctypes.c_void_p)
|
|
||||||
ffi_capsule = jax.ffi.pycapsule(ffi_ccall_address.value)
|
|
||||||
jax.ffi.register_ffi_target(self.name, ffi_capsule, platform="CUDA")
|
|
||||||
|
|
||||||
def __call__(self, *args, output_dims=None, launch_dims=None, vmap_method=None):
|
|
||||||
num_inputs = len(args)
|
|
||||||
if num_inputs != self.num_inputs:
|
|
||||||
raise ValueError(f"Expected {self.num_inputs} inputs, but got {num_inputs}")
|
|
||||||
|
|
||||||
# default argument fallback
|
|
||||||
if launch_dims is None:
|
|
||||||
launch_dims = self.launch_dims
|
|
||||||
if output_dims is None:
|
|
||||||
output_dims = self.output_dims
|
|
||||||
if vmap_method is None:
|
|
||||||
vmap_method = self.vmap_method
|
|
||||||
|
|
||||||
# output types
|
|
||||||
out_types = []
|
|
||||||
|
|
||||||
# process inputs
|
|
||||||
static_inputs = {}
|
|
||||||
for i in range(num_inputs):
|
|
||||||
input_arg = self.input_args[i]
|
|
||||||
input_value = args[i]
|
|
||||||
if input_arg.is_array:
|
|
||||||
# check dtype
|
|
||||||
if input_value.dtype != input_arg.jax_scalar_type:
|
|
||||||
raise TypeError(
|
|
||||||
f"Invalid data type for array argument '{input_arg.name}', expected {input_arg.jax_scalar_type}, got {input_value.dtype}"
|
|
||||||
)
|
|
||||||
# check ndim
|
|
||||||
if input_value.ndim != input_arg.jax_ndim:
|
|
||||||
raise TypeError(
|
|
||||||
f"Invalid dimensionality for array argument '{input_arg.name}', expected {input_arg.jax_ndim} dimensions, got {input_value.ndim}"
|
|
||||||
)
|
|
||||||
# check inner dims
|
|
||||||
for d in range(input_arg.dtype_ndim):
|
|
||||||
if input_value.shape[input_arg.type.ndim + d] != input_arg.dtype_shape[d]:
|
|
||||||
raise TypeError(
|
|
||||||
f"Invalid inner dimensions for array argument '{input_arg.name}', expected {input_arg.dtype_shape}, got {input_value.shape[-input_arg.dtype_ndim :]}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# make sure scalar is not a traced variable, should be static
|
|
||||||
if isinstance(input_value, jax.core.Tracer):
|
|
||||||
raise ValueError(f"Argument '{input_arg.name}' must be a static value")
|
|
||||||
# stash the value to be retrieved by callback
|
|
||||||
static_inputs[input_arg.name] = input_arg.type(input_value)
|
|
||||||
|
|
||||||
# append in-out arg to output types
|
|
||||||
if input_arg.in_out:
|
|
||||||
out_types.append(get_jax_output_type(input_arg, input_value.shape))
|
|
||||||
|
|
||||||
# launch dimensions
|
|
||||||
if launch_dims is None:
|
|
||||||
# use the shape of the first input array
|
|
||||||
if self.first_array_arg is not None:
|
|
||||||
launch_dims = get_warp_shape(self.input_args[self.first_array_arg], args[self.first_array_arg].shape)
|
|
||||||
else:
|
|
||||||
raise RuntimeError("Failed to determine launch dimensions")
|
|
||||||
elif isinstance(launch_dims, int):
|
|
||||||
launch_dims = (launch_dims,)
|
|
||||||
else:
|
|
||||||
launch_dims = tuple(launch_dims)
|
|
||||||
|
|
||||||
# output shapes
|
|
||||||
if isinstance(output_dims, dict):
|
|
||||||
# assume a dictionary of shapes keyed on argument name
|
|
||||||
for output_arg in self.output_args:
|
|
||||||
dims = output_dims.get(output_arg.name)
|
|
||||||
if dims is None:
|
|
||||||
raise ValueError(f"Missing output dimensions for argument '{output_arg.name}'")
|
|
||||||
out_types.append(get_jax_output_type(output_arg, dims))
|
|
||||||
else:
|
|
||||||
if output_dims is None:
|
|
||||||
# use launch dimensions
|
|
||||||
output_dims = launch_dims
|
|
||||||
elif isinstance(output_dims, int):
|
|
||||||
output_dims = (output_dims,)
|
|
||||||
# assume same dimensions for all outputs
|
|
||||||
for output_arg in self.output_args:
|
|
||||||
out_types.append(get_jax_output_type(output_arg, output_dims))
|
|
||||||
|
|
||||||
call = jax.ffi.ffi_call(
|
|
||||||
self.name,
|
|
||||||
out_types,
|
|
||||||
vmap_method=vmap_method,
|
|
||||||
input_output_aliases=self.input_output_aliases,
|
|
||||||
)
|
|
||||||
|
|
||||||
# ensure the kernel module is loaded before the callback, otherwise graph capture may fail
|
|
||||||
device = wp.device_from_jax(get_jax_device())
|
|
||||||
self.kernel.module.load(device)
|
|
||||||
|
|
||||||
# save launch data to be retrieved by callback
|
|
||||||
launch_id = self.launch_id
|
|
||||||
self.launch_descriptors[launch_id] = FfiLaunchDesc(static_inputs, launch_dims)
|
|
||||||
self.launch_id += 1
|
|
||||||
|
|
||||||
return call(*args, launch_id=launch_id)
|
|
||||||
|
|
||||||
def ffi_callback(self, call_frame):
|
|
||||||
try:
|
|
||||||
# On the first call, XLA runtime will query the API version and traits
|
|
||||||
# metadata using the |extension| field. Let us respond to that query
|
|
||||||
# if the metadata extension is present.
|
|
||||||
extension = call_frame.contents.extension_start
|
|
||||||
if extension:
|
|
||||||
# Try to set the version metadata.
|
|
||||||
if extension.contents.type == XLA_FFI_Extension_Type.Metadata:
|
|
||||||
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.minor_version = 1
|
|
||||||
# Turn on CUDA graphs for this handler.
|
|
||||||
metadata_ext.contents.metadata.contents.traits = (
|
|
||||||
XLA_FFI_Handler_TraitsBits.COMMAND_BUFFER_COMPATIBLE
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
# retrieve call info
|
|
||||||
attrs = decode_attrs(call_frame.contents.attrs)
|
|
||||||
launch_id = int(attrs["launch_id"])
|
|
||||||
launch_desc = self.launch_descriptors[launch_id]
|
|
||||||
|
|
||||||
num_inputs = call_frame.contents.args.size
|
|
||||||
inputs = ctypes.cast(call_frame.contents.args.args, ctypes.POINTER(ctypes.POINTER(XLA_FFI_Buffer)))
|
|
||||||
|
|
||||||
num_outputs = call_frame.contents.rets.size
|
|
||||||
outputs = ctypes.cast(call_frame.contents.rets.rets, ctypes.POINTER(ctypes.POINTER(XLA_FFI_Buffer)))
|
|
||||||
|
|
||||||
assert num_inputs == self.num_inputs
|
|
||||||
assert num_outputs == self.num_outputs
|
|
||||||
|
|
||||||
launch_bounds = launch_bounds_t(launch_desc.launch_dims)
|
|
||||||
|
|
||||||
# first kernel param is the launch bounds
|
|
||||||
kernel_params = (ctypes.c_void_p * (1 + self.num_kernel_args))()
|
|
||||||
kernel_params[0] = ctypes.addressof(launch_bounds)
|
|
||||||
|
|
||||||
arg_refs = []
|
|
||||||
|
|
||||||
# input and in-out args
|
|
||||||
for i, input_arg in enumerate(self.input_args):
|
|
||||||
if input_arg.is_array:
|
|
||||||
buffer = inputs[i].contents
|
|
||||||
shape = buffer.dims[: input_arg.type.ndim]
|
|
||||||
strides = strides_from_shape(shape, input_arg.type.dtype)
|
|
||||||
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
|
|
||||||
else:
|
|
||||||
# scalar argument, get stashed value
|
|
||||||
value = launch_desc.static_inputs[input_arg.name]
|
|
||||||
arg = input_arg.type._type_(value)
|
|
||||||
kernel_params[i + 1] = ctypes.addressof(arg)
|
|
||||||
arg_refs.append(arg) # keep a reference
|
|
||||||
|
|
||||||
# pure output args (skip in-out FFI buffers)
|
|
||||||
for i, output_arg in enumerate(self.output_args):
|
|
||||||
buffer = outputs[i + self.num_in_out].contents
|
|
||||||
shape = buffer.dims[: output_arg.type.ndim]
|
|
||||||
strides = strides_from_shape(shape, output_arg.type.dtype)
|
|
||||||
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
|
|
||||||
|
|
||||||
# get device and stream
|
|
||||||
device = wp.device_from_jax(get_jax_device())
|
|
||||||
stream = get_stream_from_callframe(call_frame.contents)
|
|
||||||
|
|
||||||
# get kernel hooks
|
|
||||||
hooks = self.kernel.module.get_kernel_hooks(self.kernel, device)
|
|
||||||
assert hooks.forward, "Failed to find kernel entry point"
|
|
||||||
|
|
||||||
# launch the kernel
|
|
||||||
wp.context.runtime.core.wp_cuda_launch_kernel(
|
|
||||||
device.context,
|
|
||||||
hooks.forward,
|
|
||||||
launch_bounds.size,
|
|
||||||
0,
|
|
||||||
256,
|
|
||||||
hooks.forward_smem_bytes,
|
|
||||||
kernel_params,
|
|
||||||
stream,
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(traceback.format_exc())
|
|
||||||
return create_ffi_error(
|
|
||||||
call_frame.contents.api, XLA_FFI_Error_Code.UNKNOWN, f"FFI callback error: {type(e).__name__}: {e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class FfiCallDesc:
|
|
||||||
def __init__(self, static_inputs):
|
|
||||||
self.static_inputs = static_inputs
|
|
||||||
|
|
||||||
|
|
||||||
class FfiCallable:
|
|
||||||
def __init__(self, func, num_outputs, graph_mode, vmap_method, output_dims, in_out_argnames, graph_cache_max):
|
|
||||||
self.func = func
|
|
||||||
self.name = generate_unique_name(func)
|
|
||||||
self.num_outputs = num_outputs
|
|
||||||
self.vmap_method = vmap_method
|
|
||||||
self.graph_mode = graph_mode
|
|
||||||
self.output_dims = output_dims
|
|
||||||
self.first_array_arg = None
|
|
||||||
self.call_id = 0
|
|
||||||
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 = set(in_out_argnames_list)
|
|
||||||
if len(in_out_argnames_list) != len(in_out_argnames):
|
|
||||||
raise AssertionError("in_out_argnames must not contain duplicate names")
|
|
||||||
|
|
||||||
# get arguments and annotations
|
|
||||||
argspec = get_full_arg_spec(func)
|
|
||||||
|
|
||||||
num_args = len(argspec.args)
|
|
||||||
self.num_in_out = len(in_out_argnames)
|
|
||||||
self.num_inputs = num_args - num_outputs + self.num_in_out
|
|
||||||
if self.num_outputs < 1:
|
|
||||||
raise ValueError("At least one output is required")
|
|
||||||
if self.num_outputs > num_args:
|
|
||||||
raise ValueError("Number of outputs cannot be greater than the number of kernel arguments")
|
|
||||||
if self.num_outputs < self.num_in_out:
|
|
||||||
raise ValueError("Number of outputs cannot be smaller than the number of in_out_argnames")
|
|
||||||
|
|
||||||
if len(argspec.annotations) < num_args:
|
|
||||||
raise RuntimeError(f"Incomplete argument annotations on function {self.name}")
|
|
||||||
|
|
||||||
# parse type annotations
|
|
||||||
self.args = []
|
|
||||||
arg_idx = 0
|
|
||||||
for arg_name, arg_type in argspec.annotations.items():
|
|
||||||
if arg_name == "return":
|
|
||||||
if arg_type is not None:
|
|
||||||
raise TypeError("Function must not return a value")
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
arg = FfiArg(arg_name, arg_type, arg_name in in_out_argnames)
|
|
||||||
if arg_name in in_out_argnames:
|
|
||||||
in_out_argnames.remove(arg_name)
|
|
||||||
if arg.is_array:
|
|
||||||
if arg_idx < self.num_inputs and self.first_array_arg is None:
|
|
||||||
self.first_array_arg = arg_idx
|
|
||||||
self.args.append(arg)
|
|
||||||
|
|
||||||
if arg.in_out and arg_idx >= self.num_inputs:
|
|
||||||
raise AssertionError(
|
|
||||||
f"Expected an output-only argument for argument {arg_name}."
|
|
||||||
" in_out arguments should be placed before output-only arguments."
|
|
||||||
)
|
|
||||||
|
|
||||||
arg_idx += 1
|
|
||||||
|
|
||||||
if in_out_argnames:
|
|
||||||
raise ValueError(f"in_out_argnames: '{in_out_argnames}' did not match any function argument names.")
|
|
||||||
|
|
||||||
self.input_args = self.args[: self.num_inputs] # includes in-out args
|
|
||||||
self.output_args = self.args[self.num_inputs :] # pure output args
|
|
||||||
|
|
||||||
# Buffer indices for array arguments in callback.
|
|
||||||
# In-out buffers are the same pointers in the XLA call frame,
|
|
||||||
# so we only include them for inputs and skip them for outputs.
|
|
||||||
self.array_input_indices = [i for i, arg in enumerate(self.input_args) if arg.is_array]
|
|
||||||
self.array_output_indices = list(range(self.num_in_out, self.num_outputs))
|
|
||||||
|
|
||||||
# Build input output aliases.
|
|
||||||
out_id = 0
|
|
||||||
input_output_aliases = {}
|
|
||||||
for in_id, arg in enumerate(self.input_args):
|
|
||||||
if not arg.in_out:
|
|
||||||
continue
|
|
||||||
input_output_aliases[in_id] = out_id
|
|
||||||
out_id += 1
|
|
||||||
self.input_output_aliases = input_output_aliases
|
|
||||||
|
|
||||||
# register the callback
|
|
||||||
FFI_CCALLFUNC = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.POINTER(XLA_FFI_CallFrame))
|
|
||||||
self.callback_func = FFI_CCALLFUNC(lambda call_frame: self.ffi_callback(call_frame))
|
|
||||||
ffi_ccall_address = ctypes.cast(self.callback_func, ctypes.c_void_p)
|
|
||||||
ffi_capsule = jax.ffi.pycapsule(ffi_ccall_address.value)
|
|
||||||
jax.ffi.register_ffi_target(self.name, ffi_capsule, platform="CUDA")
|
|
||||||
|
|
||||||
def __call__(self, *args, output_dims=None, vmap_method=None):
|
|
||||||
num_inputs = len(args)
|
|
||||||
if num_inputs != self.num_inputs:
|
|
||||||
input_names = ", ".join(arg.name for arg in self.input_args)
|
|
||||||
s = "" if self.num_inputs == 1 else "s"
|
|
||||||
raise ValueError(f"Expected {self.num_inputs} input{s} ({input_names}), but got {num_inputs}")
|
|
||||||
|
|
||||||
# default argument fallback
|
|
||||||
if vmap_method is None:
|
|
||||||
vmap_method = self.vmap_method
|
|
||||||
if output_dims is None:
|
|
||||||
output_dims = self.output_dims
|
|
||||||
|
|
||||||
# output types
|
|
||||||
out_types = []
|
|
||||||
|
|
||||||
# process inputs
|
|
||||||
static_inputs = {}
|
|
||||||
for i in range(num_inputs):
|
|
||||||
input_arg = self.input_args[i]
|
|
||||||
input_value = args[i]
|
|
||||||
if input_arg.is_array:
|
|
||||||
# check dtype
|
|
||||||
if input_value.dtype != input_arg.jax_scalar_type:
|
|
||||||
raise TypeError(
|
|
||||||
f"Invalid data type for array argument '{input_arg.name}', expected {input_arg.jax_scalar_type}, got {input_value.dtype}"
|
|
||||||
)
|
|
||||||
# check ndim
|
|
||||||
if input_value.ndim != input_arg.jax_ndim:
|
|
||||||
raise TypeError(
|
|
||||||
f"Invalid dimensionality for array argument '{input_arg.name}', expected {input_arg.jax_ndim} dimensions, got {input_value.ndim}"
|
|
||||||
)
|
|
||||||
# check inner dims
|
|
||||||
for d in range(input_arg.dtype_ndim):
|
|
||||||
if input_value.shape[input_arg.type.ndim + d] != input_arg.dtype_shape[d]:
|
|
||||||
raise TypeError(
|
|
||||||
f"Invalid inner dimensions for array argument '{input_arg.name}', expected {input_arg.dtype_shape}, got {input_value.shape[-input_arg.dtype_ndim :]}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# make sure scalar is not a traced variable, should be static
|
|
||||||
if isinstance(input_value, jax.core.Tracer):
|
|
||||||
raise ValueError(f"Argument '{input_arg.name}' must be a static value")
|
|
||||||
# stash the value to be retrieved by callback
|
|
||||||
static_inputs[input_arg.name] = input_arg.type(input_value)
|
|
||||||
|
|
||||||
# append in-out arg to output types
|
|
||||||
if input_arg.in_out:
|
|
||||||
out_types.append(get_jax_output_type(input_arg, input_value.shape))
|
|
||||||
|
|
||||||
# output shapes
|
|
||||||
if isinstance(output_dims, dict):
|
|
||||||
# assume a dictionary of shapes keyed on argument name
|
|
||||||
for output_arg in self.output_args:
|
|
||||||
dims = output_dims.get(output_arg.name)
|
|
||||||
if dims is None:
|
|
||||||
raise ValueError(f"Missing output dimensions for argument '{output_arg.name}'")
|
|
||||||
out_types.append(get_jax_output_type(output_arg, dims))
|
|
||||||
else:
|
|
||||||
if output_dims is None:
|
|
||||||
if self.first_array_arg is None:
|
|
||||||
raise ValueError("Unable to determine output dimensions")
|
|
||||||
output_dims = get_warp_shape(self.input_args[self.first_array_arg], args[self.first_array_arg].shape)
|
|
||||||
elif isinstance(output_dims, int):
|
|
||||||
output_dims = (output_dims,)
|
|
||||||
# assume same dimensions for all outputs
|
|
||||||
for output_arg in self.output_args:
|
|
||||||
out_types.append(get_jax_output_type(output_arg, output_dims))
|
|
||||||
|
|
||||||
call = jax.ffi.ffi_call(
|
|
||||||
self.name,
|
|
||||||
out_types,
|
|
||||||
vmap_method=vmap_method,
|
|
||||||
input_output_aliases=self.input_output_aliases,
|
|
||||||
# has_side_effect=True, # force this function to execute even if outputs aren't used
|
|
||||||
)
|
|
||||||
|
|
||||||
# load the module
|
|
||||||
# NOTE: if the target function uses kernels from different modules, they will not be loaded here
|
|
||||||
device = wp.device_from_jax(get_jax_device())
|
|
||||||
module = wp.get_module(self.func.__module__)
|
|
||||||
module.load(device)
|
|
||||||
|
|
||||||
# save call data to be retrieved by callback
|
|
||||||
call_id = self.call_id
|
|
||||||
self.call_descriptors[call_id] = FfiCallDesc(static_inputs)
|
|
||||||
self.call_id += 1
|
|
||||||
return call(*args, call_id=call_id)
|
|
||||||
|
|
||||||
def ffi_callback(self, call_frame):
|
|
||||||
try:
|
|
||||||
# On the first call, XLA runtime will query the API version and traits
|
|
||||||
# metadata using the |extension| field. Let us respond to that query
|
|
||||||
# if the metadata extension is present.
|
|
||||||
extension = call_frame.contents.extension_start
|
|
||||||
if extension:
|
|
||||||
# Try to set the version metadata.
|
|
||||||
if extension.contents.type == XLA_FFI_Extension_Type.Metadata:
|
|
||||||
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.minor_version = 1
|
|
||||||
# Turn on CUDA graphs for this handler.
|
|
||||||
if self.graph_mode is GraphMode.JAX:
|
|
||||||
metadata_ext.contents.metadata.contents.traits = (
|
|
||||||
XLA_FFI_Handler_TraitsBits.COMMAND_BUFFER_COMPATIBLE
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
# retrieve call info
|
|
||||||
# NOTE: this assumes that there's only one attribute - call_id (int64).
|
|
||||||
# A more general but slower approach is this:
|
|
||||||
# attrs = decode_attrs(call_frame.contents.attrs)
|
|
||||||
# call_id = int(attrs["call_id"])
|
|
||||||
attr = ctypes.cast(call_frame.contents.attrs.attrs[0], ctypes.POINTER(XLA_FFI_Scalar)).contents
|
|
||||||
call_id = ctypes.cast(attr.value, ctypes.POINTER(ctypes.c_int64)).contents.value
|
|
||||||
call_desc = self.call_descriptors[call_id]
|
|
||||||
|
|
||||||
num_inputs = call_frame.contents.args.size
|
|
||||||
inputs = ctypes.cast(call_frame.contents.args.args, ctypes.POINTER(ctypes.POINTER(XLA_FFI_Buffer)))
|
|
||||||
|
|
||||||
num_outputs = call_frame.contents.rets.size
|
|
||||||
outputs = ctypes.cast(call_frame.contents.rets.rets, ctypes.POINTER(ctypes.POINTER(XLA_FFI_Buffer)))
|
|
||||||
|
|
||||||
assert num_inputs == self.num_inputs
|
|
||||||
assert num_outputs == self.num_outputs
|
|
||||||
|
|
||||||
cuda_stream = get_stream_from_callframe(call_frame.contents)
|
|
||||||
|
|
||||||
if self.graph_mode == GraphMode.WARP:
|
|
||||||
# check if we already captured an identical call
|
|
||||||
ip = [inputs[i].contents.data for i in self.array_input_indices]
|
|
||||||
op = [outputs[i].contents.data for i in self.array_output_indices]
|
|
||||||
capture_key = hash((call_id, *ip, *op))
|
|
||||||
capture = self.captures.get(capture_key)
|
|
||||||
|
|
||||||
# launch existing graph
|
|
||||||
if capture is not None:
|
|
||||||
# NOTE: We use the native graph API to avoid overhead with obtaining Stream and Device objects in Python.
|
|
||||||
# This code should match wp.capture_launch().
|
|
||||||
graph = capture.graph
|
|
||||||
if graph.graph_exec is None:
|
|
||||||
g = ctypes.c_void_p()
|
|
||||||
if not wp.context.runtime.core.wp_cuda_graph_create_exec(
|
|
||||||
graph.device.context, cuda_stream, graph.graph, ctypes.byref(g)
|
|
||||||
):
|
|
||||||
raise RuntimeError(f"Graph creation error: {wp.context.runtime.get_error_string()}")
|
|
||||||
graph.graph_exec = g
|
|
||||||
|
|
||||||
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()}")
|
|
||||||
|
|
||||||
# update the graph cache to keep recently used graphs alive
|
|
||||||
self.captures.move_to_end(capture_key)
|
|
||||||
|
|
||||||
# early out
|
|
||||||
return
|
|
||||||
|
|
||||||
device = wp.device_from_jax(get_jax_device())
|
|
||||||
stream = wp.Stream(device, cuda_stream=cuda_stream)
|
|
||||||
|
|
||||||
# reconstruct the argument list
|
|
||||||
arg_list = []
|
|
||||||
|
|
||||||
# input and in-out args
|
|
||||||
for i, arg in enumerate(self.input_args):
|
|
||||||
if arg.is_array:
|
|
||||||
buffer = inputs[i].contents
|
|
||||||
shape = buffer.dims[: buffer.rank - arg.dtype_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 = buffer.dims[: buffer.rank - arg.dtype_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.ScopedStream(stream, sync_enter=False):
|
|
||||||
if stream.is_capturing:
|
|
||||||
# capturing with JAX
|
|
||||||
with wp.ScopedCapture(external=True) as capture:
|
|
||||||
self.func(*arg_list)
|
|
||||||
# keep a reference to the capture object to prevent required modules getting unloaded
|
|
||||||
call_desc.capture = capture
|
|
||||||
elif self.graph_mode == GraphMode.WARP:
|
|
||||||
# capturing with WARP
|
|
||||||
with wp.ScopedCapture() as capture:
|
|
||||||
self.func(*arg_list)
|
|
||||||
wp.capture_launch(capture.graph)
|
|
||||||
# keep a reference to the capture object and reuse it with same buffers
|
|
||||||
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:
|
|
||||||
# not capturing
|
|
||||||
self.func(*arg_list)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(traceback.format_exc())
|
|
||||||
return create_ffi_error(
|
|
||||||
call_frame.contents.api, XLA_FFI_Error_Code.UNKNOWN, f"FFI callback error: {type(e).__name__}: {e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
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.
|
|
||||||
_FFI_KERNEL_REGISTRY: dict[str, FfiKernel] = {}
|
|
||||||
_FFI_CALLABLE_REGISTRY: dict[str, FfiCallable] = {}
|
|
||||||
_FFI_CALLBACK_REGISTRY: dict[str, ctypes.CFUNCTYPE] = {}
|
|
||||||
_FFI_REGISTRY_LOCK = threading.Lock()
|
|
||||||
|
|
||||||
|
|
||||||
def jax_kernel(
|
|
||||||
kernel, num_outputs=1, vmap_method="broadcast_all", launch_dims=None, output_dims=None, in_out_argnames=None
|
|
||||||
):
|
|
||||||
"""Create a JAX callback from a Warp kernel.
|
|
||||||
|
|
||||||
NOTE: This is an experimental feature under development.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
kernel: The Warp kernel to launch.
|
|
||||||
num_outputs: Specify the number of output arguments if greater than 1.
|
|
||||||
This must include the number of ``in_out_arguments``.
|
|
||||||
vmap_method: String specifying how the callback transforms under ``vmap()``.
|
|
||||||
This argument can also be specified for individual calls.
|
|
||||||
launch_dims: Specify the default kernel launch dimensions. If None, launch
|
|
||||||
dimensions are inferred from the shape of the first array argument.
|
|
||||||
This argument can also be specified for individual calls.
|
|
||||||
output_dims: Specify the default dimensions of output arrays. If None, output
|
|
||||||
dimensions are inferred from the launch dimensions.
|
|
||||||
This argument can also be specified for individual calls.
|
|
||||||
in_out_argnames: Names of input-output arguments.
|
|
||||||
|
|
||||||
Limitations:
|
|
||||||
- All kernel arguments must be contiguous arrays or scalars.
|
|
||||||
- Scalars must be static arguments in JAX.
|
|
||||||
- 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.
|
|
||||||
- Only the CUDA backend is supported.
|
|
||||||
"""
|
|
||||||
|
|
||||||
check_jax_version()
|
|
||||||
|
|
||||||
key = (
|
|
||||||
kernel.func,
|
|
||||||
kernel.sig,
|
|
||||||
num_outputs,
|
|
||||||
vmap_method,
|
|
||||||
tuple(launch_dims) if launch_dims else launch_dims,
|
|
||||||
tuple(sorted(output_dims.items())) if output_dims else output_dims,
|
|
||||||
)
|
|
||||||
|
|
||||||
with _FFI_REGISTRY_LOCK:
|
|
||||||
if key not in _FFI_KERNEL_REGISTRY:
|
|
||||||
new_kernel = FfiKernel(kernel, num_outputs, vmap_method, launch_dims, output_dims, in_out_argnames)
|
|
||||||
_FFI_KERNEL_REGISTRY[key] = new_kernel
|
|
||||||
|
|
||||||
return _FFI_KERNEL_REGISTRY[key]
|
|
||||||
|
|
||||||
|
|
||||||
def jax_callable(
|
|
||||||
func: Callable,
|
|
||||||
num_outputs: int = 1,
|
|
||||||
graph_compatible: Optional[bool] = None, # deprecated
|
|
||||||
graph_mode: GraphMode = GraphMode.JAX,
|
|
||||||
vmap_method: Optional[str] = "broadcast_all",
|
|
||||||
output_dims=None,
|
|
||||||
in_out_argnames=None,
|
|
||||||
graph_cache_max: int | None = None,
|
|
||||||
):
|
|
||||||
"""Create a JAX callback from an annotated Python function.
|
|
||||||
|
|
||||||
The Python function arguments must have type annotations like Warp kernels.
|
|
||||||
|
|
||||||
NOTE: This is an experimental feature under development.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
func: The Python function to call.
|
|
||||||
num_outputs: Specify the number of output arguments if greater than 1.
|
|
||||||
This must include the number of ``in_out_arguments``.
|
|
||||||
graph_compatible: Whether the function can be called during CUDA graph capture.
|
|
||||||
This argument is deprecated, use ``graph_mode`` instead.
|
|
||||||
graph_mode: CUDA graph capture mode.
|
|
||||||
``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 subgraph,
|
|
||||||
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,
|
|
||||||
such as host synchronization.
|
|
||||||
vmap_method: String specifying how the callback transforms under ``vmap()``.
|
|
||||||
This argument can also be specified for individual calls.
|
|
||||||
output_dims: Specify the default dimensions of output arrays.
|
|
||||||
If ``None``, output dimensions are inferred from the launch dimensions.
|
|
||||||
This argument can also be specified for individual calls.
|
|
||||||
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:
|
|
||||||
- All kernel arguments must be contiguous arrays or scalars.
|
|
||||||
- Scalars must be static arguments in JAX.
|
|
||||||
- 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.
|
|
||||||
- Only the CUDA backend is supported.
|
|
||||||
"""
|
|
||||||
|
|
||||||
check_jax_version()
|
|
||||||
|
|
||||||
if graph_compatible is not None:
|
|
||||||
wp.utils.warn(
|
|
||||||
"The `graph_compatible` argument is deprecated, use `graph_mode` instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=3,
|
|
||||||
)
|
|
||||||
if graph_compatible is False:
|
|
||||||
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 = (
|
|
||||||
func,
|
|
||||||
num_outputs,
|
|
||||||
graph_mode,
|
|
||||||
vmap_method,
|
|
||||||
tuple(sorted(output_dims.items())) if output_dims else output_dims,
|
|
||||||
)
|
|
||||||
|
|
||||||
with _FFI_REGISTRY_LOCK:
|
|
||||||
callable = _FFI_CALLABLE_REGISTRY.get(key)
|
|
||||||
if callable is None:
|
|
||||||
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 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()
|
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
#
|
|
||||||
# Generic FFI callbacks for Python functions of the form
|
|
||||||
# func(inputs, outputs, attrs, ctx)
|
|
||||||
#
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
|
|
||||||
def register_ffi_callback(name: str, func: Callable, graph_compatible: bool = True) -> None:
|
|
||||||
"""Create a JAX callback from a Python function.
|
|
||||||
|
|
||||||
The Python function must have the form ``func(inputs, outputs, attrs, ctx)``.
|
|
||||||
|
|
||||||
NOTE: This is an experimental feature under development.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: A unique FFI callback name.
|
|
||||||
func: The Python function to call.
|
|
||||||
graph_compatible: Whether the function can be called during CUDA graph capture.
|
|
||||||
"""
|
|
||||||
|
|
||||||
check_jax_version()
|
|
||||||
|
|
||||||
# TODO check that the name is not already registered
|
|
||||||
|
|
||||||
def ffi_callback(call_frame):
|
|
||||||
try:
|
|
||||||
extension = call_frame.contents.extension_start
|
|
||||||
# On the first call, XLA runtime will query the API version and traits
|
|
||||||
# metadata using the |extension| field. Let us respond to that query
|
|
||||||
# if the metadata extension is present.
|
|
||||||
if extension:
|
|
||||||
# Try to set the version metadata.
|
|
||||||
if extension.contents.type == XLA_FFI_Extension_Type.Metadata:
|
|
||||||
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.minor_version = 1
|
|
||||||
if graph_compatible:
|
|
||||||
# Turn on CUDA graphs for this handler.
|
|
||||||
metadata_ext.contents.metadata.contents.traits = (
|
|
||||||
XLA_FFI_Handler_TraitsBits.COMMAND_BUFFER_COMPATIBLE
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
attrs = decode_attrs(call_frame.contents.attrs)
|
|
||||||
|
|
||||||
input_count = call_frame.contents.args.size
|
|
||||||
inputs = ctypes.cast(call_frame.contents.args.args, ctypes.POINTER(ctypes.POINTER(XLA_FFI_Buffer)))
|
|
||||||
inputs = [FfiBuffer(inputs[i].contents) for i in range(input_count)]
|
|
||||||
|
|
||||||
output_count = call_frame.contents.rets.size
|
|
||||||
outputs = ctypes.cast(call_frame.contents.rets.rets, ctypes.POINTER(ctypes.POINTER(XLA_FFI_Buffer)))
|
|
||||||
outputs = [FfiBuffer(outputs[i].contents) for i in range(output_count)]
|
|
||||||
|
|
||||||
ctx = ExecutionContext(call_frame.contents)
|
|
||||||
|
|
||||||
func(inputs, outputs, attrs, ctx)
|
|
||||||
except Exception as e:
|
|
||||||
print(traceback.format_exc())
|
|
||||||
return create_ffi_error(
|
|
||||||
call_frame.contents.api, XLA_FFI_Error_Code.UNKNOWN, f"FFI callback error: {type(e).__name__}: {e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
FFI_CCALLFUNC = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.POINTER(XLA_FFI_CallFrame))
|
|
||||||
callback_func = FFI_CCALLFUNC(ffi_callback)
|
|
||||||
with _FFI_REGISTRY_LOCK:
|
|
||||||
_FFI_CALLBACK_REGISTRY[name] = callback_func
|
|
||||||
ffi_ccall_address = ctypes.cast(callback_func, ctypes.c_void_p)
|
|
||||||
ffi_capsule = jax.ffi.pycapsule(ffi_ccall_address.value)
|
|
||||||
jax.ffi.register_ffi_target(name, ffi_capsule, platform="CUDA")
|
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
#
|
|
||||||
# Utilities
|
|
||||||
#
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
# ensure unique FFI callback names
|
|
||||||
ffi_name_counts = {}
|
|
||||||
|
|
||||||
|
|
||||||
def generate_unique_name(func) -> str:
|
|
||||||
key = make_full_qualified_name(func)
|
|
||||||
unique_id = ffi_name_counts.get(key, 0)
|
|
||||||
ffi_name_counts[key] = unique_id + 1
|
|
||||||
return f"{key}_{unique_id}"
|
|
||||||
|
|
||||||
|
|
||||||
def get_warp_shape(arg, dims):
|
|
||||||
if arg.dtype_ndim > 0:
|
|
||||||
# vector/matrix array
|
|
||||||
return dims[: arg.warp_ndim]
|
|
||||||
else:
|
|
||||||
# scalar array
|
|
||||||
return dims
|
|
||||||
|
|
||||||
|
|
||||||
def get_jax_output_type(arg, dims):
|
|
||||||
if isinstance(dims, int):
|
|
||||||
dims = (dims,)
|
|
||||||
|
|
||||||
ndim = len(dims)
|
|
||||||
|
|
||||||
if arg.dtype_ndim > 0:
|
|
||||||
# vector/matrix array
|
|
||||||
if ndim == arg.warp_ndim:
|
|
||||||
return jax.ShapeDtypeStruct((*dims, *arg.dtype_shape), arg.jax_scalar_type)
|
|
||||||
elif ndim == arg.jax_ndim:
|
|
||||||
# make sure inner dimensions match
|
|
||||||
inner_dims = dims[-arg.dtype_ndim :]
|
|
||||||
for i in range(arg.dtype_ndim):
|
|
||||||
if inner_dims[i] != arg.dtype_shape[i]:
|
|
||||||
raise ValueError(f"Invalid output dimensions for argument '{arg.name}': {dims}")
|
|
||||||
return jax.ShapeDtypeStruct(dims, arg.jax_scalar_type)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Invalid output dimensions for argument '{arg.name}': {dims}")
|
|
||||||
else:
|
|
||||||
# scalar array
|
|
||||||
if ndim != arg.warp_ndim:
|
|
||||||
raise ValueError(f"Invalid output dimensions for argument '{arg.name}': {dims}")
|
|
||||||
return jax.ShapeDtypeStruct(dims, arg.jax_scalar_type)
|
|
||||||
|
|||||||
+5
-605
@@ -13,612 +13,12 @@
|
|||||||
# 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 ctypes
|
# TODO: Remove after cleaning up the public API.
|
||||||
import enum
|
|
||||||
|
|
||||||
import jax.numpy as jnp
|
from warp._src.jax_experimental import xla_ffi as _xla_ffi
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
import warp as wp
|
|
||||||
|
|
||||||
#######################################################################
|
def __getattr__(name):
|
||||||
# ctypes structures and enums for XLA's FFI API:
|
from warp._src.utils import get_deprecated_api
|
||||||
# https://github.com/openxla/xla/blob/a1a5e62fbffa3a3b6c409d72607456cf5b353a22/xla/ffi/api/c_api.h
|
|
||||||
#######################################################################
|
|
||||||
|
|
||||||
|
return get_deprecated_api(_xla_ffi, "wp.jax_experimental", name)
|
||||||
# typedef enum {
|
|
||||||
# XLA_FFI_Extension_Metadata = 1,
|
|
||||||
# } XLA_FFI_Extension_Type;
|
|
||||||
class XLA_FFI_Extension_Type(enum.IntEnum):
|
|
||||||
Metadata = 1
|
|
||||||
|
|
||||||
|
|
||||||
# typedef struct XLA_FFI_Extension_Base {
|
|
||||||
# size_t struct_size;
|
|
||||||
# XLA_FFI_Extension_Type type;
|
|
||||||
# struct XLA_FFI_Extension_Base* next;
|
|
||||||
# } XLA_FFI_Extension_Base;
|
|
||||||
class XLA_FFI_Extension_Base(ctypes.Structure):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
XLA_FFI_Extension_Base._fields_ = [
|
|
||||||
("struct_size", ctypes.c_size_t),
|
|
||||||
("type", ctypes.c_int), # XLA_FFI_Extension_Type
|
|
||||||
("next", ctypes.POINTER(XLA_FFI_Extension_Base)),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# typedef enum {
|
|
||||||
# XLA_FFI_ExecutionStage_INSTANTIATE = 0,
|
|
||||||
# XLA_FFI_ExecutionStage_PREPARE = 1,
|
|
||||||
# XLA_FFI_ExecutionStage_INITIALIZE = 2,
|
|
||||||
# XLA_FFI_ExecutionStage_EXECUTE = 3,
|
|
||||||
# } XLA_FFI_ExecutionStage;
|
|
||||||
class XLA_FFI_ExecutionStage(enum.IntEnum):
|
|
||||||
INSTANTIATE = 0
|
|
||||||
PREPARE = 1
|
|
||||||
INITIALIZE = 2
|
|
||||||
EXECUTE = 3
|
|
||||||
|
|
||||||
|
|
||||||
# typedef enum {
|
|
||||||
# XLA_FFI_DataType_INVALID = 0,
|
|
||||||
# XLA_FFI_DataType_PRED = 1,
|
|
||||||
# XLA_FFI_DataType_S8 = 2,
|
|
||||||
# XLA_FFI_DataType_S16 = 3,
|
|
||||||
# XLA_FFI_DataType_S32 = 4,
|
|
||||||
# XLA_FFI_DataType_S64 = 5,
|
|
||||||
# XLA_FFI_DataType_U8 = 6,
|
|
||||||
# XLA_FFI_DataType_U16 = 7,
|
|
||||||
# XLA_FFI_DataType_U32 = 8,
|
|
||||||
# XLA_FFI_DataType_U64 = 9,
|
|
||||||
# XLA_FFI_DataType_F16 = 10,
|
|
||||||
# XLA_FFI_DataType_F32 = 11,
|
|
||||||
# XLA_FFI_DataType_F64 = 12,
|
|
||||||
# XLA_FFI_DataType_BF16 = 16,
|
|
||||||
# XLA_FFI_DataType_C64 = 15,
|
|
||||||
# XLA_FFI_DataType_C128 = 18,
|
|
||||||
# XLA_FFI_DataType_TOKEN = 17,
|
|
||||||
# XLA_FFI_DataType_F8E5M2 = 19,
|
|
||||||
# XLA_FFI_DataType_F8E3M4 = 29,
|
|
||||||
# XLA_FFI_DataType_F8E4M3 = 28,
|
|
||||||
# XLA_FFI_DataType_F8E4M3FN = 20,
|
|
||||||
# XLA_FFI_DataType_F8E4M3B11FNUZ = 23,
|
|
||||||
# XLA_FFI_DataType_F8E5M2FNUZ = 24,
|
|
||||||
# XLA_FFI_DataType_F8E4M3FNUZ = 25,
|
|
||||||
# XLA_FFI_DataType_F4E2M1FN = 32,
|
|
||||||
# XLA_FFI_DataType_F8E8M0FNU = 33,
|
|
||||||
# } XLA_FFI_DataType;
|
|
||||||
class XLA_FFI_DataType(enum.IntEnum):
|
|
||||||
INVALID = 0
|
|
||||||
PRED = 1
|
|
||||||
S8 = 2
|
|
||||||
S16 = 3
|
|
||||||
S32 = 4
|
|
||||||
S64 = 5
|
|
||||||
U8 = 6
|
|
||||||
U16 = 7
|
|
||||||
U32 = 8
|
|
||||||
U64 = 9
|
|
||||||
F16 = 10
|
|
||||||
F32 = 11
|
|
||||||
F64 = 12
|
|
||||||
BF16 = 16
|
|
||||||
C64 = 15
|
|
||||||
C128 = 18
|
|
||||||
TOKEN = 17
|
|
||||||
F8E5M2 = 19
|
|
||||||
F8E3M4 = 29
|
|
||||||
F8E4M3 = 28
|
|
||||||
F8E4M3FN = 20
|
|
||||||
F8E4M3B11FNUZ = 23
|
|
||||||
F8E5M2FNUZ = 24
|
|
||||||
F8E4M3FNUZ = 25
|
|
||||||
F4E2M1FN = 32
|
|
||||||
F8E8M0FNU = 33
|
|
||||||
|
|
||||||
|
|
||||||
# struct XLA_FFI_Buffer {
|
|
||||||
# size_t struct_size;
|
|
||||||
# XLA_FFI_Extension_Base* extension_start;
|
|
||||||
#
|
|
||||||
# XLA_FFI_DataType dtype;
|
|
||||||
# void* data;
|
|
||||||
# int64_t rank;
|
|
||||||
# int64_t* dims; // length == rank
|
|
||||||
# };
|
|
||||||
class XLA_FFI_Buffer(ctypes.Structure):
|
|
||||||
_fields_ = (
|
|
||||||
("struct_size", ctypes.c_size_t),
|
|
||||||
("extension_start", ctypes.POINTER(XLA_FFI_Extension_Base)),
|
|
||||||
("dtype", ctypes.c_int), # XLA_FFI_DataType
|
|
||||||
("data", ctypes.c_void_p),
|
|
||||||
("rank", ctypes.c_int64),
|
|
||||||
("dims", ctypes.POINTER(ctypes.c_int64)),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# typedef enum {
|
|
||||||
# XLA_FFI_ArgType_BUFFER = 1,
|
|
||||||
# } XLA_FFI_ArgType;
|
|
||||||
class XLA_FFI_ArgType(enum.IntEnum):
|
|
||||||
BUFFER = 1
|
|
||||||
|
|
||||||
|
|
||||||
# typedef enum {
|
|
||||||
# XLA_FFI_RetType_BUFFER = 1,
|
|
||||||
# } XLA_FFI_RetType;
|
|
||||||
class XLA_FFI_RetType(enum.IntEnum):
|
|
||||||
BUFFER = 1
|
|
||||||
|
|
||||||
|
|
||||||
# struct XLA_FFI_Args {
|
|
||||||
# size_t struct_size;
|
|
||||||
# XLA_FFI_Extension_Base* extension_start;
|
|
||||||
# int64_t size;
|
|
||||||
# XLA_FFI_ArgType* types; // length == size
|
|
||||||
# void** args; // length == size
|
|
||||||
# };
|
|
||||||
class XLA_FFI_Args(ctypes.Structure):
|
|
||||||
_fields_ = (
|
|
||||||
("struct_size", ctypes.c_size_t),
|
|
||||||
("extension_start", ctypes.POINTER(XLA_FFI_Extension_Base)),
|
|
||||||
("size", ctypes.c_int64),
|
|
||||||
("types", ctypes.POINTER(ctypes.c_int)), # XLA_FFI_ArgType*
|
|
||||||
("args", ctypes.POINTER(ctypes.c_void_p)),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# struct XLA_FFI_Rets {
|
|
||||||
# size_t struct_size;
|
|
||||||
# XLA_FFI_Extension_Base* extension_start;
|
|
||||||
# int64_t size;
|
|
||||||
# XLA_FFI_RetType* types; // length == size
|
|
||||||
# void** rets; // length == size
|
|
||||||
# };
|
|
||||||
class XLA_FFI_Rets(ctypes.Structure):
|
|
||||||
_fields_ = (
|
|
||||||
("struct_size", ctypes.c_size_t),
|
|
||||||
("extension_start", ctypes.POINTER(XLA_FFI_Extension_Base)),
|
|
||||||
("size", ctypes.c_int64),
|
|
||||||
("types", ctypes.POINTER(ctypes.c_int)), # XLA_FFI_RetType*
|
|
||||||
("rets", ctypes.POINTER(ctypes.c_void_p)),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# typedef struct XLA_FFI_ByteSpan {
|
|
||||||
# const char* ptr;
|
|
||||||
# size_t len;
|
|
||||||
# } XLA_FFI_ByteSpan;
|
|
||||||
class XLA_FFI_ByteSpan(ctypes.Structure):
|
|
||||||
_fields_ = (
|
|
||||||
("ptr", ctypes.POINTER(ctypes.c_char)),
|
|
||||||
("len", ctypes.c_size_t),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# typedef struct XLA_FFI_Scalar {
|
|
||||||
# XLA_FFI_DataType dtype;
|
|
||||||
# void* value;
|
|
||||||
# } XLA_FFI_Scalar;
|
|
||||||
class XLA_FFI_Scalar(ctypes.Structure):
|
|
||||||
_fields_ = (
|
|
||||||
("dtype", ctypes.c_int),
|
|
||||||
("value", ctypes.c_void_p),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# typedef struct XLA_FFI_Array {
|
|
||||||
# XLA_FFI_DataType dtype;
|
|
||||||
# size_t size;
|
|
||||||
# void* data;
|
|
||||||
# } XLA_FFI_Array;
|
|
||||||
class XLA_FFI_Array(ctypes.Structure):
|
|
||||||
_fields_ = (
|
|
||||||
("dtype", ctypes.c_int),
|
|
||||||
("size", ctypes.c_size_t),
|
|
||||||
("data", ctypes.c_void_p),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# typedef enum {
|
|
||||||
# XLA_FFI_AttrType_ARRAY = 1,
|
|
||||||
# XLA_FFI_AttrType_DICTIONARY = 2,
|
|
||||||
# XLA_FFI_AttrType_SCALAR = 3,
|
|
||||||
# XLA_FFI_AttrType_STRING = 4,
|
|
||||||
# } XLA_FFI_AttrType;
|
|
||||||
class XLA_FFI_AttrType(enum.IntEnum):
|
|
||||||
ARRAY = 1
|
|
||||||
DICTIONARY = 2
|
|
||||||
SCALAR = 3
|
|
||||||
STRING = 4
|
|
||||||
|
|
||||||
|
|
||||||
# struct XLA_FFI_Attrs {
|
|
||||||
# size_t struct_size;
|
|
||||||
# XLA_FFI_Extension_Base* extension_start;
|
|
||||||
# int64_t size;
|
|
||||||
# XLA_FFI_AttrType* types; // length == size
|
|
||||||
# XLA_FFI_ByteSpan** names; // length == size
|
|
||||||
# void** attrs; // length == size
|
|
||||||
# };
|
|
||||||
class XLA_FFI_Attrs(ctypes.Structure):
|
|
||||||
_fields_ = (
|
|
||||||
("struct_size", ctypes.c_size_t),
|
|
||||||
("extension_start", ctypes.POINTER(XLA_FFI_Extension_Base)),
|
|
||||||
("size", ctypes.c_int64),
|
|
||||||
("types", ctypes.POINTER(ctypes.c_int)), # XLA_FFI_AttrType*
|
|
||||||
("names", ctypes.POINTER(ctypes.POINTER(XLA_FFI_ByteSpan))),
|
|
||||||
("attrs", ctypes.POINTER(ctypes.c_void_p)),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# struct XLA_FFI_Api_Version {
|
|
||||||
# size_t struct_size;
|
|
||||||
# XLA_FFI_Extension_Base* extension_start;
|
|
||||||
# int major_version; // out
|
|
||||||
# int minor_version; // out
|
|
||||||
# };
|
|
||||||
class XLA_FFI_Api_Version(ctypes.Structure):
|
|
||||||
_fields_ = (
|
|
||||||
("struct_size", ctypes.c_size_t),
|
|
||||||
("extension_start", ctypes.POINTER(XLA_FFI_Extension_Base)),
|
|
||||||
("major_version", ctypes.c_int),
|
|
||||||
("minor_version", ctypes.c_int),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# enum XLA_FFI_Handler_TraitsBits {
|
|
||||||
# // Calls to FFI handler are safe to trace into the command buffer. It means
|
|
||||||
# // that calls to FFI handler always launch exactly the same device operations
|
|
||||||
# // (can depend on attribute values) that can be captured and then replayed.
|
|
||||||
# XLA_FFI_HANDLER_TRAITS_COMMAND_BUFFER_COMPATIBLE = 1u << 0,
|
|
||||||
# };
|
|
||||||
class XLA_FFI_Handler_TraitsBits(enum.IntEnum):
|
|
||||||
COMMAND_BUFFER_COMPATIBLE = 1 << 0
|
|
||||||
|
|
||||||
|
|
||||||
# struct XLA_FFI_Metadata {
|
|
||||||
# size_t struct_size;
|
|
||||||
# XLA_FFI_Api_Version api_version;
|
|
||||||
# XLA_FFI_Handler_Traits traits;
|
|
||||||
# };
|
|
||||||
class XLA_FFI_Metadata(ctypes.Structure):
|
|
||||||
_fields_ = (
|
|
||||||
("struct_size", ctypes.c_size_t),
|
|
||||||
("api_version", XLA_FFI_Api_Version), # XLA_FFI_Extension_Type
|
|
||||||
("traits", ctypes.c_uint32), # XLA_FFI_Handler_Traits
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# struct XLA_FFI_Metadata_Extension {
|
|
||||||
# XLA_FFI_Extension_Base extension_base;
|
|
||||||
# XLA_FFI_Metadata* metadata;
|
|
||||||
# };
|
|
||||||
class XLA_FFI_Metadata_Extension(ctypes.Structure):
|
|
||||||
_fields_ = (
|
|
||||||
("extension_base", XLA_FFI_Extension_Base),
|
|
||||||
("metadata", ctypes.POINTER(XLA_FFI_Metadata)),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# typedef enum {
|
|
||||||
# XLA_FFI_Error_Code_OK = 0,
|
|
||||||
# XLA_FFI_Error_Code_CANCELLED = 1,
|
|
||||||
# XLA_FFI_Error_Code_UNKNOWN = 2,
|
|
||||||
# XLA_FFI_Error_Code_INVALID_ARGUMENT = 3,
|
|
||||||
# XLA_FFI_Error_Code_DEADLINE_EXCEEDED = 4,
|
|
||||||
# XLA_FFI_Error_Code_NOT_FOUND = 5,
|
|
||||||
# XLA_FFI_Error_Code_ALREADY_EXISTS = 6,
|
|
||||||
# XLA_FFI_Error_Code_PERMISSION_DENIED = 7,
|
|
||||||
# XLA_FFI_Error_Code_RESOURCE_EXHAUSTED = 8,
|
|
||||||
# XLA_FFI_Error_Code_FAILED_PRECONDITION = 9,
|
|
||||||
# XLA_FFI_Error_Code_ABORTED = 10,
|
|
||||||
# XLA_FFI_Error_Code_OUT_OF_RANGE = 11,
|
|
||||||
# XLA_FFI_Error_Code_UNIMPLEMENTED = 12,
|
|
||||||
# XLA_FFI_Error_Code_INTERNAL = 13,
|
|
||||||
# XLA_FFI_Error_Code_UNAVAILABLE = 14,
|
|
||||||
# XLA_FFI_Error_Code_DATA_LOSS = 15,
|
|
||||||
# XLA_FFI_Error_Code_UNAUTHENTICATED = 16
|
|
||||||
# } XLA_FFI_Error_Code;
|
|
||||||
class XLA_FFI_Error_Code(enum.IntEnum):
|
|
||||||
OK = 0
|
|
||||||
CANCELLED = 1
|
|
||||||
UNKNOWN = 2
|
|
||||||
INVALID_ARGUMENT = 3
|
|
||||||
DEADLINE_EXCEEDED = 4
|
|
||||||
NOT_FOUND = 5
|
|
||||||
ALREADY_EXISTS = 6
|
|
||||||
PERMISSION_DENIED = 7
|
|
||||||
RESOURCE_EXHAUSTED = 8
|
|
||||||
FAILED_PRECONDITION = 9
|
|
||||||
ABORTED = 10
|
|
||||||
OUT_OF_RANGE = 11
|
|
||||||
UNIMPLEMENTED = 12
|
|
||||||
INTERNAL = 13
|
|
||||||
UNAVAILABLE = 14
|
|
||||||
DATA_LOSS = 15
|
|
||||||
UNAUTHENTICATED = 16
|
|
||||||
|
|
||||||
|
|
||||||
# struct XLA_FFI_Error_Create_Args {
|
|
||||||
# size_t struct_size;
|
|
||||||
# XLA_FFI_Extension_Base* extension_start;
|
|
||||||
# const char* message;
|
|
||||||
# XLA_FFI_Error_Code errc;
|
|
||||||
# };
|
|
||||||
class XLA_FFI_Error_Create_Args(ctypes.Structure):
|
|
||||||
_fields_ = (
|
|
||||||
("struct_size", ctypes.c_size_t),
|
|
||||||
("extension_start", ctypes.POINTER(XLA_FFI_Extension_Base)),
|
|
||||||
("message", ctypes.c_char_p),
|
|
||||||
("errc", ctypes.c_int),
|
|
||||||
) # XLA_FFI_Error_Code
|
|
||||||
|
|
||||||
|
|
||||||
XLA_FFI_Error_Create = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.POINTER(XLA_FFI_Error_Create_Args))
|
|
||||||
|
|
||||||
|
|
||||||
# struct XLA_FFI_Stream_Get_Args {
|
|
||||||
# size_t struct_size;
|
|
||||||
# XLA_FFI_Extension_Base* extension_start;
|
|
||||||
# XLA_FFI_ExecutionContext* ctx;
|
|
||||||
# void* stream; // out
|
|
||||||
# };
|
|
||||||
class XLA_FFI_Stream_Get_Args(ctypes.Structure):
|
|
||||||
_fields_ = (
|
|
||||||
("struct_size", ctypes.c_size_t),
|
|
||||||
("extension_start", ctypes.POINTER(XLA_FFI_Extension_Base)),
|
|
||||||
("ctx", ctypes.c_void_p), # XLA_FFI_ExecutionContext*
|
|
||||||
("stream", ctypes.c_void_p),
|
|
||||||
) # // out
|
|
||||||
|
|
||||||
|
|
||||||
XLA_FFI_Stream_Get = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.POINTER(XLA_FFI_Stream_Get_Args))
|
|
||||||
|
|
||||||
|
|
||||||
# struct XLA_FFI_Api {
|
|
||||||
# size_t struct_size;
|
|
||||||
# XLA_FFI_Extension_Base* extension_start;
|
|
||||||
#
|
|
||||||
# XLA_FFI_Api_Version api_version;
|
|
||||||
# XLA_FFI_InternalApi* internal_api;
|
|
||||||
#
|
|
||||||
# _XLA_FFI_API_STRUCT_FIELD(XLA_FFI_Error_Create);
|
|
||||||
# _XLA_FFI_API_STRUCT_FIELD(XLA_FFI_Error_GetMessage);
|
|
||||||
# _XLA_FFI_API_STRUCT_FIELD(XLA_FFI_Error_Destroy);
|
|
||||||
# _XLA_FFI_API_STRUCT_FIELD(XLA_FFI_Handler_Register);
|
|
||||||
# _XLA_FFI_API_STRUCT_FIELD(XLA_FFI_Stream_Get);
|
|
||||||
# _XLA_FFI_API_STRUCT_FIELD(XLA_FFI_TypeId_Register);
|
|
||||||
# _XLA_FFI_API_STRUCT_FIELD(XLA_FFI_ExecutionContext_Get);
|
|
||||||
# _XLA_FFI_API_STRUCT_FIELD(XLA_FFI_State_Set);
|
|
||||||
# _XLA_FFI_API_STRUCT_FIELD(XLA_FFI_State_Get);
|
|
||||||
# _XLA_FFI_API_STRUCT_FIELD(XLA_FFI_DeviceMemory_Allocate);
|
|
||||||
# _XLA_FFI_API_STRUCT_FIELD(XLA_FFI_DeviceMemory_Free);
|
|
||||||
# _XLA_FFI_API_STRUCT_FIELD(XLA_FFI_ThreadPool_Schedule);
|
|
||||||
# _XLA_FFI_API_STRUCT_FIELD(XLA_FFI_ThreadPool_NumThreads);
|
|
||||||
# _XLA_FFI_API_STRUCT_FIELD(XLA_FFI_Future_Create);
|
|
||||||
# _XLA_FFI_API_STRUCT_FIELD(XLA_FFI_Future_SetAvailable);
|
|
||||||
# _XLA_FFI_API_STRUCT_FIELD(XLA_FFI_Future_SetError);
|
|
||||||
# };
|
|
||||||
class XLA_FFI_Api(ctypes.Structure):
|
|
||||||
_fields_ = (
|
|
||||||
("struct_size", ctypes.c_size_t),
|
|
||||||
("extension_start", ctypes.POINTER(XLA_FFI_Extension_Base)),
|
|
||||||
("api_version", XLA_FFI_Api_Version),
|
|
||||||
("internal_api", ctypes.c_void_p), # XLA_FFI_InternalApi*
|
|
||||||
("XLA_FFI_Error_Create", XLA_FFI_Error_Create), # XLA_FFI_Error_Create
|
|
||||||
("XLA_FFI_Error_GetMessage", ctypes.c_void_p), # XLA_FFI_Error_GetMessage
|
|
||||||
("XLA_FFI_Error_Destroy", ctypes.c_void_p), # XLA_FFI_Error_Destroy
|
|
||||||
("XLA_FFI_Handler_Register", ctypes.c_void_p), # XLA_FFI_Handler_Register
|
|
||||||
("XLA_FFI_Stream_Get", XLA_FFI_Stream_Get), # XLA_FFI_Stream_Get
|
|
||||||
("XLA_FFI_TypeId_Register", ctypes.c_void_p), # XLA_FFI_TypeId_Register
|
|
||||||
("XLA_FFI_ExecutionContext_Get", ctypes.c_void_p), # XLA_FFI_ExecutionContext_Get
|
|
||||||
("XLA_FFI_State_Set", ctypes.c_void_p), # XLA_FFI_State_Set
|
|
||||||
("XLA_FFI_State_Get", ctypes.c_void_p), # XLA_FFI_State_Get
|
|
||||||
("XLA_FFI_DeviceMemory_Allocate", ctypes.c_void_p), # XLA_FFI_DeviceMemory_Allocate
|
|
||||||
("XLA_FFI_DeviceMemory_Free", ctypes.c_void_p), # XLA_FFI_DeviceMemory_Free
|
|
||||||
("XLA_FFI_ThreadPool_Schedule", ctypes.c_void_p), # XLA_FFI_ThreadPool_Schedule
|
|
||||||
("XLA_FFI_ThreadPool_NumThreads", ctypes.c_void_p), # XLA_FFI_ThreadPool_NumThreads
|
|
||||||
("XLA_FFI_Future_Create", ctypes.c_void_p), # XLA_FFI_Future_Create
|
|
||||||
("XLA_FFI_Future_SetAvailable", ctypes.c_void_p), # XLA_FFI_Future_SetAvailable
|
|
||||||
("XLA_FFI_Future_SetError", ctypes.c_void_p), # XLA_FFI_Future_SetError
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# struct XLA_FFI_CallFrame {
|
|
||||||
# size_t struct_size;
|
|
||||||
# XLA_FFI_Extension_Base* extension_start;
|
|
||||||
# const XLA_FFI_Api* api;
|
|
||||||
# XLA_FFI_ExecutionContext* ctx;
|
|
||||||
# XLA_FFI_ExecutionStage stage;
|
|
||||||
# XLA_FFI_Args args;
|
|
||||||
# XLA_FFI_Rets rets;
|
|
||||||
# XLA_FFI_Attrs attrs;
|
|
||||||
#
|
|
||||||
# // XLA FFI handler implementation can use `future` to signal a result of
|
|
||||||
# // asynchronous computation to the XLA runtime. XLA runtime will keep all
|
|
||||||
# // arguments, results and attributes alive until `future` is completed.
|
|
||||||
# XLA_FFI_Future* future; // out
|
|
||||||
# };
|
|
||||||
class XLA_FFI_CallFrame(ctypes.Structure):
|
|
||||||
_fields_ = (
|
|
||||||
("struct_size", ctypes.c_size_t),
|
|
||||||
("extension_start", ctypes.POINTER(XLA_FFI_Extension_Base)),
|
|
||||||
("api", ctypes.POINTER(XLA_FFI_Api)),
|
|
||||||
("ctx", ctypes.c_void_p), # XLA_FFI_ExecutionContext*
|
|
||||||
("stage", ctypes.c_int), # XLA_FFI_ExecutionStage
|
|
||||||
("args", XLA_FFI_Args),
|
|
||||||
("rets", XLA_FFI_Rets),
|
|
||||||
("attrs", XLA_FFI_Attrs),
|
|
||||||
("future", ctypes.c_void_p), # XLA_FFI_Future* // out
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
_xla_data_type_to_constructor = {
|
|
||||||
# XLA_FFI_DataType.INVALID
|
|
||||||
XLA_FFI_DataType.PRED: jnp.bool,
|
|
||||||
XLA_FFI_DataType.S8: jnp.int8,
|
|
||||||
XLA_FFI_DataType.S16: jnp.int16,
|
|
||||||
XLA_FFI_DataType.S32: jnp.int32,
|
|
||||||
XLA_FFI_DataType.S64: jnp.int64,
|
|
||||||
XLA_FFI_DataType.U8: jnp.uint8,
|
|
||||||
XLA_FFI_DataType.U16: jnp.uint16,
|
|
||||||
XLA_FFI_DataType.U32: jnp.uint32,
|
|
||||||
XLA_FFI_DataType.U64: jnp.uint64,
|
|
||||||
XLA_FFI_DataType.F16: jnp.float16,
|
|
||||||
XLA_FFI_DataType.F32: jnp.float32,
|
|
||||||
XLA_FFI_DataType.F64: jnp.float64,
|
|
||||||
XLA_FFI_DataType.BF16: jnp.bfloat16,
|
|
||||||
XLA_FFI_DataType.C64: jnp.complex64,
|
|
||||||
XLA_FFI_DataType.C128: jnp.complex128,
|
|
||||||
# XLA_FFI_DataType.TOKEN
|
|
||||||
# XLA_FFI_DataType.F4E2M1FN: jnp.float4_e2m1fn.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
|
|
||||||
#######################################################################
|
|
||||||
|
|
||||||
|
|
||||||
def decode_bytespan(span: XLA_FFI_ByteSpan):
|
|
||||||
len = span.len
|
|
||||||
chars = ctypes.cast(span.ptr, ctypes.POINTER(ctypes.c_char * len))
|
|
||||||
return chars.contents.value.decode("utf-8")
|
|
||||||
|
|
||||||
|
|
||||||
def decode_scalar(scalar: XLA_FFI_Scalar):
|
|
||||||
# TODO validate if dtype supported
|
|
||||||
dtype = jnp.dtype(_xla_data_type_to_constructor[scalar.dtype])
|
|
||||||
bytes = ctypes.string_at(scalar.value, dtype.itemsize)
|
|
||||||
return np.frombuffer(bytes, dtype=dtype).reshape(())
|
|
||||||
|
|
||||||
|
|
||||||
def decode_array(array: XLA_FFI_Array):
|
|
||||||
# TODO validate if dtype supported
|
|
||||||
dtype = jnp.dtype(_xla_data_type_to_constructor[array.dtype])
|
|
||||||
bytes = ctypes.string_at(array.data, dtype.itemsize * array.size)
|
|
||||||
return np.frombuffer(bytes, dtype=dtype)
|
|
||||||
|
|
||||||
|
|
||||||
def decode_attrs(attrs: XLA_FFI_Attrs):
|
|
||||||
result = {}
|
|
||||||
for i in range(attrs.size):
|
|
||||||
attr_name = decode_bytespan(attrs.names[i].contents)
|
|
||||||
attr_type = attrs.types[i]
|
|
||||||
if attr_type == XLA_FFI_AttrType.STRING:
|
|
||||||
bytespan = ctypes.cast(attrs.attrs[i], ctypes.POINTER(XLA_FFI_ByteSpan))
|
|
||||||
attr_value = decode_bytespan(bytespan.contents)
|
|
||||||
elif attr_type == XLA_FFI_AttrType.SCALAR:
|
|
||||||
attr_value = ctypes.cast(attrs.attrs[i], ctypes.POINTER(XLA_FFI_Scalar))
|
|
||||||
attr_value = decode_scalar(attr_value.contents)
|
|
||||||
elif attr_type == XLA_FFI_AttrType.ARRAY:
|
|
||||||
attr_value = ctypes.cast(attrs.attrs[i], ctypes.POINTER(XLA_FFI_Array))
|
|
||||||
attr_value = decode_array(attr_value.contents)
|
|
||||||
elif attr_type == XLA_FFI_AttrType.DICTIONARY:
|
|
||||||
attr_value = ctypes.cast(attrs.attrs[i], ctypes.POINTER(XLA_FFI_Attrs))
|
|
||||||
attr_value = decode_attrs(attr_value.contents)
|
|
||||||
else:
|
|
||||||
raise Exception("Unexpected attr type")
|
|
||||||
result[attr_name] = attr_value
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
# error-string to XLA_FFI_Error
|
|
||||||
def create_ffi_error(api, errc, message):
|
|
||||||
create_args = XLA_FFI_Error_Create_Args(
|
|
||||||
ctypes.sizeof(XLA_FFI_Error_Create_Args),
|
|
||||||
ctypes.POINTER(XLA_FFI_Extension_Base)(),
|
|
||||||
ctypes.c_char_p(message.encode("utf-8")),
|
|
||||||
errc,
|
|
||||||
)
|
|
||||||
return api.contents.XLA_FFI_Error_Create(create_args)
|
|
||||||
|
|
||||||
|
|
||||||
def create_invalid_argument_ffi_error(api, message):
|
|
||||||
return create_ffi_error(api, XLA_FFI_Error_Code.INVALID_ARGUMENT, message)
|
|
||||||
|
|
||||||
|
|
||||||
# Extract CUDA stream from XLA_FFI_CallFrame.
|
|
||||||
def get_stream_from_callframe(call_frame):
|
|
||||||
api = call_frame.api
|
|
||||||
get_stream_args = XLA_FFI_Stream_Get_Args(
|
|
||||||
ctypes.sizeof(XLA_FFI_Stream_Get_Args), ctypes.POINTER(XLA_FFI_Extension_Base)(), call_frame.ctx, None
|
|
||||||
)
|
|
||||||
api.contents.XLA_FFI_Stream_Get(get_stream_args)
|
|
||||||
# TODO check result
|
|
||||||
return get_stream_args.stream
|
|
||||||
|
|
||||||
|
|
||||||
_dtype_from_ffi = {
|
|
||||||
XLA_FFI_DataType.S8: wp.int8,
|
|
||||||
XLA_FFI_DataType.S16: wp.int16,
|
|
||||||
XLA_FFI_DataType.S32: wp.int32,
|
|
||||||
XLA_FFI_DataType.S64: wp.int64,
|
|
||||||
XLA_FFI_DataType.U8: wp.uint8,
|
|
||||||
XLA_FFI_DataType.U16: wp.uint16,
|
|
||||||
XLA_FFI_DataType.U32: wp.uint32,
|
|
||||||
XLA_FFI_DataType.U64: wp.uint64,
|
|
||||||
XLA_FFI_DataType.F16: wp.float16,
|
|
||||||
XLA_FFI_DataType.F32: wp.float32,
|
|
||||||
XLA_FFI_DataType.F64: wp.float64,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def dtype_from_ffi(ffi_dtype):
|
|
||||||
return _dtype_from_ffi.get(ffi_dtype)
|
|
||||||
|
|
||||||
|
|
||||||
def jax_dtype_from_ffi(ffi_dtype):
|
|
||||||
return _xla_data_type_to_constructor.get(ffi_dtype)
|
|
||||||
|
|
||||||
|
|
||||||
# Execution context (stream, stage)
|
|
||||||
class ExecutionContext:
|
|
||||||
stage: XLA_FFI_ExecutionStage
|
|
||||||
stream: int
|
|
||||||
|
|
||||||
def __init__(self, callframe: XLA_FFI_CallFrame):
|
|
||||||
self.stage = XLA_FFI_ExecutionStage(callframe.stage)
|
|
||||||
self.stream = get_stream_from_callframe(callframe)
|
|
||||||
|
|
||||||
|
|
||||||
class FfiBuffer:
|
|
||||||
dtype: str
|
|
||||||
data: int
|
|
||||||
shape: tuple[int]
|
|
||||||
|
|
||||||
def __init__(self, xla_buffer):
|
|
||||||
# TODO check if valid
|
|
||||||
self.dtype = jnp.dtype(_xla_data_type_to_constructor[xla_buffer.dtype])
|
|
||||||
self.shape = tuple(xla_buffer.dims[i] for i in range(xla_buffer.rank))
|
|
||||||
self.data = xla_buffer.data
|
|
||||||
|
|
||||||
@property
|
|
||||||
def __cuda_array_interface__(self):
|
|
||||||
return {
|
|
||||||
"shape": self.shape,
|
|
||||||
"typestr": self.dtype.char,
|
|
||||||
"data": (self.data, False),
|
|
||||||
"version": 2,
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user