Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
811 changes: 811 additions & 0 deletions docs/plans/2026-03-21-cython-runtime-core-alignment.md

Large diffs are not rendered by default.

60 changes: 20 additions & 40 deletions src/candle/_cython/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
_HAS_CYTHON_TENSOR_API = False

try:
from ._dispatch import cy_dispatch, cy_dispatch_with_keyset # noqa: F401
from ._dispatch import FastDispatchKeySet # noqa: F401
_HAS_CYTHON_DISPATCH = True
except ImportError:
pass
Expand Down Expand Up @@ -79,13 +79,25 @@
except ImportError:
pass

from ._tensor_impl import TensorImpl, _VersionCounterProxy # noqa: F401 # pylint: disable=import-error,no-name-in-module
try:
from ._tensor_impl import TensorImpl, _VersionCounterProxy # noqa: F401 # pylint: disable=import-error,no-name-in-module
_HAS_CYTHON_TENSOR_IMPL = True
except ImportError as exc:
raise ImportError(
"Failed to import candle._cython._tensor_impl. Build the required Cython "
"runtime core with `python setup.py build_ext --inplace` or install with "
"a build that includes the compiled extensions."
) from exc

try:
from ._dispatcher_core import cy_dispatch_with_keyset_fast # noqa: F401
_HAS_CYTHON_DISPATCHER_CORE = True
except ImportError:
pass
except ImportError as exc:
raise ImportError(
"Failed to import candle._cython._dispatcher_core. Build the required Cython "
"runtime core with `python setup.py build_ext --inplace` or install with "
"a build that includes the compiled extensions."
) from exc

try:
from ._device import FastDevice # noqa: F401
Expand Down Expand Up @@ -113,48 +125,16 @@
except ImportError:
_HAS_CYTHON_AUTOGRAD_NODE = False

from ._autograd_graph import ( # noqa: F401 # pylint: disable=import-error,no-name-in-module
GradientEdge,
current_saved_tensors_hooks,
get_gradient_edge,
saved_tensors_hooks,
)
# _autograd_graph is imported directly by autograd/graph.py (avoids circular import)
_HAS_CYTHON_AUTOGRAD_GRAPH = True

from ._autograd_engine import ( # noqa: F401 # pylint: disable=import-error,no-name-in-module
_GraphTask,
_build_dependencies,
_run_backward,
backward,
current_anomaly_parent,
grad,
is_anomaly_check_nan_enabled,
is_anomaly_enabled,
is_create_graph_enabled,
pop_anomaly_config,
pop_evaluating_node,
push_anomaly_config,
push_evaluating_node,
)
# _autograd_engine is imported directly by autograd/engine.py (avoids circular import)
_HAS_CYTHON_AUTOGRAD_ENGINE = True

from ._autograd_function import ( # noqa: F401 # pylint: disable=import-error,no-name-in-module
FunctionCtx,
_function_apply,
)
# _autograd_function is imported directly by autograd/function.py (avoids circular import)
_HAS_CYTHON_AUTOGRAD_FUNCTION = True

from ._autograd_ops import ( # noqa: F401 # pylint: disable=import-error,no-name-in-module
_strip_autograd_keys,
_grad_context,
_backward_dispatch_keyset,
_autograd_unary_passthrough,
_autograd_binary,
_autograd_binary_args,
_autograd_unary_args,
_norm_extract_weight_bias,
_autograd_norm,
)
# _autograd_ops is imported directly by _backends/autograd.py (avoids circular import)
_HAS_CYTHON_AUTOGRAD_OPS = True

try:
Expand Down
24 changes: 17 additions & 7 deletions src/candle/_cython/_dispatcher_core_fallback.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
"""Pure-Python fallback for _dispatcher_core.pyx.
"""Failure stubs for dispatcher_core when the compiled extension is missing.

