This is an automated email from the ASF dual-hosted git repository.
tqchen pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm.git
The following commit(s) were added to refs/heads/main by this push:
new 1a764d7993 [Tests] Reduce runtime of slow Python tests (#20006)
1a764d7993 is described below
commit 1a764d799316c7d73123750a0a3f7ce40c7d5cba
Author: Shushi Hong <[email protected]>
AuthorDate: Wed Jul 15 18:07:20 2026 -0400
[Tests] Reduce runtime of slow Python tests (#20006)
This PR reduces the runtime of several slow Python test groups:
- Parameterize LLVM division and CUDA vectorized-cast cases so
pytest-xdist can schedule them independently.
- Replace exhaustive ONNX execution with structural importer checks plus
representative numerical cases, and avoid registering unsupported
backend cases.
- Reuse compiled paged-attention kernels across compatible test cases.
Targeted measurements showed:
- LLVM division: 25.34s → 19.87s
- CUDA vectorized casts: 142.85s → 108.40s
- Paged-attention CPU: 316.32s → 192.50s
- ONNX Conv: 25.84s → 1.81s
- ONNX Reduce: 20.66s → 4.04s
This PR also fixes a latent CUDA Graph cleanup bug that could leave
`cudaErrorStreamCaptureInvalidated` in the worker thread and cause
unrelated subsequent GPU tests to fail.
---
src/runtime/vm/cuda/cuda_graph_builtin.cc | 9 +-
tests/python/codegen/test_target_codegen_cuda.py | 143 +++---
tests/python/codegen/test_target_codegen_llvm.py | 107 +++--
tests/python/relax/test_frontend_onnx.py | 478 +++++++++++----------
tests/python/relax/test_frontend_onnx_backend.py | 41 +-
...runtime_builtin_paged_attention_kv_cache_cpu.py | 75 ++--
...runtime_builtin_paged_attention_kv_cache_tir.py | 83 ++--
tests/python/relax/test_vm_cuda_graph.py | 16 +
8 files changed, 563 insertions(+), 389 deletions(-)
diff --git a/src/runtime/vm/cuda/cuda_graph_builtin.cc
b/src/runtime/vm/cuda/cuda_graph_builtin.cc
index b51b287d81..75876dba97 100644
--- a/src/runtime/vm/cuda/cuda_graph_builtin.cc
+++ b/src/runtime/vm/cuda/cuda_graph_builtin.cc
@@ -123,7 +123,14 @@ class CUDACaptureStream {
TVM_FFI_CHECK_CUDA_ERROR(cudaStreamBeginCapture(capture_stream_,
cudaStreamCaptureModeGlobal));
}
~CUDACaptureStream() noexcept(false) {
- cudaStreamEndCapture(capture_stream_, output_graph_);
+ cudaError_t capture_error = cudaStreamEndCapture(capture_stream_,
output_graph_);
+ if (capture_error != cudaSuccess) {
+ // The capture may have been invalidated by the exception that is
+ // currently unwinding the stack. Do not throw a second exception from
+ // this destructor, but clear CUDA's thread-local error so that it is not
+ // reported by an unrelated CUDA call later in the same host thread.
+ cudaGetLastError();
+ }
TVM_FFI_CHECK_SAFE_CALL(TVMFFIEnvSetStream(kDLCUDA, device_id_,
prev_default_stream_, nullptr));
}
diff --git a/tests/python/codegen/test_target_codegen_cuda.py
b/tests/python/codegen/test_target_codegen_cuda.py
index a820cf5f67..8bb32f68a4 100644
--- a/tests/python/codegen/test_target_codegen_cuda.py
+++ b/tests/python/codegen/test_target_codegen_cuda.py
@@ -607,76 +607,93 @@ def test_cuda_floormod_with_vectorization():
tvm.testing.run_with_gpu_lock(run_and_check)
+_VECTORIZED_CAST_TYPES_4 = [
+ "float16",
+ "float32",
+ "int8",
+ "uint8",
+ "int16",
+ "uint16",
+ "int32",
+ "uint32",
+ "float64",
+ "int64",
+ "uint64",
+]
+_VECTORIZED_CAST_TYPES_8 = [
+ "float16",
+ "float32",
+ "int8",
+ "uint8",
+ "int16",
+ "uint16",
+ "int32",
+ "uint32",
+]
+
+
+def _skip_vectorized_cast(t0, t1):
+ if t0 == t1:
+ return True
+ # CUDA does support cast between {u}int8 and fp16.
+ skip_set = {"float16", "uint8", "int8"}
+ return t0 in skip_set and t1 in skip_set
+
+
+_VECTORIZED_CAST_CASES = [
+ (t0, t1, 4)
+ for t0 in _VECTORIZED_CAST_TYPES_4
+ for t1 in _VECTORIZED_CAST_TYPES_4
+ if not _skip_vectorized_cast(t0, t1)
+] + [
+ (t0, t1, 8)
+ for t0 in _VECTORIZED_CAST_TYPES_8
+ for t1 in _VECTORIZED_CAST_TYPES_8
+ if not _skip_vectorized_cast(t0, t1)
+]
+_VECTORIZED_CAST_CASES += [("int8", "uint8", 16), ("uint8", "int8", 16)]
+
+
@pytest.mark.gpu
@pytest.mark.skipif(not env.has_cuda(), reason="need cuda")
-def test_vectorized_casts():
- def check(t0, t1, factor):
- if (t0 == "float16" or t1 == "float16") and not
have_fp16(tvm.cuda(0).compute_version):
- print("Skip because gpu does not have fp16 support")
- return
[email protected]("t0,t1,factor", _VECTORIZED_CAST_CASES)
+def test_vectorized_casts(t0, t1, factor):
+ if (t0 == "float16" or t1 == "float16") and not
have_fp16(tvm.cuda(0).compute_version):
+ print("Skip because gpu does not have fp16 support")
+ return
- n = 128
- num_thread = n // factor
+ n = 128
+ num_thread = n // factor
- @I.ir_module(s_tir=True)
- class Module:
- @T.prim_func(s_tir=True)
- def main(A: T.Buffer((n,), t0), B: T.Buffer((n,), t1), C:
T.Buffer((n,), t0)):
- T.func_attr({"tirx.noalias": True})
- for i_0 in T.thread_binding(num_thread, thread="threadIdx.x"):
- for i_1 in T.vectorized(factor):
- with T.sblock("C"):
- v_i = T.axis.spatial(n, i_0 * factor + i_1)
- T.reads(A[v_i], B[v_i])
- T.writes(C[v_i])
- C[v_i] = A[v_i] + T.Cast(t0, B[v_i])
-
- func = tvm.compile(Module, target="cuda")
+ @I.ir_module(s_tir=True)
+ class Module:
+ @T.prim_func(s_tir=True)
+ def main(A: T.Buffer((n,), t0), B: T.Buffer((n,), t1), C:
T.Buffer((n,), t0)):
+ T.func_attr({"tirx.noalias": True})
+ for i_0 in T.thread_binding(num_thread, thread="threadIdx.x"):
+ for i_1 in T.vectorized(factor):
+ with T.sblock("C"):
+ v_i = T.axis.spatial(n, i_0 * factor + i_1)
+ T.reads(A[v_i], B[v_i])
+ T.writes(C[v_i])
+ C[v_i] = A[v_i] + T.Cast(t0, B[v_i])
- # correctness
- def run_and_check():
- dev = tvm.cuda(0)
- low, high = (0, 20) if t0.startswith("u") or t1.startswith("u")
else (-10, 10)
- a_np = np.random.randint(low, high, size=n).astype(t0)
- b_np = np.random.randint(low, high, size=n).astype(t1)
- c_np = (a_np + b_np).astype(t0)
- a_nd = tvm.runtime.tensor(a_np, dev)
- b_nd = tvm.runtime.tensor(b_np, dev)
- c_nd = tvm.runtime.tensor(np.zeros(c_np.shape, dtype=c_np.dtype),
dev)
- func(a_nd, b_nd, c_nd)
- tvm.testing.assert_allclose(c_nd.numpy(), c_np, rtol=1e-3)
+ func = tvm.compile(Module, target="cuda")
- tvm.testing.run_with_gpu_lock(run_and_check)
+ # correctness
+ def run_and_check():
+ dev = tvm.cuda(0)
+ low, high = (0, 20) if t0.startswith("u") or t1.startswith("u") else
(-10, 10)
+ a_np = np.random.randint(low, high, size=n).astype(t0)
+ b_np = np.random.randint(low, high, size=n).astype(t1)
+ c_np = (a_np + b_np).astype(t0)
+ a_nd = tvm.runtime.tensor(a_np, dev)
+ b_nd = tvm.runtime.tensor(b_np, dev)
+ c_nd = tvm.runtime.tensor(np.zeros(c_np.shape, dtype=c_np.dtype), dev)
+ func(a_nd, b_nd, c_nd)
+ tvm.testing.assert_allclose(c_nd.numpy(), c_np, rtol=1e-3)
- def skip(t0, t1):
- if t0 == t1:
- return True
- # CUDA does support cast between {u}int8 and fp16.
- skip_set = {"float16", "uint8", "int8"}
- if t0 in skip_set and t1 in skip_set:
- return True
- return False
-
- types_4 = [
- "float16",
- "float32",
- "int8",
- "uint8",
- "int16",
- "uint16",
- "int32",
- "uint32",
- "float64",
- "int64",
- "uint64",
- ]
- types_8 = ["float16", "float32", "int8", "uint8", "int16", "uint16",
"int32", "uint32"]
- for t0, t1 in [(x, y) for x in types_4 for y in types_4 if not skip(x, y)]:
- check(t0, t1, 4)
- for t0, t1 in [(x, y) for x in types_8 for y in types_8 if not skip(x, y)]:
- check(t0, t1, 8)
- check("int8", "uint8", 16)
- check("uint8", "int8", 16)
+ tvm.testing.run_with_gpu_lock(run_and_check)
def sched(compute_fn, dtype, n=128):
diff --git a/tests/python/codegen/test_target_codegen_llvm.py
b/tests/python/codegen/test_target_codegen_llvm.py
index 4f67f1803c..9de6b73bb1 100644
--- a/tests/python/codegen/test_target_codegen_llvm.py
+++ b/tests/python/codegen/test_target_codegen_llvm.py
@@ -558,8 +558,74 @@ def test_alignment():
assert has_call_to_assume()
+def _llvm_div_cases():
+ dividend_ranges = [
+ (-12, -12),
+ (-11, -1),
+ (-11, 0),
+ (0, 0),
+ (12, 12),
+ (1, 11),
+ (0, 11),
+ (-11, 11),
+ ]
+ divisor_ranges = [
+ (-11, -1),
+ (-11, 1),
+ (-4, -4),
+ (-2, -2),
+ (1, 11),
+ (0, 11),
+ (4, 4),
+ (2, 2),
+ (-11, 11),
+ ]
+
+ for start, end in dividend_ranges:
+ for dstart, dend in divisor_ranges:
+ for dtype in ["int32", "int8"]:
+ for floor_div in [False, True]:
+ yield pytest.param(
+ start,
+ end,
+ dstart,
+ dend,
+ dtype,
+ floor_div,
+ id=f"{dtype}-{'floor' if floor_div else
'trunc'}-{start}:{end}-{dstart}:{dend}",
+ )
+ if start >= 0 and dstart >= 0:
+ for floor_div in [False, True]:
+ yield pytest.param(
+ start,
+ end,
+ dstart,
+ dend,
+ "uint32",
+ floor_div,
+ id=f"uint32-{'floor' if floor_div else
'trunc'}-{start}:{end}-{dstart}:{dend}",
+ )
+
+ for dstart, dend in [(0, 11), (1, 11), (2, 2), (4, 4)]:
+ for start, end in [(123, 133), (0, 255)]:
+ for floor_div in [False, True]:
+ yield pytest.param(
+ start,
+ end,
+ dstart,
+ dend,
+ "uint8",
+ floor_div,
+ id=f"uint8-{'floor' if floor_div else
'trunc'}-{start}:{end}-{dstart}:{dend}",
+ )
+
+
@pytest.mark.skipif(not env.has_llvm(), reason="need llvm")
-def test_llvm_div():
[email protected](
+ "start,end,dstart,dend,dtype,floor_div",
+ list(_llvm_div_cases()),
+)
+def test_llvm_div(start, end, dstart, dend, dtype, floor_div):
"""Check that the semantics of div and mod is correct"""
def check(start, end, dstart, dend, dtype, floor_div=False):
@@ -658,44 +724,7 @@ def test_llvm_div():
f"but should be {mref}"
)
- # Try different ranges to cover different cases
- for start, end in [
- (-12, -12),
- (-11, -1),
- (-11, 0),
- (0, 0),
- (12, 12),
- (1, 11),
- (0, 11),
- (-11, 11),
- ]:
- for dstart, dend in [
- (-11, -1),
- (-11, 1),
- (-4, -4),
- (-2, -2),
- (1, 11),
- (0, 11),
- (4, 4),
- (2, 2),
- (-11, 11),
- ]:
- if end < start or dend < dstart or (dend == 0 and dstart == 0) or
dend == 0:
- continue
- check(start, end, dstart, dend, "int32", floor_div=False)
- check(start, end, dstart, dend, "int32", floor_div=True)
- check(start, end, dstart, dend, "int8", floor_div=False)
- check(start, end, dstart, dend, "int8", floor_div=True)
- if start >= 0 and dstart >= 0:
- check(start, end, dstart, dend, "uint32", floor_div=False)
- check(start, end, dstart, dend, "uint32", floor_div=True)
-
- # Additional tests for uint8
- for dstart, dend in [(0, 11), (1, 11), (2, 2), (4, 4)]:
- check(123, 133, dstart, dend, "uint8", floor_div=False)
- check(123, 133, dstart, dend, "uint8", floor_div=True)
- check(0, 255, dstart, dend, "uint8", floor_div=False)
- check(0, 255, dstart, dend, "uint8", floor_div=True)
+ check(start, end, dstart, dend, dtype, floor_div)
@pytest.mark.skipif(not env.has_llvm(), reason="need llvm")
diff --git a/tests/python/relax/test_frontend_onnx.py
b/tests/python/relax/test_frontend_onnx.py
index 84c3260bf6..7269594104 100644
--- a/tests/python/relax/test_frontend_onnx.py
+++ b/tests/python/relax/test_frontend_onnx.py
@@ -3494,84 +3494,129 @@ def test_shrink():
tvm.ir.assert_structural_equal(tvm_model, ExpectedCustom)
[email protected]("stride", [1, 2])
[email protected]("dilation", [1, 2])
+def _make_conv_model(input_shape, weight_shape, stride, dilation, pad, bias,
auto_pad):
+ nd = len(weight_shape) - 2
+ groups = input_shape[1] // weight_shape[1]
+ node_attrs = {
+ "strides": [stride] * nd,
+ "dilations": [dilation] * nd,
+ "group": groups,
+ }
+ if auto_pad == "VALID":
+ output_shape = [input_shape[0], weight_shape[0]] + [
+ (input_shape[i] - dilation * (weight_shape[i] - 1) - 1) // stride
+ 1
+ for i in range(2, len(input_shape))
+ ]
+ node_attrs["auto_pad"] = auto_pad
+ elif auto_pad in ("SAME_UPPER", "SAME_LOWER"):
+ output_shape = [input_shape[0], weight_shape[0]] + [
+ (input_shape[i] + stride - 1) // stride for i in range(2,
len(input_shape))
+ ]
+ node_attrs["auto_pad"] = auto_pad
+ else:
+ output_shape = [input_shape[0], weight_shape[0]] + [
+ (input_shape[i] + 2 * pad - dilation * (weight_shape[i] - 1) - 1)
// stride + 1
+ for i in range(2, len(input_shape))
+ ]
+ node_attrs["pads"] = [pad] * nd * 2
+
+ conv_node = helper.make_node(
+ "Conv",
+ inputs=["x", "w"] + (["b"] if bias else []),
+ outputs=["y"],
+ **node_attrs,
+ )
+ graph = helper.make_graph(
+ [conv_node],
+ "conv_test",
+ inputs=[
+ helper.make_tensor_value_info("x", TensorProto.FLOAT, input_shape),
+ helper.make_tensor_value_info("w", TensorProto.FLOAT,
weight_shape),
+ ]
+ + (
+ [helper.make_tensor_value_info("b", TensorProto.FLOAT,
[output_shape[1]])]
+ if bias
+ else []
+ ),
+ outputs=[helper.make_tensor_value_info("y", TensorProto.FLOAT,
output_shape)],
+ )
+ model = helper.make_model(
+ graph,
+ producer_name="conv_test",
+ opset_imports=[helper.make_opsetid("", 14)],
+ )
+ return model, output_shape, groups
+
+
+CONV_IMPORT_CONFIGS = [
+ *(("VALID", stride, dilation, 0) for stride in [1, 2] for dilation in [1,
2]),
+ *((auto_pad, stride, 1, 0) for auto_pad in ["SAME_UPPER", "SAME_LOWER"]
for stride in [1, 2]),
+ *(
+ ("NOTSET", stride, dilation, pad)
+ for stride in [1, 2]
+ for dilation in [1, 2]
+ for pad in [0, 2]
+ ),
+]
+
+
+def _verify_conv_import(auto_pad, stride, dilation, pad, bias, nd, groups):
+ input_shape = [1, 4] + [8] * nd
+ weight_shape = [4, 4 // groups] + [3] * nd
+ model, output_shape, expected_groups = _make_conv_model(
+ input_shape, weight_shape, stride, dilation, pad, bias, auto_pad
+ )
+ tvm_model = from_onnx(model, opset=14, keep_params_in_input=True)
+ func = tvm_model["main"]
+
+ conv_op_name = f"relax.nn.conv{nd}d"
+ conv_calls = []
+
+ def visit(expr):
+ if (
+ isinstance(expr, relax.Call)
+ and isinstance(expr.op, tvm.ir.Op)
+ and expr.op.name == conv_op_name
+ ):
+ conv_calls.append(expr)
+
+ relax.analysis.post_order_visit(func.body, visit)
+ assert len(conv_calls) == 1
+ conv_call = conv_calls[0]
+ assert tuple(int(value) for value in func.ret_ty.shape.values) ==
tuple(output_shape)
+ assert tuple(int(value) for value in conv_call.attrs.strides) == (stride,)
* nd
+ assert tuple(int(value) for value in conv_call.attrs.dilation) ==
(dilation,) * nd
+ assert int(conv_call.attrs.groups) == expected_groups
+ assert ("relax.add" in collect_relax_call_ops(func)) == bias
+
+ expected_padding = (pad,) * (nd * 2) if auto_pad == "NOTSET" else (0,) *
(nd * 2)
+ assert tuple(int(value) for value in conv_call.attrs.padding) ==
expected_padding
+
+
@pytest.mark.parametrize("bias", [True, False])
[email protected]("pad", [0, 2])
[email protected]("auto_pad", ["SAME_UPPER", "SAME_LOWER", "VALID"])
-def test_conv(stride: int, dilation: int, pad: int, bias: bool, auto_pad: str):
- def _verify_conv(input_shape, weight_shape):
- nd = len(weight_shape) - 2
- if auto_pad == "VALID":
- output_shape = [input_shape[0], weight_shape[0]] + [
- (input_shape[i] - dilation * (weight_shape[i] - 1) - 1) //
stride + 1
- for i in range(2, len(input_shape))
- ]
- bias_shape = [output_shape[1]]
- conv_node = helper.make_node(
- "Conv",
- inputs=["x", "w"] + (["b"] if bias else []),
- outputs=["y"],
- strides=[stride] * nd,
- dilations=[dilation] * nd,
- auto_pad=auto_pad,
- group=input_shape[1] // weight_shape[1],
- )
- elif auto_pad in ("SAME_UPPER", "SAME_LOWER"):
- if dilation == 2:
- # auto_pad = "SAME" and dilation = 2 is not supported in ONNX
- return
- output_shape = [input_shape[0], weight_shape[0]] + [
- (input_shape[i] + stride - 1) // stride for i in range(2,
len(input_shape))
- ]
- bias_shape = [output_shape[1]]
- conv_node = helper.make_node(
- "Conv",
- inputs=["x", "w"] + (["b"] if bias else []),
- outputs=["y"],
- strides=[stride] * nd,
- dilations=[dilation] * nd,
- auto_pad=auto_pad,
- group=input_shape[1] // weight_shape[1],
- )
- else:
- output_shape = [input_shape[0], weight_shape[0]] + [
- (input_shape[i] + 2 * pad - dilation * (weight_shape[i] - 1) -
1) // stride + 1
- for i in range(2, len(input_shape))
- ]
- bias_shape = [output_shape[1]]
- conv_node = helper.make_node(
- "Conv",
- inputs=["x", "w"] + (["b"] if bias else []),
- outputs=["y"],
- strides=[stride] * nd,
- dilations=[dilation] * nd,
- pads=[pad] * nd * 2,
- group=input_shape[1] // weight_shape[1],
- )
- graph = helper.make_graph(
- [conv_node],
- "conv_test",
- inputs=[
- helper.make_tensor_value_info("x", TensorProto.FLOAT,
input_shape),
- helper.make_tensor_value_info("w", TensorProto.FLOAT,
weight_shape),
- ]
- + ([helper.make_tensor_value_info("b", TensorProto.FLOAT,
bias_shape)] if bias else []),
- outputs=[helper.make_tensor_value_info("y", TensorProto.FLOAT,
output_shape)],
- )
[email protected]("nd", [1, 2, 3])
[email protected]("groups", [1, 2])
+def test_conv_import(bias, nd, groups):
+ for auto_pad, stride, dilation, pad in CONV_IMPORT_CONFIGS:
+ _verify_conv_import(auto_pad, stride, dilation, pad, bias, nd, groups)
- model = helper.make_model(graph, producer_name="conv_test")
- check_correctness(model, atol=1e-4)
- # Conv1D
- _verify_conv([3, 4, 32], [4, 4, 3])
- _verify_conv([3, 4, 32], [2, 4, 3]) # group=2
- # Conv2D
- _verify_conv([3, 4, 32, 32], [4, 4, 3, 3])
- _verify_conv([3, 4, 32, 32], [2, 4, 3, 3]) # group=2
- # Conv3D
- _verify_conv([3, 4, 32, 32, 32], [4, 4, 3, 3, 3])
- _verify_conv([3, 4, 32, 32, 32], [2, 4, 3, 3, 3]) # group=2
[email protected](
+ "nd, groups, auto_pad, stride, dilation, pad, bias",
+ [
+ (1, 1, "VALID", 1, 2, 0, False),
+ (1, 2, "NOTSET", 2, 1, 2, True),
+ (2, 1, "SAME_UPPER", 2, 1, 0, True),
+ (2, 2, "SAME_LOWER", 2, 1, 0, False),
+ (3, 2, "VALID", 2, 1, 0, True),
+ (3, 1, "NOTSET", 1, 2, 2, False),
+ ],
+)
+def test_conv_numerical(nd, groups, auto_pad, stride, dilation, pad, bias):
+ input_shape = [1, 4] + [8] * nd
+ weight_shape = [4, 4 // groups] + [3] * nd
+ model, _, _ = _make_conv_model(input_shape, weight_shape, stride,
dilation, pad, bias, auto_pad)
+ check_correctness(model, opset=14, atol=1e-4)
@pytest.mark.parametrize("stride", [2])
@@ -5144,64 +5189,97 @@ def create_composite_reduce_test_parameters_axes_attr():
return output
[email protected]("func, dynamic, opset",
create_reduce_test_parameters_axes_attr())
-def test_all_reduce_funcs_axes_attr(func, dynamic, opset):
- def verify_reduce_func(func, data, axis, keepdims):
- inshape = data.shape
- outshape = np.sum(data, axis=axis, keepdims=keepdims == 1).shape
+def _verify_reduce_numerical(
+ func: str,
+ opset: int,
+ *,
+ axes_as_input: bool,
+ axes,
+ noop_with_empty_axes: bool = False,
+ dynamic: bool = False,
+ keepdims: bool = False,
+):
+ input_shape = [3, 3, 3]
+ node_inputs = ["x"]
+ initializers = []
+ node_attrs = {"keepdims": keepdims}
- if axis:
- node = onnx.helper.make_node(
- func, inputs=["x"], outputs=["y"], axes=axis, keepdims=keepdims
+ if axes_as_input:
+ node_attrs["noop_with_empty_axes"] = noop_with_empty_axes
+ if axes is not None:
+ axes_np = np.asarray(axes, dtype=np.int64)
+ initializers.append(
+ helper.make_tensor(
+ name="reduce_axes",
+ data_type=TensorProto.INT64,
+ dims=axes_np.shape,
+ vals=axes_np,
+ )
)
- else:
- node = onnx.helper.make_node(func, inputs=["x"], outputs=["y"],
keepdims=keepdims)
-
- if dynamic:
- in_list = ["?" for _ in range(len(inshape))]
- out_list = ["?" for _ in range(len(outshape))]
- else:
- in_list = list(inshape)
- out_list = list(outshape)
- graph = helper.make_graph(
- [node],
- "reduce_test",
- inputs=[helper.make_tensor_value_info("x", TensorProto.FLOAT,
in_list)],
- outputs=[helper.make_tensor_value_info("y", TensorProto.FLOAT,
out_list)],
- )
+ node_inputs.append("reduce_axes")
+ elif axes:
+ node_attrs["axes"] = axes
- model = helper.make_model(graph, producer_name="reduce_test")
- inputs_dict = {"x": data}
- # Reduction ops accumulate arithmetic errors, so we use a higher
tolerance.
- check_correctness(model, inputs_dict, opset=opset, rtol=1e-4,
atol=1e-4)
+ if axes_as_input and noop_with_empty_axes and not axes:
+ output_shape = input_shape
+ else:
+ axis = None if not axes else tuple(axes)
+ output_shape = list(np.sum(np.empty(input_shape), axis=axis,
keepdims=keepdims).shape)
- for keepdims in [True, False]:
- verify_reduce_func(
- func, np.random.randn(3, 2, 2).astype(np.float32), axis=None,
keepdims=keepdims
- )
+ graph_input_shape = ["?"] * len(input_shape) if dynamic else input_shape
+ graph_output_shape = ["?"] * len(output_shape) if dynamic else output_shape
- verify_reduce_func(
- func, np.random.randn(3, 2, 3).astype(np.float32), axis=None,
keepdims=keepdims
- )
+ node = helper.make_node(func, inputs=node_inputs, outputs=["y"],
**node_attrs)
+ graph = helper.make_graph(
+ [node],
+ "reduce_numerical_test",
+ inputs=[helper.make_tensor_value_info("x", TensorProto.FLOAT,
graph_input_shape)],
+ initializer=initializers,
+ outputs=[helper.make_tensor_value_info("y", TensorProto.FLOAT,
graph_output_shape)],
+ )
+ model = helper.make_model(
+ graph,
+ producer_name="reduce_numerical_test",
+ opset_imports=[helper.make_opsetid("", opset)],
+ )
+ inputs = {"x": np.random.randn(*input_shape).astype(np.float32)}
+ check_correctness(model, inputs, opset=opset, rtol=1e-4, atol=1e-4)
- verify_reduce_func(
- func, np.random.randn(3, 3, 3).astype(np.float32), axis=(1,),
keepdims=keepdims
- )
- verify_reduce_func(
- func, np.random.randn(3, 3, 3, 1).astype(np.float32), axis=(1, 2),
keepdims=keepdims
- )
[email protected]("func, dynamic, opset",
create_reduce_test_parameters_axes_attr())
+def test_all_reduce_funcs_axes_attr(func, dynamic, opset):
+ for keepdims in [True, False]:
+ for input_shape, axes in REDUCE_AXES_ATTR_TEST_CASES:
+ expected = _make_reduce_expected_ir(func, input_shape, axes,
False, keepdims, dynamic)
+ verify_composite_reduce_axes_attr_ir(
+ func, input_shape, axes, keepdims, dynamic, opset, expected
+ )
- verify_reduce_func(
- func, np.random.randn(3, 3, 3, 1).astype(np.float32), axis=(1,),
keepdims=keepdims
- )
- verify_reduce_func(
- func, np.random.randn(1, 3, 4, 1).astype(np.float32), axis=(1,),
keepdims=keepdims
- )
[email protected](
+ "func, opset, dynamic, keepdims",
+ [
+ ("ReduceMax", 11, False, False),
+ ("ReduceMean", 11, False, False),
+ ("ReduceMean", 13, True, True),
+ ("ReduceMin", 11, False, False),
+ ("ReduceProd", 11, False, False),
+ ("ReduceProd", 13, False, False),
+ ("ReduceSum", 11, False, False),
+ ],
+)
+def test_reduce_funcs_axes_attr_numerical(func, opset, dynamic, keepdims):
+ _verify_reduce_numerical(
+ func,
+ opset,
+ axes_as_input=False,
+ axes=[1],
+ dynamic=dynamic,
+ keepdims=keepdims,
+ )
-def _make_composite_reduce_expected_ir(
+def _make_reduce_expected_ir(
func: str,
input_shape: list[int],
axes,
@@ -5229,8 +5307,19 @@ def _make_composite_reduce_expected_ir(
parser_vars["axes_shape"] = axes_shape
params.append(' reduce_axes: R.Tensor(axes_shape,
dtype="int64")')
+ basic_reduce_op = {
+ "ReduceMax": R.max,
+ "ReduceMean": R.mean,
+ "ReduceMin": R.min,
+ "ReduceProd": R.prod,
+ "ReduceSum": R.sum,
+ }.get(func)
+
if noop_with_empty_axes and not axes:
body = [" gv = x"]
+ elif basic_reduce_op is not None:
+ parser_vars["reduce_op"] = basic_reduce_op
+ body = [" gv = reduce_op(x, axis=axis, keepdims=keepdims)"]
elif func == "ReduceSumSquare":
body = [
" lv = R.multiply(x, x)",
@@ -5296,7 +5385,7 @@ def test_composite_reduce_funcs_axes_attr_ir():
for keepdims in [True, False]:
for dynamic in [True, False]:
for input_shape, axes in REDUCE_AXES_ATTR_TEST_CASES:
- expected = _make_composite_reduce_expected_ir(
+ expected = _make_reduce_expected_ir(
func, input_shape, axes, False, keepdims, dynamic
)
for opset in [13, 11]:
@@ -5391,107 +5480,52 @@ def verify_composite_reduce_axes_input_ir(
@pytest.mark.parametrize("func, dynamic, opset",
create_reduce_test_parameters_axes_input())
def test_all_reduce_funcs_axes_input(func, dynamic, opset):
- def verify_reduce_func(func, data, axes, keepdims,
noop_with_empty_axes=False):
- inshape = data.shape
- inputs = ["x"]
- initializers = []
-
- # Optional `axes` input
- if axes is not None:
- axes_name = "reduce_axes"
- axes_np = np.asarray(axes, dtype=np.int64)
- axes_init = helper.make_tensor(
- name=axes_name,
- data_type=TensorProto.INT64,
- dims=axes_np.shape,
- vals=axes_np,
- )
- initializers.append(axes_init)
- inputs.append(axes_name)
-
- # Determine input and output shapes
- if not axes and not noop_with_empty_axes:
- outshape = np.sum(data, axis=None, keepdims=keepdims).shape
- elif not axes and noop_with_empty_axes:
- outshape = inshape
- else:
- outshape = np.sum(data, axis=axes, keepdims=keepdims).shape
-
- if dynamic:
- in_list = ["?"] * len(inshape)
- out_list = ["?"] * len(outshape)
- else:
- in_list = list(inshape)
- out_list = list(outshape)
-
- # Make a model node
- node = helper.make_node(
- func,
- inputs=inputs,
- outputs=["y"],
- keepdims=keepdims,
- noop_with_empty_axes=noop_with_empty_axes,
- )
-
- # Make a model graph and a model
- graph = helper.make_graph(
- [node],
- "reduce18_test",
- inputs=[helper.make_tensor_value_info("x", TensorProto.FLOAT,
in_list)],
- initializer=initializers,
- outputs=[helper.make_tensor_value_info("y", TensorProto.FLOAT,
out_list)],
- )
- model = helper.make_model(graph, producer_name="reduce18_test")
-
- inputs_dict = {"x": data}
- check_correctness(model, inputs_dict, opset=opset, rtol=1e-4,
atol=1e-4)
-
- # Verify
for keepdims in [True, False]:
- # no `axes` input && `noop_with_empty_axes` = 0 -> reduce over all
dimensions.
- verify_reduce_func(
- func,
- np.random.randn(3, 2, 2).astype(np.float32),
- axes=[],
- keepdims=keepdims,
- noop_with_empty_axes=False,
- )
-
- # no `axes` input && `noop_with_empty_axes` = 0 -> reduce over all
dimensions.
- verify_reduce_func(
- func,
- np.random.randn(3, 2, 2).astype(np.float32),
- axes=None,
- keepdims=keepdims,
- noop_with_empty_axes=False,
- )
-
- # no `axes` input && `noop_with_empty_axes` = 1 -> return the input
unchanged.
- verify_reduce_func(
- func,
- np.random.randn(4, 3).astype(np.float32),
- axes=[],
- keepdims=keepdims,
- noop_with_empty_axes=True,
- )
+ for input_shape, axes, noop_with_empty_axes in
REDUCE_AXES_INPUT_TEST_CASES:
+ expected = _make_reduce_expected_ir(
+ func,
+ input_shape,
+ axes,
+ noop_with_empty_axes,
+ keepdims,
+ dynamic,
+ axes_as_input=True,
+ )
+ verify_composite_reduce_axes_input_ir(
+ func,
+ input_shape,
+ axes,
+ noop_with_empty_axes,
+ keepdims,
+ dynamic,
+ opset,
+ expected,
+ )
- # no `axes` input && `noop_with_empty_axes` = 1 -> return the input
unchanged.
- # (onnxruntime bug) Runtime error on the onnxruntime part
- # verify_reduce_func(
- # func,
- # np.random.randn(4, 3).astype(np.float32),
- # axes=None,
- # keepdims=keepdims,
- # noop_with_empty_axes=True,
- # )
- # `axes` provided -> reduce over specified axes.
- verify_reduce_func(
- func,
- np.random.randn(3, 3, 3, 1).astype(np.float32),
- axes=(1, 2),
- keepdims=keepdims,
- )
[email protected](
+ "func, opset, axes, noop_with_empty_axes, dynamic, keepdims",
+ [
+ ("ReduceMax", 18, [1], False, False, False),
+ ("ReduceMean", 18, [1], False, True, True),
+ ("ReduceMin", 18, [1], False, False, False),
+ ("ReduceProd", 18, [1], False, False, False),
+ ("ReduceSum", 13, [1], False, False, False),
+ ("ReduceSum", 13, [], True, False, False),
+ ],
+)
+def test_reduce_funcs_axes_input_numerical(
+ func, opset, axes, noop_with_empty_axes, dynamic, keepdims
+):
+ _verify_reduce_numerical(
+ func,
+ opset,
+ axes_as_input=True,
+ axes=axes,
+ noop_with_empty_axes=noop_with_empty_axes,
+ dynamic=dynamic,
+ keepdims=keepdims,
+ )
def test_composite_reduce_funcs_axes_input_ir():
@@ -5499,7 +5533,7 @@ def test_composite_reduce_funcs_axes_input_ir():
for keepdims in [True, False]:
for dynamic in [True, False]:
for input_shape, axes, noop_with_empty_axes in
REDUCE_AXES_INPUT_TEST_CASES:
- expected = _make_composite_reduce_expected_ir(
+ expected = _make_reduce_expected_ir(
func,
input_shape,
axes,
diff --git a/tests/python/relax/test_frontend_onnx_backend.py
b/tests/python/relax/test_frontend_onnx_backend.py
index 3a80473cb1..1b7526aa1f 100644
--- a/tests/python/relax/test_frontend_onnx_backend.py
+++ b/tests/python/relax/test_frontend_onnx_backend.py
@@ -29,6 +29,8 @@ semantic verification.
"""
+import re
+
import numpy as np
import pytest
@@ -119,12 +121,6 @@ class TVMRelaxBackend(Backend):
return device == "CPU"
-# ---------------------------------------------------------------------------
-# Test registration
-# ---------------------------------------------------------------------------
-
-backend_test = onnx.backend.test.BackendTest(TVMRelaxBackend, __name__)
-
# Operators where ALL ONNX node tests pass on the Relax importer.
# Each prefix covers the base test and all its variants
# (e.g. test_add, test_add_bcast, test_add_uint8).
@@ -206,7 +202,36 @@ _INCLUDE_OPS = [
"xor",
]
-for _op in _INCLUDE_OPS:
- backend_test.include(rf"^test_{_op}(?:_.*)?(?:_cpu|_cuda)$")
+# ``BackendTest.include`` registers every ONNX backend test and marks the
+# non-matches as skipped. Filter at the runner's registration hook instead so
+# CI only generates allowlisted cases for devices the backend supports.
+_INCLUDE_PATTERN = re.compile(
+ rf"^test_(?:{'|'.join(map(re.escape,
_INCLUDE_OPS))})(?:_.*)?(?:_cpu|_cuda)$"
+)
+
+
+class _AllowlistedBackendTest(onnx.backend.test.BackendTest):
+ """Register only allowlisted, supported variants with the ONNX test
runner."""
+
+ def _add_test(
+ self,
+ category,
+ test_name,
+ test_func,
+ report_item,
+ devices=("CPU", "CUDA"),
+ **kwargs,
+ ):
+ devices = [
+ device
+ for device in devices
+ if self.backend.supports_device(device)
+ and _INCLUDE_PATTERN.search(f"{test_name}_{device.lower()}")
+ ]
+ if devices:
+ super()._add_test(category, test_name, test_func, report_item,
devices, **kwargs)
+
+
+backend_test = _AllowlistedBackendTest(TVMRelaxBackend, __name__)
globals().update(backend_test.test_cases)
diff --git
a/tests/python/relax/test_runtime_builtin_paged_attention_kv_cache_cpu.py
b/tests/python/relax/test_runtime_builtin_paged_attention_kv_cache_cpu.py
index 57f4061d96..dfd0135178 100644
--- a/tests/python/relax/test_runtime_builtin_paged_attention_kv_cache_cpu.py
+++ b/tests/python/relax/test_runtime_builtin_paged_attention_kv_cache_cpu.py
@@ -17,6 +17,7 @@
# ruff: noqa: E501, E741
import enum
import itertools
+import json
import numpy as np
import pytest
@@ -88,6 +89,8 @@ fattention_rotary = None
fcopy_single_page = None
fcompact_copy = None
+_COMPILED_KERNEL_CACHE = {}
+
def set_global_func(head_dim, dtype):
global fclear, fadd_sequence, fremove_sequence, ffork_sequence,
fenable_sliding_window_for_seq
@@ -121,33 +124,51 @@ def set_global_func(head_dim, dtype):
fdebug_get_kv =
tvm.get_global_func("vm.builtin.attention_kv_cache_debug_get_kv")
target = tvm.target.Target.from_device(device)
- builts = []
- for tir_func in [
- _kv_cache_transpose_append(num_kv_heads, head_dim, dtype),
- _kv_cache_debug_get_kv(num_layers, num_kv_heads, head_dim, dtype),
- _attention_prefill_cpu(num_kv_heads, num_qo_heads, head_dim, dtype,
False, rope_scaling),
- _attention_decode_cpu(num_kv_heads, num_qo_heads, head_dim, dtype,
False, rope_scaling),
- _attention_prefill_cpu(num_kv_heads, num_qo_heads, head_dim, dtype,
True, rope_scaling),
- _attention_decode_cpu(num_kv_heads, num_qo_heads, head_dim, dtype,
True, rope_scaling),
- _attention_prefill_ragged_cpu(
- num_kv_heads, num_qo_heads, head_dim, head_dim, dtype, rope_scaling
- ),
- tree_attn_cpu(num_kv_heads, num_qo_heads, head_dim, dtype,
rope_scaling),
- tree_attn_with_paged_kv_cache_cpu(
- num_kv_heads, num_qo_heads, head_dim, dtype, rope_scaling
- ),
- _merge_state_inplace_cpu(dtype),
- llama_rope_with_position_map(
- rope_theta, rope_scale, head_dim, num_qo_heads, num_kv_heads,
dtype, rope_scaling
- ),
- _copy_single_page_cpu(num_kv_heads, page_size, head_dim, dtype),
- _compact_kv_copy_cpu(num_kv_heads, head_dim, dtype),
- ]:
- mod = tvm.IRModule({"main": tir_func})
- with target:
- mod = dl.ApplyDefaultSchedule(dl.gpu.Fallback())(mod)
- f = tvm.tirx.build(mod["main"], target=target)
- builts.append(f.main)
+ cache_key = (
+ str(target),
+ num_layers,
+ num_qo_heads,
+ num_kv_heads,
+ head_dim,
+ page_size,
+ dtype,
+ rope_scale,
+ rope_theta,
+ json.dumps(rope_scaling, sort_keys=True),
+ )
+ builts = _COMPILED_KERNEL_CACHE.get(cache_key)
+ if builts is None:
+ builts = []
+ for tir_func in [
+ _kv_cache_transpose_append(num_kv_heads, head_dim, dtype),
+ _kv_cache_debug_get_kv(num_layers, num_kv_heads, head_dim, dtype),
+ _attention_prefill_cpu(
+ num_kv_heads, num_qo_heads, head_dim, dtype, False,
rope_scaling
+ ),
+ _attention_decode_cpu(num_kv_heads, num_qo_heads, head_dim, dtype,
False, rope_scaling),
+ _attention_prefill_cpu(num_kv_heads, num_qo_heads, head_dim,
dtype, True, rope_scaling),
+ _attention_decode_cpu(num_kv_heads, num_qo_heads, head_dim, dtype,
True, rope_scaling),
+ _attention_prefill_ragged_cpu(
+ num_kv_heads, num_qo_heads, head_dim, head_dim, dtype,
rope_scaling
+ ),
+ tree_attn_cpu(num_kv_heads, num_qo_heads, head_dim, dtype,
rope_scaling),
+ tree_attn_with_paged_kv_cache_cpu(
+ num_kv_heads, num_qo_heads, head_dim, dtype, rope_scaling
+ ),
+ _merge_state_inplace_cpu(dtype),
+ llama_rope_with_position_map(
+ rope_theta, rope_scale, head_dim, num_qo_heads, num_kv_heads,
dtype, rope_scaling
+ ),
+ _copy_single_page_cpu(num_kv_heads, page_size, head_dim, dtype),
+ _compact_kv_copy_cpu(num_kv_heads, head_dim, dtype),
+ ]:
+ mod = tvm.IRModule({"main": tir_func})
+ with target:
+ mod = dl.ApplyDefaultSchedule(dl.gpu.Fallback())(mod)
+ f = tvm.tirx.build(mod["main"], target=target)
+ builts.append(f.main)
+ builts = tuple(builts)
+ _COMPILED_KERNEL_CACHE[cache_key] = builts
(
ftranspose_append,
diff --git
a/tests/python/relax/test_runtime_builtin_paged_attention_kv_cache_tir.py
b/tests/python/relax/test_runtime_builtin_paged_attention_kv_cache_tir.py
index c48ddeb7df..fe7fc0ff07 100644
--- a/tests/python/relax/test_runtime_builtin_paged_attention_kv_cache_tir.py
+++ b/tests/python/relax/test_runtime_builtin_paged_attention_kv_cache_tir.py
@@ -17,6 +17,7 @@
# ruff: noqa: E501, E741
import functools
import itertools
+import json
import pytest
import torch
@@ -87,6 +88,8 @@ fattention_rotary = None
fcopy_single_page = None
fcompact_copy = None
+_COMPILED_KERNEL_CACHE = {}
+
def set_global_func(head_dim, dtype, target):
global fclear, fadd_sequence, fremove_sequence, ffork_sequence,
fenable_sliding_window_for_seq
@@ -119,35 +122,57 @@ def set_global_func(head_dim, dtype, target):
fis_empty = tvm.get_global_func("vm.builtin.attention_kv_cache_empty")
fdebug_get_kv =
tvm.get_global_func("vm.builtin.attention_kv_cache_debug_get_kv")
- builts = []
- for tir_func in [
- _kv_cache_transpose_append(num_kv_heads, head_dim, dtype),
- _kv_cache_debug_get_kv(num_layers, num_kv_heads, head_dim, dtype),
- _attention_prefill(
- num_kv_heads, num_qo_heads, head_dim, dtype, False, rope_scaling,
target
- ),
- _attention_decode(num_kv_heads, num_qo_heads, head_dim, dtype, False,
rope_scaling, target),
- _attention_prefill(num_kv_heads, num_qo_heads, head_dim, dtype, True,
rope_scaling, target),
- _attention_decode(num_kv_heads, num_qo_heads, head_dim, dtype, True,
rope_scaling, target),
- _attention_prefill_ragged(
- num_kv_heads, num_qo_heads, head_dim, head_dim, dtype,
rope_scaling, target
- ),
- tree_attn(num_kv_heads, num_qo_heads, head_dim, dtype, rope_scaling,
target),
- tree_attn_with_paged_kv_cache(
- num_kv_heads, num_qo_heads, head_dim, dtype, rope_scaling, target
- ),
- _merge_state_inplace(num_qo_heads, head_dim, dtype, target),
- llama_rope_with_position_map(
- rope_theta, rope_scale, head_dim, num_qo_heads, num_kv_heads,
dtype, rope_scaling
- ),
- _copy_single_page(num_kv_heads, page_size, head_dim, dtype, target),
- _compact_kv_copy(num_kv_heads, head_dim, dtype, target),
- ]:
- mod = tvm.IRModule({"main": tir_func})
- with target:
- mod = dl.ApplyDefaultSchedule(dl.gpu.Fallback())(mod)
- f = tvm.tirx.build(mod["main"], target=target)
- builts.append(f.main)
+ cache_key = (
+ str(target),
+ num_layers,
+ num_qo_heads,
+ num_kv_heads,
+ head_dim,
+ page_size,
+ dtype,
+ rope_scale,
+ rope_theta,
+ json.dumps(rope_scaling, sort_keys=True),
+ )
+ builts = _COMPILED_KERNEL_CACHE.get(cache_key)
+ if builts is None:
+ builts = []
+ for tir_func in [
+ _kv_cache_transpose_append(num_kv_heads, head_dim, dtype),
+ _kv_cache_debug_get_kv(num_layers, num_kv_heads, head_dim, dtype),
+ _attention_prefill(
+ num_kv_heads, num_qo_heads, head_dim, dtype, False,
rope_scaling, target
+ ),
+ _attention_decode(
+ num_kv_heads, num_qo_heads, head_dim, dtype, False,
rope_scaling, target
+ ),
+ _attention_prefill(
+ num_kv_heads, num_qo_heads, head_dim, dtype, True,
rope_scaling, target
+ ),
+ _attention_decode(
+ num_kv_heads, num_qo_heads, head_dim, dtype, True,
rope_scaling, target
+ ),
+ _attention_prefill_ragged(
+ num_kv_heads, num_qo_heads, head_dim, head_dim, dtype,
rope_scaling, target
+ ),
+ tree_attn(num_kv_heads, num_qo_heads, head_dim, dtype,
rope_scaling, target),
+ tree_attn_with_paged_kv_cache(
+ num_kv_heads, num_qo_heads, head_dim, dtype, rope_scaling,
target
+ ),
+ _merge_state_inplace(num_qo_heads, head_dim, dtype, target),
+ llama_rope_with_position_map(
+ rope_theta, rope_scale, head_dim, num_qo_heads, num_kv_heads,
dtype, rope_scaling
+ ),
+ _copy_single_page(num_kv_heads, page_size, head_dim, dtype,
target),
+ _compact_kv_copy(num_kv_heads, head_dim, dtype, target),
+ ]:
+ mod = tvm.IRModule({"main": tir_func})
+ with target:
+ mod = dl.ApplyDefaultSchedule(dl.gpu.Fallback())(mod)
+ f = tvm.tirx.build(mod["main"], target=target)
+ builts.append(f.main)
+ builts = tuple(builts)
+ _COMPILED_KERNEL_CACHE[cache_key] = builts
(
ftranspose_append,
diff --git a/tests/python/relax/test_vm_cuda_graph.py
b/tests/python/relax/test_vm_cuda_graph.py
index 663578227f..a842f035dd 100644
--- a/tests/python/relax/test_vm_cuda_graph.py
+++ b/tests/python/relax/test_vm_cuda_graph.py
@@ -16,6 +16,9 @@
# under the License.
# ruff: noqa: E501
+import ctypes
+import ctypes.util
+
import numpy as np
import pytest
@@ -170,6 +173,12 @@ def test_capture_error_is_recoverable():
def run_and_check():
dev = tvm.cuda()
+ cudart_path = ctypes.util.find_library("cudart")
+ assert cudart_path is not None, "Unable to locate the CUDA runtime
library"
+ cudart = ctypes.CDLL(cudart_path)
+ cudart.cudaGetLastError.argtypes = []
+ cudart.cudaGetLastError.restype = ctypes.c_int
+ cudart.cudaGetLastError()
@tvm.register_global_func("test_vm_cuda_graph.invalid_impl_for_cudagraph",
override=True)
def invalid_impl_for_cudagraph(arg_tensor):
@@ -186,6 +195,13 @@ def test_capture_error_is_recoverable():
with pytest.raises(RuntimeError):
vm["main"](arg)
+ # cudaGetLastError is host-thread-local, so query it in the same
+ # callback that triggered the invalid capture.
+ cuda_error = cudart.cudaGetLastError()
+ assert cuda_error == 0, (
+ f"CUDA error state was not cleared after failed graph capture:
{cuda_error}"
+ )
+
tvm.testing.run_with_gpu_lock(run_and_check)