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
71 changes: 14 additions & 57 deletions src/candle/_backends/npu/ops/math.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ def add(a, b):
if isinstance(b, (int, float)):
b = _scalar_to_npu_tensor(b, a)
if _HAS_FAST_ADD:
return _fast_add_impl(a, b)
try:
from candle.profiler.profiler import is_profiler_enabled
if not is_profiler_enabled():
return _fast_add_impl(a, b)
except Exception:
return _fast_add_impl(a, b)
return _binary_op(a, b, aclnn.add, "add")


Expand Down Expand Up @@ -287,67 +292,19 @@ def isinf(a):


def isnan(a):
runtime = npu_runtime.get_runtime((a.device.index or 0))
stream = npu_state.current_stream((a.device.index or 0))
from .comparison import ne

if a.device.type != "npu":
raise ValueError("NPU isnan expects NPU tensors")
out_shape = a.shape
out_stride = npu_runtime._contiguous_stride(out_shape)
out_size = _numel(out_shape) * _dtype_itemsize(bool_dtype)
out_ptr = npu_runtime._alloc_device(out_size, runtime=runtime)
if not a.dtype.is_floating_point:
aclnn.logical_not(
_unwrap_storage(isfinite(a)).data_ptr(),
out_ptr,
out_shape,
out_stride,
bool_dtype,
runtime,
stream=stream.stream,
)
runtime = npu_runtime.get_runtime((a.device.index or 0))
out_shape = a.shape
out_stride = npu_runtime._contiguous_stride(out_shape)
out_size = _numel(out_shape) * _dtype_itemsize(bool_dtype)
out_ptr = npu_runtime._alloc_device(out_size, runtime=runtime)
out_storage = npu_typed_storage_from_ptr(out_ptr, _numel(out_shape), bool_dtype, device=a.device)
return _wrap_tensor(out_storage, out_shape, out_stride)
if not (aclnn.logical_not_symbols_ok() and aclnn.logical_and_symbols_ok()):
raise RuntimeError("aclnn logical ops missing for isnan")
finite = isfinite(a)
recip = pow(a, -1.0)
recip_finite = isfinite(recip)
tmp_ptr = npu_runtime._alloc_device(out_size, runtime=runtime)
aclnn.logical_not(
_unwrap_storage(finite).data_ptr(),
tmp_ptr,
out_shape,
out_stride,
bool_dtype,
runtime,
stream=stream.stream,
)
aclnn.logical_not(
_unwrap_storage(recip_finite).data_ptr(),
out_ptr,
out_shape,
out_stride,
bool_dtype,
runtime,
stream=stream.stream,
)
aclnn.logical_and(
tmp_ptr,
out_ptr,
out_ptr,
out_shape,
out_stride,
out_shape,
out_stride,
out_shape,
out_stride,
bool_dtype,
runtime,
stream=stream.stream,
)
runtime.defer_free(tmp_ptr)
out_storage = npu_typed_storage_from_ptr(out_ptr, _numel(out_shape), bool_dtype, device=a.device)
return _wrap_tensor(out_storage, out_shape, out_stride)
return ne(a, a)


def isposinf(a):
Expand Down
133 changes: 97 additions & 36 deletions src/candle/_cython/_aclnn_ffi.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,42 @@ _op_cache = {}

cdef dict _executor_cleanup = {}

# ---------------------------------------------------------------------------
# Pending executor CANN-destroy list.
# Executors are created by GetWorkspaceSize and must eventually be returned
# to the CANN pool via aclDestroyAclOpExecutor. Destroying them immediately
# after Execute segfaults (async kernel still references the executor).
# Instead, we collect handles here and destroy them in bulk once the stream
# has been synchronised (safe because all async work has completed).
# ---------------------------------------------------------------------------

cdef list _pending_executor_destroys = []


def _defer_cann_executor_destroy(uintptr_t handle):
"""Record an executor handle for later aclDestroyAclOpExecutor."""
if handle != 0:
_pending_executor_destroys.append(int(handle))


def flush_pending_executor_destroys():
"""Destroy all pending executors via aclDestroyAclOpExecutor.

MUST only be called after the NPU stream is synchronised so that
no async kernel still references any of these executors.
"""
global _pending_executor_destroys
if not _pending_executor_destroys:
return
cdef list batch = _pending_executor_destroys
_pending_executor_destroys = []
cdef uintptr_t h
for h_int in batch:
h = <uintptr_t>h_int
if h != 0 and _fn_destroy_executor != NULL:
with nogil:
_fn_destroy_executor(<void*>h)

# ---------------------------------------------------------------------------
# Tensor descriptor cache — reuse aclTensor handles for input tensors
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -638,11 +674,11 @@ def destroy_int_array(uintptr_t handle):
def destroy_executor(uintptr_t handle):
if handle == 0:
return 0
cdef int32_t ret
with nogil:
ret = _fn_destroy_executor(<void*>handle)
# torch_npu never calls aclDestroyAclOpExecutor — the CANN runtime
# manages executor lifetime internally via PTA cache. Only clean up
# the associated tensor/scalar/array handles that Candle created.
_release_executor_cleanup(handle)
return ret
return 0