When Cython is not available, dispatch_with_keyset remains the original
Python implementation in dispatcher.py — this module is only imported
to satisfy the conditional import pattern.
The runtime-critical dispatcher path requires the compiled Cython core.
This module exists only to provide actionable errors if something imports the
fallback shim directly.
"""


def cy_dispatch_with_keyset_fast(name, keyset, dispatch_device, *args, **kwargs):
"""Fallback: delegate to the original Python dispatch_with_keyset."""
from candle._dispatch.dispatcher import _py_dispatch_with_keyset
return _py_dispatch_with_keyset(name, keyset, dispatch_device, *args, **kwargs)
raise ImportError(
"Failed to import candle._cython._dispatcher_core. Build the required Cython "
"runtime core with `python setup.py build_ext --inplace` or install with "
"a build that includes the compiled extensions."
)


def cy_dispatch_full(name, dispatch_device, *args, **kwargs):
raise ImportError(
"Failed to import candle._cython._dispatcher_core. Build the required Cython "
"runtime core with `python setup.py build_ext --inplace` or install with "
"a build that includes the compiled extensions."
)
11 changes: 7 additions & 4 deletions src/candle/_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,14 @@ def set_default_device(dev):


# ---------------------------------------------------------------------------
# Cython fast-path: replace device class if Cython extension is available.
# FastDevice is a drop-in replacement with int-based comparison.
# Runtime-core requires the compiled Cython FastDevice implementation.
# ---------------------------------------------------------------------------
try:
from ._cython._device import FastDevice as device # noqa: F811
_default_device = device("cpu")
except ImportError:
pass
except ImportError as exc:
raise ImportError(
"Failed to import candle._cython._device. Build the required Cython "
"runtime core with `python setup.py build_ext --inplace` or install with "
"a build that includes the compiled extensions."
) from exc
28 changes: 21 additions & 7 deletions src/candle/_dispatch/dispatcher.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
"""Dispatcher policy veneer over the canonical Cython runtime core.

