diff --git a/python/quadrants/lang/misc.py b/python/quadrants/lang/misc.py index 1353ec70d2..6c8ab2a359 100644 --- a/python/quadrants/lang/misc.py +++ b/python/quadrants/lang/misc.py @@ -836,13 +836,33 @@ def get_host_arch_list(): def is_extension_enabled(ext: Extension) -> bool: """ - Directly returns whether extension is enabled, without needing to - pass in current architecture. Also takes into account config, in the case - of adstack. + Directly returns whether extension is enabled on the CURRENT program, without needing to pass in the current + architecture. + + Unlike the arch-level `is_extension_supported`, this also accounts for the config (for adstack) and for the + capabilities of the initialized device: on the SPIR-V archs (vulkan, metal), extension support depends on + per-device capabilities that only the running device can report. """ arch = impl.current_cfg().arch if ext == extension.adstack: - return is_extension_supported(arch, ext) and impl.current_cfg().ad_stack_experimental_enabled + if not (is_extension_supported(arch, ext) and impl.current_cfg().ad_stack_experimental_enabled): + return False + if arch in (vulkan, metal): + # The on-device adstack size-expression interpreter reads through raw 64-bit buffer addresses, so it + # requires physical storage buffers and 64-bit integer arithmetic (see the launch gate in + # runtime/gfx/adstack_sizer_launch.cpp). + caps = impl.get_runtime().prog.get_device_caps() + return bool(caps.get(_qd_core.DeviceCapability.spirv_has_physical_storage_buffer)) and bool( + caps.get(_qd_core.DeviceCapability.spirv_has_int64) + ) + return True + if ext == extension.data64 and arch in (vulkan, metal): + # 64-bit data support on the SPIR-V archs is a property of the device, so the static arch-level table (which + # cannot advertise it) is superseded by the device capabilities. + caps = impl.get_runtime().prog.get_device_caps() + return bool(caps.get(_qd_core.DeviceCapability.spirv_has_int64)) and bool( + caps.get(_qd_core.DeviceCapability.spirv_has_float64) + ) return is_extension_supported(arch, ext) diff --git a/quadrants/codegen/spirv/spirv_codegen.cpp b/quadrants/codegen/spirv/spirv_codegen.cpp index eb509cf464..2fdb8abdf5 100644 --- a/quadrants/codegen/spirv/spirv_codegen.cpp +++ b/quadrants/codegen/spirv/spirv_codegen.cpp @@ -900,10 +900,16 @@ void TaskCodegen::visit(ExternalPtrStmt *stmt) { ir_->decorate(spv::OpDecorate, linear_offset, spv::DecorationNoSignedWrap); } } - if (caps_->get(DeviceCapability::spirv_has_physical_storage_buffer)) { + if (caps_->get(DeviceCapability::spirv_has_physical_storage_buffer) && !stmt->is_grad) { std::vector indices = arg_id; // Pick the data or gradient pointer slot of the ndarray argument struct. Without this, reverse-mode AD kernels // accumulate into x.data instead of x.grad and host-side gradients stay at zero. + // + // Gradient slots deliberately do NOT take this physical_storage_buffer path: MoltenVK misreads the second + // buffer_device_address member of the args struct (the grad pointer), so the reverse accumulate lands on a + // garbage address and host gradients stay zero. Route the gradient through the bound-storage-buffer path + // instead (the same path used on backends without physical_storage_buffer, and the reason field gradients work + // on Vulkan), which binds the grad ndarray as its own descriptor. indices.push_back(stmt->is_grad ? TypeFactory::GRAD_PTR_POS_IN_NDARRAY : TypeFactory::DATA_PTR_POS_IN_NDARRAY); spirv::Value addr_ptr = ir_->make_access_chain(ir_->get_pointer_type(ir_->u64_type(), spv::StorageClassUniform), get_buffer_value(BufferType::Args, PrimitiveType::i32), indices); diff --git a/tests/python/conftest.py b/tests/python/conftest.py index f6ab8225e5..483ecfe572 100644 --- a/tests/python/conftest.py +++ b/tests/python/conftest.py @@ -96,7 +96,7 @@ def _warmup() -> qd.i8: @pytest.fixture(autouse=True) -def wanted_arch(request, req_arch, req_options): +def wanted_arch(request, req_arch, req_options, req_extensions): if req_arch is not None: if req_arch in (qd.cuda, qd.amdgpu): if not request.node.get_closest_marker("run_in_serial"): @@ -113,6 +113,13 @@ def wanted_arch(request, req_arch, req_options): if "print_full_traceback" not in req_options: req_options["print_full_traceback"] = True qd.init(arch=req_arch, enable_fallback=False, **req_options) + + # Extension support can depend on the capabilities of the device that `qd.init` just created, so the test + # requirements resolve against the initialized program rather than a static arch table. + for ext in req_extensions: + if not qd.is_extension_enabled(ext): + qd.reset() + pytest.skip(f"Extension '{ext.name}' is unsupported by the '{req_arch.name}' device on this machine.") yield if req_arch is not None: qd.reset() @@ -122,7 +129,7 @@ def pytest_generate_tests(metafunc): if not getattr(metafunc.function, "__qd_test__", False): # For test functions not wrapped with @test_utils.test(), # fill with empty values to avoid undefined fixtures - metafunc.parametrize("req_arch,req_options", [(None, None)], ids=["none"]) + metafunc.parametrize("req_arch,req_options,req_extensions", [(None, None, ())], ids=["none"]) def _exit_marker_dir(): diff --git a/tests/python/test_ad_dataclass.py b/tests/python/test_ad_dataclass.py index 6ece3e3f78..e347bdec10 100644 --- a/tests/python/test_ad_dataclass.py +++ b/tests/python/test_ad_dataclass.py @@ -23,15 +23,12 @@ from tests import test_utils -archs_support_ndarray_ad = [qd.cpu, qd.cuda, qd.amdgpu, qd.metal] - - # ---------------------------------------------------------------------------- # qd.ndarray members # ---------------------------------------------------------------------------- -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack) +@test_utils.test(require=qd.extension.adstack) def test_ad_dataclass_ndarray_typed_annotation(): """dataclass holding qd.ndarrays, passed via typed-dataclass kernel-arg annotation.""" N = 5 @@ -65,7 +62,7 @@ def compute(s: State): np.testing.assert_allclose(a.grad.to_numpy(), b.to_numpy()) -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack) +@test_utils.test(require=qd.extension.adstack) def test_ad_dataclass_ndarray_template(): """dataclass holding qd.ndarrays, passed via qd.template().""" N = 5 @@ -143,7 +140,7 @@ def compute(s: qd.template()): # ---------------------------------------------------------------------------- -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack) +@test_utils.test(require=qd.extension.adstack) def test_ad_dataclass_tensor_ndarray_backend(): """dataclass holding qd.tensor(..., backend=NDARRAY) members; ndarray-AD via kernel.grad().""" N = 5 @@ -219,7 +216,7 @@ def compute(s: qd.template()): # ---------------------------------------------------------------------------- -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack) +@test_utils.test(require=qd.extension.adstack) def test_ad_dataclass_mixed_ndarray_and_tensor_ndarray_backend(): """Single dataclass holds one qd.ndarray member and one qd.tensor(NDARRAY) member; verify the kernel can read/write both and that gradients flow through both.""" @@ -259,7 +256,7 @@ def compute(s: State): np.testing.assert_allclose(a_tens.grad.to_numpy(), b.to_numpy()) -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack) +@test_utils.test(require=qd.extension.adstack) def test_ad_dataclass_mixed_ndarray_and_field_in_same_class(): """Single dataclass holds both a qd.ndarray member and a qd.field member. The kernel reads and writes both; differentiation is checked through the ndarray path via ``kernel.grad()`` (the field is along for the ride; its diff --git a/tests/python/test_ad_ndarray.py b/tests/python/test_ad_ndarray.py index 1f3e632c7c..47bb59e96a 100644 --- a/tests/python/test_ad_ndarray.py +++ b/tests/python/test_ad_ndarray.py @@ -5,10 +5,8 @@ from tests import test_utils -archs_support_ndarray_ad = [qd.cpu, qd.cuda, qd.amdgpu, qd.metal] - -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack) +@test_utils.test(require=qd.extension.adstack) def test_ad_sum(): N = 10 @@ -40,7 +38,7 @@ def compute_sum(a: qd.types.ndarray(), b: qd.types.ndarray(), p: qd.types.ndarra assert a.grad[i] == b[i] -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack) +@test_utils.test(require=qd.extension.adstack) def test_ad_sum_local_atomic(): N = 10 @@ -72,7 +70,7 @@ def compute_sum(a: qd.types.ndarray(), b: qd.types.ndarray(), p: qd.types.ndarra assert a.grad[i] == b[i] -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack) +@test_utils.test(require=qd.extension.adstack) def test_ad_power(): N = 10 a = qd.ndarray(qd.f32, shape=N, needs_grad=True) @@ -103,7 +101,7 @@ def power(a: qd.types.ndarray(), b: qd.types.ndarray(), p: qd.types.ndarray()): assert a.grad[i] == b[i] * 3 ** (b[i] - 1) -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack) +@test_utils.test(require=qd.extension.adstack) def test_ad_fibonacci(): N = 15 a = qd.ndarray(qd.f32, shape=N, needs_grad=True) @@ -141,7 +139,7 @@ def fib(a: qd.types.ndarray(), b: qd.types.ndarray(), c: qd.types.ndarray(), f: assert b.grad[i] == f[i] -@test_utils.test(arch=archs_support_ndarray_ad, default_fp=qd.f32, require=qd.extension.adstack) +@test_utils.test(default_fp=qd.f32, require=qd.extension.adstack) def test_ad_fibonacci_index(): N = 5 M = 10 @@ -173,7 +171,7 @@ def fib(a: qd.types.ndarray(), b: qd.types.ndarray(), f: qd.types.ndarray()): assert b[i] == is_fib * N -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack) +@test_utils.test(require=qd.extension.adstack) def test_integer_stack(): N = 5 a = qd.ndarray(qd.f32, shape=N, needs_grad=True) @@ -212,7 +210,7 @@ def int_stack(a: qd.types.ndarray(), b: qd.types.ndarray(), c: qd.types.ndarray( t = t * 10 + 1 -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack) +@test_utils.test(require=qd.extension.adstack) def test_double_for_loops(): N = 5 a = qd.ndarray(qd.f32, shape=N, needs_grad=True) @@ -250,7 +248,7 @@ def double_for(a: qd.types.ndarray(), b: qd.types.ndarray(), c: qd.types.ndarray assert b.grad[i] == 2 * i -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack) +@test_utils.test(require=qd.extension.adstack) def test_double_for_loops_more_nests(): N = 6 a = qd.ndarray(qd.f32, shape=N, needs_grad=True) @@ -296,7 +294,7 @@ def double_for(a: qd.types.ndarray(), b: qd.types.ndarray(), c: qd.types.ndarray assert b.grad[i] == total_grad_b -@test_utils.test(arch=archs_support_ndarray_ad, default_fp=qd.f64, require=[qd.extension.adstack, qd.extension.data64]) +@test_utils.test(default_fp=qd.f64, require=[qd.extension.adstack, qd.extension.data64]) def test_complex_body(): N = 5 a = qd.ndarray(qd.f32, shape=N, needs_grad=True) @@ -335,7 +333,7 @@ def complex(a: qd.types.ndarray(), c: qd.types.ndarray(), f: qd.types.ndarray(), # assert a.grad[i] == g[i] -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack) +@test_utils.test(require=qd.extension.adstack) def test_mixed_inner_loops(): x = qd.ndarray(dtype=qd.f32, shape=(1,), needs_grad=True) arr = qd.ndarray(dtype=qd.f32, shape=(5)) @@ -357,7 +355,7 @@ def mixed_inner_loops(x: qd.types.ndarray(), arr: qd.types.ndarray(), loss: qd.t assert x.grad[0] == 15.0 -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack) +@test_utils.test(require=qd.extension.adstack) def test_mixed_inner_loops_tape(): x = qd.ndarray(dtype=qd.f32, shape=(1,), needs_grad=True) arr = qd.ndarray(dtype=qd.f32, shape=(5)) @@ -377,7 +375,7 @@ def mixed_inner_loops_tape(x: qd.types.ndarray(), arr: qd.types.ndarray(), loss: assert x.grad[0] == 15.0 -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack, ad_stack_size=32) +@test_utils.test(require=qd.extension.adstack, ad_stack_size=32) def test_inner_loops_local_variable_fixed_stack_size_kernel_grad(): x = qd.ndarray(dtype=float, shape=(1), needs_grad=True) arr = qd.ndarray(dtype=float, shape=(2), needs_grad=True) @@ -403,7 +401,7 @@ def test_inner_loops_local_variable(x: qd.types.ndarray(), arr: qd.types.ndarray assert x.grad[0] == 36.0 -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack, ad_stack_size=0) +@test_utils.test(require=qd.extension.adstack, ad_stack_size=0) def test_inner_loops_local_variable_adaptive_stack_size_tape(): x = qd.ndarray(dtype=float, shape=(1), needs_grad=True) arr = qd.ndarray(dtype=float, shape=(2), needs_grad=True) @@ -428,7 +426,7 @@ def test_inner_loops_local_variable(x: qd.types.ndarray(), arr: qd.types.ndarray assert x.grad[0] == 36.0 -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack, ad_stack_size=0) +@test_utils.test(require=qd.extension.adstack, ad_stack_size=0) def test_more_inner_loops_local_variable_adaptive_stack_size_tape(): x = qd.ndarray(dtype=float, shape=(1), needs_grad=True) arr = qd.ndarray(dtype=float, shape=(2), needs_grad=True) @@ -455,7 +453,7 @@ def test_more_inner_loops_local_variable(x: qd.types.ndarray(), arr: qd.types.nd assert x.grad[0] == 36.0 -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack, ad_stack_size=32) +@test_utils.test(require=qd.extension.adstack, ad_stack_size=32) def test_more_inner_loops_local_variable_fixed_stack_size_tape(): x = qd.ndarray(dtype=float, shape=(1), needs_grad=True) arr = qd.ndarray(dtype=float, shape=(2), needs_grad=True) @@ -482,7 +480,7 @@ def test_more_inner_loops_local_variable(x: qd.types.ndarray(), arr: qd.types.nd assert x.grad[0] == 36.0 -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack, ad_stack_size=32) +@test_utils.test(require=qd.extension.adstack, ad_stack_size=32) def test_stacked_inner_loops_local_variable_fixed_stack_size_kernel_grad(): x = qd.ndarray(dtype=float, shape=(), needs_grad=True) arr = qd.ndarray(dtype=float, shape=(2), needs_grad=True) @@ -514,7 +512,7 @@ def test_stacked_inner_loops_local_variable( assert x.grad[None] == 38.0 -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack, ad_stack_size=32) +@test_utils.test(require=qd.extension.adstack, ad_stack_size=32) def test_stacked_mixed_ib_and_non_ib_inner_loops_local_variable_fixed_stack_size_kernel_grad(): x = qd.ndarray(dtype=float, shape=(), needs_grad=True) arr = qd.ndarray(dtype=float, shape=(2), needs_grad=True) @@ -547,7 +545,7 @@ def test_stacked_mixed_ib_and_non_ib_inner_loops_local_variable( assert x.grad[None] == 56.0 -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack, ad_stack_size=0) +@test_utils.test(require=qd.extension.adstack, ad_stack_size=0) def test_stacked_inner_loops_local_variable_adaptive_stack_size_kernel_grad(): x = qd.ndarray(dtype=float, shape=(), needs_grad=True) arr = qd.ndarray(dtype=float, shape=(2), needs_grad=True) @@ -580,7 +578,7 @@ def test_stacked_inner_loops_local_variable( @pytest.mark.flaky(reruns=5) -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack, ad_stack_size=0) +@test_utils.test(require=qd.extension.adstack, ad_stack_size=0) def test_stacked_mixed_ib_and_non_ib_inner_loops_local_variable_adaptive_stack_size_kernel_grad(): x = qd.ndarray(dtype=float, shape=(), needs_grad=True) arr = qd.ndarray(dtype=float, shape=(2), needs_grad=True) @@ -613,7 +611,7 @@ def test_stacked_mixed_ib_and_non_ib_inner_loops_local_variable( assert x.grad[None] == 56.0 -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack, ad_stack_size=0) +@test_utils.test(require=qd.extension.adstack, ad_stack_size=0) def test_large_for_loops_adaptive_stack_size(): x = qd.ndarray(dtype=float, shape=(), needs_grad=True) arr = qd.ndarray(dtype=float, shape=(2), needs_grad=True) @@ -633,7 +631,7 @@ def test_large_loop(x: qd.types.ndarray(), arr: qd.types.ndarray(), loss: qd.typ assert x.grad[None] == 1e7 -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack, ad_stack_size=1) +@test_utils.test(require=qd.extension.adstack, ad_stack_size=1) def test_large_for_loops_fixed_stack_size(): x = qd.ndarray(dtype=float, shape=(), needs_grad=True) arr = qd.ndarray(dtype=float, shape=(2), needs_grad=True) @@ -653,7 +651,7 @@ def test_large_loop(x: qd.types.ndarray(), arr: qd.types.ndarray(), loss: qd.typ assert x.grad[None] == 1e7 -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack) +@test_utils.test(require=qd.extension.adstack) def test_multiple_ib(): x = qd.ndarray(float, (), needs_grad=True) y = qd.ndarray(float, (), needs_grad=True) @@ -674,7 +672,7 @@ def compute_y(x: qd.types.ndarray(), y: qd.types.ndarray()): assert x.grad[None] == 12.0 -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack) +@test_utils.test(require=qd.extension.adstack) def test_multiple_ib_multiple_outermost(): x = qd.ndarray(float, (), needs_grad=True) y = qd.ndarray(float, (), needs_grad=True) @@ -700,7 +698,7 @@ def compute_y(x: qd.types.ndarray(), y: qd.types.ndarray()): assert x.grad[None] == 24.0 -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack) +@test_utils.test(require=qd.extension.adstack) def test_multiple_ib_multiple_outermost_mixed(): x = qd.ndarray(float, (), needs_grad=True) y = qd.ndarray(float, (), needs_grad=True) @@ -728,7 +726,7 @@ def compute_y(x: qd.types.ndarray(), y: qd.types.ndarray()): assert x.grad[None] == 42.0 -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack) +@test_utils.test(require=qd.extension.adstack) def test_multiple_ib_mixed(): x = qd.ndarray(float, (), needs_grad=True) y = qd.ndarray(float, (), needs_grad=True) @@ -753,7 +751,7 @@ def compute_y(x: qd.types.ndarray(), y: qd.types.ndarray()): assert x.grad[None] == 30.0 -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack) +@test_utils.test(require=qd.extension.adstack) def test_multiple_ib_deeper(): x = qd.ndarray(float, (), needs_grad=True) y = qd.ndarray(float, (), needs_grad=True) @@ -779,7 +777,7 @@ def compute_y(x: qd.types.ndarray(), y: qd.types.ndarray()): assert x.grad[None] == 42.0 -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack) +@test_utils.test(require=qd.extension.adstack) def test_multiple_ib_deeper_non_scalar(): N = 10 x = qd.ndarray(float, shape=N, needs_grad=True) @@ -808,7 +806,7 @@ def compute_y(x: qd.types.ndarray(), y: qd.types.ndarray()): assert x.grad[i] == i * 10.0 -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack) +@test_utils.test(require=qd.extension.adstack) def test_multiple_ib_inner_mixed(): x = qd.ndarray(float, (), needs_grad=True) y = qd.ndarray(float, (), needs_grad=True) @@ -838,7 +836,7 @@ def compute_y(x: qd.types.ndarray(), y: qd.types.ndarray()): assert x.grad[None] == 78.0 -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack) +@test_utils.test(require=qd.extension.adstack) def test_ib_global_load(): N = 10 a = qd.ndarray(qd.f32, shape=N, needs_grad=True) @@ -868,7 +866,7 @@ def compute(a: qd.types.ndarray(), b: qd.types.ndarray(), p: qd.types.ndarray()) assert a.grad[i] == i -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack) +@test_utils.test(require=qd.extension.adstack) def test_ad_if_simple(): x = qd.ndarray(qd.f32, shape=(), needs_grad=True) y = qd.ndarray(qd.f32, shape=(), needs_grad=True) @@ -887,7 +885,7 @@ def func(x: qd.types.ndarray(), y: qd.types.ndarray()): assert x.grad[None] == 1 -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack) +@test_utils.test(require=qd.extension.adstack) def test_ad_if(): x = qd.ndarray(qd.f32, shape=2, needs_grad=True) y = qd.ndarray(qd.f32, shape=2, needs_grad=True) @@ -913,7 +911,7 @@ def func(i: qd.i32, x: qd.types.ndarray(), y: qd.types.ndarray()): assert x.grad[1] == 1 -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack) +@test_utils.test(require=qd.extension.adstack) def test_ad_if_nested(): n = 20 x = qd.ndarray(qd.f32, shape=n, needs_grad=True) @@ -949,7 +947,7 @@ def func(x: qd.types.ndarray(), y: qd.types.ndarray(), z: qd.types.ndarray()): assert z.grad[i] == i % 4 -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack) +@test_utils.test(require=qd.extension.adstack) def test_ad_if_mutable(): x = qd.ndarray(qd.f32, shape=2, needs_grad=True) y = qd.ndarray(qd.f32, shape=2, needs_grad=True) @@ -976,7 +974,7 @@ def func(i: qd.i32, x: qd.types.ndarray(), y: qd.types.ndarray()): assert x.grad[1] == 1 -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack) +@test_utils.test(require=qd.extension.adstack) def test_ad_if_parallel(): x = qd.ndarray(qd.f32, shape=2, needs_grad=True) y = qd.ndarray(qd.f32, shape=2, needs_grad=True) @@ -1002,7 +1000,7 @@ def func(x: qd.types.ndarray(), y: qd.types.ndarray()): assert x.grad[1] == 1 -@test_utils.test(arch=archs_support_ndarray_ad, require=[qd.extension.adstack, qd.extension.data64]) +@test_utils.test(require=[qd.extension.adstack, qd.extension.data64]) def test_ad_if_parallel_f64(): x = qd.ndarray(qd.f64, shape=2, needs_grad=True) y = qd.ndarray(qd.f64, shape=2, needs_grad=True) @@ -1028,7 +1026,7 @@ def func(x: qd.types.ndarray(), y: qd.types.ndarray()): assert x.grad[1] == 1 -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack) +@test_utils.test(require=qd.extension.adstack) def test_ad_if_parallel_complex(): x = qd.ndarray(qd.f32, shape=2, needs_grad=True) y = qd.ndarray(qd.f32, shape=2, needs_grad=True) @@ -1055,13 +1053,13 @@ def func(x: qd.types.ndarray(), y: qd.types.ndarray()): @pytest.mark.flaky(retries=5) -@test_utils.test(arch=archs_support_ndarray_ad) +@test_utils.test() def test_ad_ndarray_i32(): with pytest.raises(QuadrantsRuntimeError, match=r"i32 is not supported for ndarray"): qd.ndarray(qd.i32, shape=3, needs_grad=True) -@test_utils.test(arch=archs_support_ndarray_ad) +@test_utils.test() @pytest.mark.flaky(retries=5) def test_ad_sum_vector(): N = 10 @@ -1089,7 +1087,7 @@ def compute_sum(a: qd.types.ndarray(), p: qd.types.ndarray()): assert a.grad[i][j] == 2 -@test_utils.test(arch=archs_support_ndarray_ad) +@test_utils.test() def test_ad_multiple_tapes(): N = 10 @@ -1126,7 +1124,7 @@ def compute_sum(a: qd.types.ndarray(), p: qd.types.ndarray()): assert a.grad[i][1] == 3 -@test_utils.test(arch=archs_support_ndarray_ad) +@test_utils.test() def test_ad_set_loss_grad(): x = qd.ndarray(dtype=qd.f32, shape=(), needs_grad=True) loss = qd.ndarray(dtype=qd.f32, shape=(), needs_grad=True) @@ -1157,7 +1155,7 @@ def compute_3(x: qd.types.ndarray(), loss: qd.types.ndarray()): assert x.grad[None] == 4 -@test_utils.test(arch=archs_support_ndarray_ad) +@test_utils.test() def test_grad_tensor_in_kernel(): N = 10 @@ -1177,7 +1175,7 @@ def test(x: qd.types.ndarray(), b: qd.types.ndarray()): test.grad(a, b) -@test_utils.test(arch=archs_support_ndarray_ad, require=qd.extension.adstack) +@test_utils.test(require=qd.extension.adstack) def test_ndarray_needs_grad_false(): N = 3 @@ -1200,7 +1198,7 @@ def test(x: qd.types.ndarray(needs_grad=False), y: qd.types.ndarray()): assert x.grad[i] == 0.0 -@test_utils.test(arch=archs_support_ndarray_ad) +@test_utils.test() def test_ad_vector_arg(): N = 10 @@ -1228,7 +1226,7 @@ def compute_sum(a: qd.types.ndarray(), p: qd.types.ndarray(), z: qd.math.vec2): assert a.grad[i][j] == 2 -@test_utils.test(arch=archs_support_ndarray_ad) +@test_utils.test() def test_hash_encoder_simple(): @qd.kernel def hash_encoder_kernel( diff --git a/tests/test_utils.py b/tests/test_utils.py index 6f7c7c3dde..7af9ee68a9 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -247,33 +247,32 @@ def exclude_arch_platform(arch, system, exclude): if exclude_arch_platform(req_arch, curr_system, exclude): continue - if not all(_qd_core.is_extension_supported(req_arch, e) for e in require): - continue - if qd.extension.adstack in require: options["ad_stack_experimental_enabled"] = True + # Extension support can depend on the capabilities of the device, which does not exist yet at collection + # time: the requirements are carried along the parametrization and resolved against the initialized + # program by the `wanted_arch` fixture, which skips when one is unsupported. current_options = copy.deepcopy(options) + required_extensions = list(require) for feature, param in zip(_test_features, req_params): value = param.value - required_extensions = param.required_extensions - if current_options.setdefault(feature, value) != value or any( - not _qd_core.is_extension_supported(req_arch, e) for e in required_extensions - ): + if current_options.setdefault(feature, value) != value: break - else: # no break occurs, required extensions are supported - parameters.append((req_arch, current_options)) + required_extensions.extend(param.required_extensions) + else: # no break occurs, the feature options are consistent + parameters.append((req_arch, current_options, tuple(required_extensions))) if not parameters: - marks.append(pytest.mark.skip(reason="No all required extensions are supported")) + marks.append(pytest.mark.skip(reason="No supported archs after exclusions")) else: marks.append( pytest.mark.parametrize( - "req_arch,req_options", + "req_arch,req_options,req_extensions", parameters, ids=[ f"arch={arch.name}-{i}" if len(parameters) > 1 else f"arch={arch.name}" - for i, (arch, _) in enumerate(parameters) + for i, (arch, _, _) in enumerate(parameters) ], ) )