# ---------------------------------------------------------------------------
# Op symbol resolution
Expand Down Expand Up @@ -802,24 +838,27 @@ def binary_op_with_alpha(
cdef uint64_t ws_size = 0
cdef void* executor = NULL
cdef int32_t ret
cdef list cleanup_list

# Input tensors: use descriptor cache (skips aclCreateTensor on cache hit)
self_t = <void*><uintptr_t>_tensor_desc_cache.get_or_create(
<int64_t>self_ptr,
tuple(self_shape[:self_ndim]), tuple(self_stride[:self_ndim]),
dtype_code, fmt)
other_t = <void*><uintptr_t>_tensor_desc_cache.get_or_create(
<int64_t>other_ptr,
tuple(other_shape[:other_ndim]), tuple(other_stride[:other_ndim]),
dtype_code, fmt)
# Output tensor: always create fresh (new device ptr each op)
# torch_npu alignment: create ALL tensor handles fresh per-op.
# torch_npu's ConvertTypes creates new aclTensor* each call and
# ReleaseConvertTypes destroys them all after Execute.
with nogil:
self_t = _fast_create_tensor(
s_shape, s_stride, <uint64_t>self_ndim,
dtype_code, fmt, <void*>self_ptr)
other_t = _fast_create_tensor(
o_shape, o_stride, <uint64_t>other_ndim,
dtype_code, fmt, <void*>other_ptr)
out_t = _fast_create_tensor(
r_shape, r_stride, <uint64_t>out_ndim,
dtype_code, fmt, <void*>out_ptr)

if self_t == NULL or other_t == NULL or out_t == NULL:
if out_t != NULL: _fast_destroy_tensor(out_t)
with nogil:
if self_t != NULL: _fast_destroy_tensor(self_t)
if other_t != NULL: _fast_destroy_tensor(other_t)
if out_t != NULL: _fast_destroy_tensor(out_t)
raise RuntimeError("aclCreateTensor returned null")

try:
Expand All @@ -830,11 +869,18 @@ def binary_op_with_alpha(
&ws_size, &executor)
if ret != 0:
raise RuntimeError(f"GetWorkspaceSize failed: {ret}")
# Only out_t goes into executor cleanup — self_t and other_t are owned by cache
_register_executor_cleanup(
<uintptr_t>executor,
([('t', <uintptr_t>out_t)] if out_t != NULL else []),
)
# torch_npu alignment: register ALL tensor handles for cleanup
# (matching ReleaseConvertTypes which destroys everything after Execute)
cleanup_list = []
if self_t != NULL:
cleanup_list.append(('t', <uintptr_t>self_t))
if other_t != NULL:
cleanup_list.append(('t', <uintptr_t>other_t))
if out_t != NULL:
cleanup_list.append(('t', <uintptr_t>out_t))
_register_executor_cleanup(<uintptr_t>executor, cleanup_list)
self_t = NULL
other_t = NULL
out_t = NULL

# Fast path: no workspace needed, execute immediately
Expand All @@ -855,6 +901,10 @@ def binary_op_with_alpha(
return (ws_size, <uintptr_t>executor)
finally:
with nogil:
if self_t != NULL:
_fast_destroy_tensor(self_t)
if other_t != NULL:
_fast_destroy_tensor(other_t)
if out_t != NULL:
_fast_destroy_tensor(out_t)

Expand Down Expand Up @@ -898,24 +948,25 @@ def binary_op_no_alpha(
cdef uint64_t ws_size = 0
cdef void* executor = NULL
cdef int32_t ret
cdef list cleanup_list_na

# Input tensors: use descriptor cache (skips aclCreateTensor on cache hit)
self_t = <void*><uintptr_t>_tensor_desc_cache.get_or_create(
<int64_t>self_ptr,
tuple(self_shape[:self_ndim]), tuple(self_stride[:self_ndim]),
dtype_code, fmt)
other_t = <void*><uintptr_t>_tensor_desc_cache.get_or_create(
<int64_t>other_ptr,
tuple(other_shape[:other_ndim]), tuple(other_stride[:other_ndim]),
dtype_code, fmt)
# Output tensor: always create fresh (new device ptr each op)
# torch_npu alignment: create ALL tensor handles fresh per-op.
with nogil:
self_t = _fast_create_tensor(
s_shape, s_stride, <uint64_t>self_ndim,
dtype_code, fmt, <void*>self_ptr)
other_t = _fast_create_tensor(
o_shape, o_stride, <uint64_t>other_ndim,
dtype_code, fmt, <void*>other_ptr)
out_t = _fast_create_tensor(
r_shape, r_stride, <uint64_t>out_ndim,
dtype_code, fmt, <void*>out_ptr)

if self_t == NULL or other_t == NULL or out_t == NULL:
if out_t != NULL: _fast_destroy_tensor(out_t)
with nogil:
if self_t != NULL: _fast_destroy_tensor(self_t)
if other_t != NULL: _fast_destroy_tensor(other_t)
if out_t != NULL: _fast_destroy_tensor(out_t)
raise RuntimeError("aclCreateTensor returned null")

try:
Expand All @@ -926,11 +977,17 @@ def binary_op_no_alpha(
&ws_size, &executor)
if ret != 0:
raise RuntimeError(f"GetWorkspaceSize failed: {ret}")
# Only out_t goes into executor cleanup — self_t and other_t are owned by cache
_register_executor_cleanup(
<uintptr_t>executor,
([('t', <uintptr_t>out_t)] if out_t != NULL else []),
)
# torch_npu alignment: register ALL tensor handles for cleanup
cleanup_list_na = []
if self_t != NULL:
cleanup_list_na.append(('t', <uintptr_t>self_t))
if other_t != NULL:
cleanup_list_na.append(('t', <uintptr_t>other_t))
if out_t != NULL:
cleanup_list_na.append(('t', <uintptr_t>out_t))
_register_executor_cleanup(<uintptr_t>executor, cleanup_list_na)
self_t = NULL
other_t = NULL
out_t = NULL

if ws_size == 0:
Expand All @@ -950,6 +1007,10 @@ def binary_op_no_alpha(
return (ws_size, <uintptr_t>executor)
finally:
with nogil:
if self_t != NULL:
_fast_destroy_tensor(self_t)
if other_t != NULL:
_fast_destroy_tensor(other_t)
if out_t != NULL:
_fast_destroy_tensor(out_t)

Expand Down
24 changes: 20 additions & 4 deletions src/candle/_cython/_tensor_api.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -2163,14 +2163,30 @@ def tensor_hardtanh_method(self, min_val=-1.0, max_val=1.0):
return _dispatch_fn("hardtanh", self.device.type, self, min_val, max_val)


def tensor_min_method(self, other):
def tensor_min_method(self, dim=None, keepdim=False):
cdef object amin_dispatch_fn, min_dispatch_fn
_ensure_base()
_ensure_dispatch_ref()
return _dispatch_fn("min", self.device.type, self, other)
if dim is None:
from candle._functional import amin as amin_dispatch
return amin_dispatch(self)
if isinstance(dim, _BaseTensor):
from candle._functional import min as min_dispatch
return min_dispatch(self, dim)
return _dispatch_fn("min", self.device.type, self, dim, keepdim)


def tensor_max_method(self, other):
def tensor_max_method(self, dim=None, keepdim=False):
cdef object amax_dispatch_fn, max_dispatch_fn
_ensure_base()
_ensure_dispatch_ref()
return _dispatch_fn("max", self.device.type, self, other)
if dim is None:
from candle._functional import amax as amax_dispatch
return amax_dispatch(self)
if isinstance(dim, _BaseTensor):
from candle._functional import max as max_dispatch
return max_dispatch(self, dim)
return _dispatch_fn("max", self.device.type, self, dim, keepdim)


def tensor_amin_method(self, dim=None, keepdim=False):
Expand Down
8 changes: 7 additions & 1 deletion src/candle/_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,10 +218,16 @@ def _py_matmul_wrapper(*args, **kwargs):
add = _add_impl
transpose = _transpose_impl
reshape = _reshape_impl
mul = _py_mul
matmul = _matmul_impl


def mul(*args, **kwargs):
"""Dispatch mul through Cython fast path unless out= is given."""
if kwargs.get("out") is not None:
return _py_mul(*args, **kwargs)
return _mul_impl(*args, **kwargs)


def _py_relu_wrapper(*args, **kwargs):
return _relu_impl(*args, **kwargs)

Expand Down
2 changes: 0 additions & 2 deletions src/candle/_tensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2212,8 +2212,6 @@ def __hash__(self):
Tensor.as_strided_copy = _cython_mod.tensor_as_strided_copy_method
Tensor.as_strided_scatter = _cython_mod.tensor_as_strided_scatter_method
Tensor.multinomial = _cython_mod.tensor_multinomial_method
Tensor.min = _python_tensor_min
Tensor.max = _python_tensor_max
Tensor.ndim = property(_cython_mod.tensor_ndim_fget)
Tensor.T = property(_cython_mod.tensor_T_fget)
Tensor.is_floating_point = _cython_mod.tensor_is_floating_point
Expand Down
16 changes: 16 additions & 0 deletions tests/cpu/test_profiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,22 @@ def test_profiler_npu_event_device_type():
assert any(event["device_type"] == "NPU" for event in prof.events())


@pytest.mark.skipif(not torch.npu.is_available(), reason="NPU not available")
def test_profiler_npu_event_does_not_corrupt_following_functionalize_view_writeback():
x = torch.ones((2, 2), device="npu")

with torch.profiler.profile() as prof:
_ = x + x

assert any(event["device_type"] == "NPU" for event in prof.events())

base = torch.tensor([1.0, 2.0, 3.0, 4.0], device="npu")
view = base.view((2, 2))
with torch.functionalize():
view.add_(torch.ones((2, 2), device="npu"))
assert base.to("cpu").storage().data.tolist() == [2.0, 3.0, 4.0, 5.0]


def test_profiler_rejects_unknown_activity():
with pytest.raises(ValueError):
torch.profiler.profile(activities=["TPU"])
Expand Down
Loading
Loading