This module keeps the readable Python reference implementation and the
control-plane helpers, but when the compiled dispatcher core is available the
public `dispatch()` / `dispatch_with_keyset()` entrypoints are rebound to the
Cython implementation.
"""

import inspect
import numpy as np

Expand Down Expand Up @@ -548,9 +556,6 @@ def dispatch(name, dispatch_device, *args, **kwargs):
return dispatch_with_keyset(name, keyset, dispatch_device, *args, **kwargs)


# Save original Python implementation for fallback reference
_py_dispatch_with_keyset = dispatch_with_keyset


def redispatch(name, keyset, *args, **kwargs):
return dispatch_with_keyset(name, keyset, None, *args, **kwargs)
Expand All @@ -569,7 +574,16 @@ def redispatch(name, keyset, *args, **kwargs):
# Full dispatcher core: single-function dispatch (replaces both dispatch and
# dispatch_with_keyset) — aligned with PyTorch Dispatcher::call architecture.
try:
from .._cython._dispatcher_core import cy_dispatch_full as dispatch # noqa: F811
from .._cython._dispatcher_core import cy_dispatch_with_keyset_fast as dispatch_with_keyset # noqa: F811
except ImportError:
pass # keep existing Python versions
from .._cython._dispatcher_core import (
cy_dispatch_full,
cy_dispatch_with_keyset_fast,
)
except ImportError as exc:
raise ImportError(
"Failed to import candle._cython._dispatcher_core. "
"Build the required Cython runtime core with `python setup.py build_ext --inplace` "
"or install with a build that includes the compiled extensions."
) from exc
else:
dispatch = cy_dispatch_full # noqa: F811
dispatch_with_keyset = cy_dispatch_with_keyset_fast # noqa: F811
4 changes: 3 additions & 1 deletion src/candle/_dispatch/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,9 @@ def from_tensors(cls, tensors, *, grad_enabled=False, pipeline_enabled=False, fu
# Lazy import to avoid circular imports
from .._tensor import Tensor as _BaseTensor
base_td = getattr(_BaseTensor, "__torch_dispatch__", None)
if td is not base_td:
td_func = getattr(td, "__func__", td)
base_td_func = getattr(base_td, "__func__", base_td)
if td_func is not base_td_func:
has_dispatch_subclass = True
if (not saw_device) and device is not None:
dev_type = device.type if hasattr(device, "type") else device
Expand Down
90 changes: 28 additions & 62 deletions src/candle/_dtype.py
Original file line number Diff line number Diff line change
@@ -1,49 +1,19 @@
import builtins as _builtins
import numpy as np

try:
from ._cython._dtype import FastDType as DType
except ImportError as exc:
raise ImportError(
"Failed to import candle._cython._dtype. Build the required Cython "
"runtime core with `python setup.py build_ext --inplace` or install with "
"a build that includes the compiled extensions."
) from exc

builtins_int = _builtins.int
builtins_float = _builtins.float


class DType:
def __init__(self, name, numpy_dtype, itemsize, is_floating_point=False,
is_complex=False, is_signed=True):
self.name = name
self._numpy_dtype = numpy_dtype
self.itemsize = itemsize
self._is_floating_point = is_floating_point
self._is_complex = is_complex
self._is_signed = is_signed
self._is_quantized = False

@property
def is_floating_point(self):
return self._is_floating_point

@property
def is_complex(self):
return self._is_complex

@property
def is_signed(self):
return self._is_signed

@property
def is_quantized(self):
return self._is_quantized

def __repr__(self):
return f"torch.{self.name}"

def __eq__(self, other):
if isinstance(other, DType):
return self.name == other.name
return NotImplemented

def __hash__(self):
return hash(self.name)


class _QuantizedDType(DType):
def __init__(self, name, numpy_dtype, itemsize, is_signed):
super().__init__(name, numpy_dtype, itemsize, is_signed=is_signed)
Expand All @@ -55,24 +25,24 @@ def is_signed(self):


# Floating point types
float8_e4m3fn = DType("float8_e4m3fn", np.uint8, 1, is_floating_point=True)
float8_e5m2 = DType("float8_e5m2", np.uint8, 1, is_floating_point=True)
float8_e8m0fnu = DType("float8_e8m0fnu", np.uint8, 1, is_floating_point=True)
float16 = DType("float16", np.float16, 2, is_floating_point=True)
float32 = DType("float32", np.float32, 4, is_floating_point=True)
float64 = DType("float64", np.float64, 8, is_floating_point=True)
float8_e4m3fn = DType("float8_e4m3fn", np.uint8, 1, is_floating_point=True, code=10)
float8_e5m2 = DType("float8_e5m2", np.uint8, 1, is_floating_point=True, code=11)
float8_e8m0fnu = DType("float8_e8m0fnu", np.uint8, 1, is_floating_point=True, code=12)
float16 = DType("float16", np.float16, 2, is_floating_point=True, code=1)
float32 = DType("float32", np.float32, 4, is_floating_point=True, code=0)
float64 = DType("float64", np.float64, 8, is_floating_point=True, code=2)
# bfloat16: stored as uint16 bit pattern on CPU, computed in float32
bfloat16 = DType("bfloat16", np.uint16, 2, is_floating_point=True)
bfloat16 = DType("bfloat16", np.uint16, 2, is_floating_point=True, code=3)

# Integer types
int8 = DType("int8", np.int8, 1)
int16 = DType("int16", np.int16, 2)
int32 = DType("int32", np.int32, 4)
int64 = DType("int64", np.int64, 8)
uint8 = DType("uint8", np.uint8, 1, is_signed=False)
uint16 = DType("uint16", np.uint16, 2, is_signed=False)
uint32 = DType("uint32", np.uint32, 4, is_signed=False)
uint64 = DType("uint64", np.uint64, 8, is_signed=False)
int8 = DType("int8", np.int8, 1, code=7)
int16 = DType("int16", np.int16, 2, code=6)
int32 = DType("int32", np.int32, 4, code=4)
int64 = DType("int64", np.int64, 8, code=5)
uint8 = DType("uint8", np.uint8, 1, is_signed=False, code=8)
uint16 = DType("uint16", np.uint16, 2, is_signed=False, code=13)
uint32 = DType("uint32", np.uint32, 4, is_signed=False, code=14)
uint64 = DType("uint64", np.uint64, 8, is_signed=False, code=15)

# Quantized (placeholder dtypes for compatibility)
quint8 = _QuantizedDType("quint8", np.uint8, 1, is_signed=False)
Expand All @@ -81,12 +51,12 @@ def is_signed(self):
quint4x2 = _QuantizedDType("quint4x2", np.uint8, 1, is_signed=False)

# Boolean
bool = DType("bool", np.bool_, 1, is_signed=False)
bool = DType("bool", np.bool_, 1, is_signed=False, code=9)

# Complex types
complex32 = DType("complex32", np.complex64, 8, is_complex=True)
complex64 = DType("complex64", np.complex64, 8, is_complex=True)
complex128 = DType("complex128", np.complex128, 16, is_complex=True)
complex32 = DType("complex32", np.complex64, 8, is_complex=True, code=16)
complex64 = DType("complex64", np.complex64, 8, is_complex=True, code=17)
complex128 = DType("complex128", np.complex128, 16, is_complex=True, code=18)

# Aliases (matching PyTorch)
half = float16
Expand Down Expand Up @@ -124,10 +94,6 @@ def is_signed(self):
complex32: np.complex64,
complex64: np.complex64,
complex128: np.complex128,
float16: np.float16,
float32: np.float32,
float64: np.float64,
bfloat16: np.uint16,
}

# Reverse map: numpy dtype -> DType
Expand Down
10 changes: 10 additions & 0 deletions src/candle/_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -1822,3 +1822,13 @@ def blackman_window(window_length, periodic=True, *, dtype=None, device=None):
mul(_tensor(a2, dtype=dtype, device=device), cos(t2))),
mul(_tensor(a1, dtype=dtype, device=device), cos(t1)))

# Bind hot-path functions directly to _fast_ops Cython implementations when available,
# so that F.add.__module__ == "candle._cython._fast_ops" (required by fast-ops binding tests).
try:
from ._cython._fast_ops import ( # pylint: disable=import-error,no-name-in-module
add as add, # noqa: F811
mul as mul, # noqa: F811
matmul as matmul, # noqa: F811
)
except ImportError:
pass
11 changes: 9 additions & 2 deletions src/candle/_tensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,15 @@
from .autograd.engine import backward as _backward

# TensorImpl base class: compiled Cython extension required.
from ._cython._tensor_impl import TensorImpl as _TensorBase # pylint: disable=import-error,no-name-in-module
from ._cython._tensor_impl import _VersionCounterProxy # noqa: F401 # pylint: disable=import-error,no-name-in-module
try:
from ._cython._tensor_impl import TensorImpl as _TensorBase # pylint: disable=import-error,no-name-in-module
from ._cython._tensor_impl import _VersionCounterProxy # noqa: F401 # pylint: disable=import-error,no-name-in-module
except ImportError as exc:
raise ImportError(
"Failed to import candle._cython._tensor_impl. Build the required Cython "
"runtime core with `python setup.py build_ext --inplace` or install with "
"a build that includes the compiled extensions."
) from exc


class _StrideTuple(tuple):
Expand Down
37 changes: 29 additions & 8 deletions src/candle/autograd/engine.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,38 @@
from .._cython._autograd_engine import ( # pylint: disable=import-error,no-name-in-module
_GraphTask,
_build_dependencies,
_run_backward,
backward,
grad,
is_create_graph_enabled,
)
try:
from .._cython._autograd_engine import ( # pylint: disable=import-error,no-name-in-module
_GraphTask,
_build_dependencies,
_run_backward,
backward,
current_anomaly_parent,
grad,
is_anomaly_check_nan_enabled,
is_anomaly_enabled,
is_create_graph_enabled,
pop_anomaly_config,
pop_evaluating_node,
push_anomaly_config,
push_evaluating_node,
)
except ImportError as exc:
raise ImportError(
"Failed to import candle._cython._autograd_engine. Build the required Cython "
"runtime core with `python setup.py build_ext --inplace` or install with "
"a build that includes the compiled extensions."
) from exc

__all__ = [
"_GraphTask",
"_build_dependencies",
"_run_backward",
"backward",
"current_anomaly_parent",
"grad",
"is_anomaly_check_nan_enabled",
"is_anomaly_enabled",
"is_create_graph_enabled",
"pop_anomaly_config",
"pop_evaluating_node",
"push_anomaly_config",
"push_evaluating_node",
]
Loading
Loading