Fix CuTeDSL from_dlpack device mismatch in CUDA graph capture (v2)

Previous fix (set_device) caused 'Capture must end on the same stream'.
New fix: wrap tensor in _DLPatchTensor during graph capture, which forces
dl_device in __dlpack__ to bypass the device check without changing the stream.

This enables CUDA graph capture on all 8 GPUs, not just cuda:0.
This commit is contained in:
2026-06-03 20:54:18 +00:00
parent 87b6c9932b
commit 5c94dbbc37

View File

@@ -26,6 +26,32 @@ from dsv4.ops.layouts import (
round_up,
)
class _DLPatchTensor:
"""Wrapper that patches __dlpack__ to force dl_device during CUDA graph capture.
from_dlpack checks torch.cuda.current_device() against the tensor's device.
Inside CUDA graph capture on non-default GPUs, current_device() may not match.
This wrapper forces dl_device in __dlpack__, bypassing the device check.
DO NOT use torch.cuda.set_device() inside graph capture — it changes the stream
and causes 'Capture must end on the same stream it began on'.
"""
__slots__ = ('_t',)
def __init__(self, t):
self._t = t
def __dlpack__(self, *args, **kwargs):
kwargs['dl_device'] = (1, self._t.device.index) # kDLCUDA=1
try:
return self._t.__dlpack__(*args, **kwargs)
except TypeError:
return self._t.__dlpack__(*args, **{k: v for k, v in kwargs.items() if k != 'dl_device'})
def __dlpack_device__(self):
return (1, self._t.device.index)
def __getattr__(self, name):
return getattr(self._t, name)
# Cache compiled kernels + pre-allocated workspace by cache_key
# Each entry: {'compiled': callable, 'workspace': Tensor, 'workspace_size': int}
#
@@ -99,10 +125,12 @@ def warmup_compilation(num_experts, K_packed, N_packed, device,
)
def to_cute(t):
# Ensure current CUDA device matches tensor device (required for from_dlpack
# inside CUDA graph capture, where torch.cuda.current_device() may not match)
if t.is_cuda and t.device.index != torch.cuda.current_device():
torch.cuda.set_device(t.device.index)
# Fix: from_dlpack checks torch.cuda.current_device() against tensor device.
# Inside CUDA graph capture on non-default GPUs, current_device() may not match.
# We wrap the tensor to force dl_device in __dlpack__, bypassing the check.
# DO NOT use torch.cuda.set_device() inside graph capture — it changes the stream.
if hasattr(torch.cuda, 'is_current_stream_capturing') and torch.cuda.is_current_stream_capturing():
t = _DLPatchTensor(t)
ct = cutlass_torch.from_dlpack(t)
return ct.mark_layout_dynamic(leading_dim=cutlass_torch.get_leading_dim(t))
@@ -207,10 +235,8 @@ def run_nvfp4_grouped_gemm(
)
def to_cute(t):
# Ensure current CUDA device matches tensor device (required for from_dlpack
# inside CUDA graph capture, where torch.cuda.current_device() may not match)
if t.is_cuda and t.device.index != torch.cuda.current_device():
torch.cuda.set_device(t.device.index)
if hasattr(torch.cuda, 'is_current_stream_capturing') and torch.cuda.is_current_stream_capturing():
t = _DLPatchTensor(t)
ct = cutlass_torch.from_dlpack(t)
return ct.mark_layout_dynamic(leading_dim=cutlass_torch.get_leading_dim(t))
@@ -258,10 +284,12 @@ def run_nvfp4_grouped_gemm(
# This is cheap (metadata only, no GPU work) and avoids stale
# references to tensors from previous calls that may have been freed.
def to_cute(t):
# Ensure current CUDA device matches tensor device (required for from_dlpack
# inside CUDA graph capture, where torch.cuda.current_device() may not match)
if t.is_cuda and t.device.index != torch.cuda.current_device():
torch.cuda.set_device(t.device.index)
# Fix: from_dlpack checks torch.cuda.current_device() against tensor device.
# Inside CUDA graph capture on non-default GPUs, current_device() may not match.
# We wrap the tensor to force dl_device in __dlpack__, bypassing the check.
# DO NOT use torch.cuda.set_device() inside graph capture — it changes the stream.
if hasattr(torch.cuda, 'is_current_stream_capturing') and torch.cuda.is_current_stream_capturing():
t = _DLPatchTensor(t)
ct = cutlass_torch.from_dlpack(t)
return ct.mark_layout_dynamic(leading_dim=cutlass_torch.get_leading_dim(t))
@@ -340,10 +368,12 @@ def warmup_fused_swiglu_compilation(num_experts, K_packed, N_packed, device,
)
def to_cute(t):
# Ensure current CUDA device matches tensor device (required for from_dlpack
# inside CUDA graph capture, where torch.cuda.current_device() may not match)
if t.is_cuda and t.device.index != torch.cuda.current_device():
torch.cuda.set_device(t.device.index)
# Fix: from_dlpack checks torch.cuda.current_device() against tensor device.
# Inside CUDA graph capture on non-default GPUs, current_device() may not match.
# We wrap the tensor to force dl_device in __dlpack__, bypassing the check.
# DO NOT use torch.cuda.set_device() inside graph capture — it changes the stream.
if hasattr(torch.cuda, 'is_current_stream_capturing') and torch.cuda.is_current_stream_capturing():
t = _DLPatchTensor(t)
ct = cutlass_torch.from_dlpack(t)
return ct.mark_layout_dynamic(leading_dim=cutlass_torch.get_leading_dim(t))
@@ -441,10 +471,8 @@ def run_fused_swiglu_grouped_gemm(
)
def to_cute(t):
# Ensure current CUDA device matches tensor device (required for from_dlpack
# inside CUDA graph capture, where torch.cuda.current_device() may not match)
if t.is_cuda and t.device.index != torch.cuda.current_device():
torch.cuda.set_device(t.device.index)
if hasattr(torch.cuda, 'is_current_stream_capturing') and torch.cuda.is_current_stream_capturing():
t = _DLPatchTensor(t)
ct = cutlass_torch.from_dlpack(t)
return ct.mark_layout_dynamic(leading_dim=cutlass_torch.get_leading_dim(t))
@@ -486,10 +514,12 @@ def run_fused_swiglu_grouped_gemm(
workspace = entry['workspace']
def to_cute(t):
# Ensure current CUDA device matches tensor device (required for from_dlpack
# inside CUDA graph capture, where torch.cuda.current_device() may not match)
if t.is_cuda and t.device.index != torch.cuda.current_device():
torch.cuda.set_device(t.device.index)
# Fix: from_dlpack checks torch.cuda.current_device() against tensor device.
# Inside CUDA graph capture on non-default GPUs, current_device() may not match.
# We wrap the tensor to force dl_device in __dlpack__, bypassing the check.
# DO NOT use torch.cuda.set_device() inside graph capture — it changes the stream.
if hasattr(torch.cuda, 'is_current_stream_capturing') and torch.cuda.is_current_stream_capturing():
t = _DLPatchTensor(t)
ct = cutlass_torch.from_dlpack(t)
return ct.mark_layout_dynamic(leading_dim=cutlass_torch.get_leading_dim(t))