From f14899db7c01a52603c77885f8d5f722057113d0 Mon Sep 17 00:00:00 2001 From: James Burton Date: Fri, 31 Jul 2026 11:43:31 +0100 Subject: [PATCH] test(unit): report hardware-gated tests as skipped, not passed (#421) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 31 tests in `tests/DotLLM.Tests.Unit/Cpu/Kernels` returned early on an ISA capability check without asserting anything, so on hardware lacking the ISA they were reported as PASSED and the coverage silently evaporated. Convert them to `[SkippableFact]`/`[SkippableTheory]` + `Skip.IfNot(...)` with an explicit reason, following the convention already used in `tests/DotLLM.Tests.Unit/Cuda`. Sites deliberately left as-is, now carrying a comment saying why: - Four CUDA fixture constructors — the early return is in a constructor, not a test; each test already declares its own `Skip.IfNot(CudaDevice.IsAvailable())`. - Two `if (Avx2.IsSupported)` blocks in `DequantizeTests` and one `if (Avx512BW.IsSupported)` block in `MatMulTests` — these gate only the vector half of a test whose scalar/dispatch assertions run everywhere, so converting the whole test would discard coverage. No assertion that ran before runs any less now: on fully-capable hardware the pass counts are unchanged. Co-Authored-By: Claude Opus 5 (1M context) --- .../Cpu/Kernels/DequantizeKQuantTests.cs | 12 ++--- .../Cpu/Kernels/DequantizeTests.cs | 4 ++ .../Cpu/Kernels/KvQuantizeTests.cs | 16 +++---- .../Cpu/Kernels/MatMulKQuantTests.cs | 16 +++---- .../Cpu/Kernels/MatMulQ5_0Tests.cs | 4 +- .../Cpu/Kernels/MatMulQ8_1Tests.cs | 16 +++---- .../Cpu/Kernels/MatMulR4VnniTests.cs | 16 +++---- .../Cpu/Kernels/MatMulTests.cs | 47 ++++++++----------- .../Cpu/Kernels/OuterProductGemmTests.cs | 5 +- .../Cpu/Kernels/RoPETests.cs | 10 ++-- tests/DotLLM.Tests.Unit/Cuda/CudaGemmTest.cs | 3 ++ .../Cuda/CudaKernelComparisonTests.cs | 3 ++ .../DotLLM.Tests.Unit/Cuda/CudaKernelTests.cs | 3 ++ .../DotLLM.Tests.Unit/Cuda/CudaTensorTests.cs | 3 ++ 14 files changed, 81 insertions(+), 77 deletions(-) diff --git a/tests/DotLLM.Tests.Unit/Cpu/Kernels/DequantizeKQuantTests.cs b/tests/DotLLM.Tests.Unit/Cpu/Kernels/DequantizeKQuantTests.cs index 2651ca03..2a6bfd55 100644 --- a/tests/DotLLM.Tests.Unit/Cpu/Kernels/DequantizeKQuantTests.cs +++ b/tests/DotLLM.Tests.Unit/Cpu/Kernels/DequantizeKQuantTests.cs @@ -138,10 +138,10 @@ public void Q6_K_SingleBlock_HandCalculated() } } - [Fact] + [SkippableFact] public void Q6_K_ScalarMatchesAvx2_RandomBlocks() { - if (!Avx2.IsSupported) return; + Skip.IfNot(Avx2.IsSupported, "Requires AVX2."); const int blockCount = 8; const int totalElements = blockCount * KQuantGroupSize; @@ -237,10 +237,10 @@ public void Q4_K_SingleBlock_HandCalculated() } } - [Fact] + [SkippableFact] public void Q4_K_ScalarMatchesAvx2_RandomBlocks() { - if (!Avx2.IsSupported) return; + Skip.IfNot(Avx2.IsSupported, "Requires AVX2."); const int blockCount = 8; const int totalElements = blockCount * KQuantGroupSize; @@ -312,10 +312,10 @@ public void Q5_K_SingleBlock_HandCalculated() } } - [Fact] + [SkippableFact] public void Q5_K_ScalarMatchesAvx2_RandomBlocks() { - if (!Avx2.IsSupported) return; + Skip.IfNot(Avx2.IsSupported, "Requires AVX2."); const int blockCount = 8; const int totalElements = blockCount * KQuantGroupSize; diff --git a/tests/DotLLM.Tests.Unit/Cpu/Kernels/DequantizeTests.cs b/tests/DotLLM.Tests.Unit/Cpu/Kernels/DequantizeTests.cs index e80a3d74..429ffc01 100644 --- a/tests/DotLLM.Tests.Unit/Cpu/Kernels/DequantizeTests.cs +++ b/tests/DotLLM.Tests.Unit/Cpu/Kernels/DequantizeTests.cs @@ -217,6 +217,8 @@ public void Q8_0_ScalarMatchesSimd_RandomBlocks() Dequantize.DequantizeQ8_0Scalar(ptr, totalElements, scalarDest); + // Deliberately NOT a [SkippableFact]: the scalar and dispatch assertions below run + // on every machine, so gating only the AVX2 half keeps that coverage where AVX2 is absent. if (Avx2.IsSupported) { Dequantize.DequantizeQ8_0Avx2(ptr, totalElements, simdDest); @@ -304,6 +306,8 @@ public void Q5_0_ScalarVsAvx2_MatchOnPseudoRandomBlocks() Dequantize.DequantizeQ5_0Scalar(ptr, totalElements, scalarDest); + // Deliberately NOT a [SkippableFact]: the scalar and dispatch assertions below run + // on every machine, so gating only the AVX2 half keeps that coverage where AVX2 is absent. if (Avx2.IsSupported) { Dequantize.DequantizeQ5_0Avx2(ptr, totalElements, simdDest); diff --git a/tests/DotLLM.Tests.Unit/Cpu/Kernels/KvQuantizeTests.cs b/tests/DotLLM.Tests.Unit/Cpu/Kernels/KvQuantizeTests.cs index 8809599f..30f6413c 100644 --- a/tests/DotLLM.Tests.Unit/Cpu/Kernels/KvQuantizeTests.cs +++ b/tests/DotLLM.Tests.Unit/Cpu/Kernels/KvQuantizeTests.cs @@ -1,4 +1,5 @@ using System.Runtime.InteropServices; +using System.Runtime.Intrinsics.X86; using DotLLM.Cpu.Kernels; using Xunit; @@ -37,11 +38,10 @@ public void Q4_0_Scalar_RoundTrip_WithinTolerance() $"Max round-trip error {maxErr} exceeds expected {expectedMaxErr}"); } - [Fact] + [SkippableFact] public void Q4_0_Avx2_MatchesScalar() { - if (!System.Runtime.Intrinsics.X86.Avx2.IsSupported) - return; // Skip on non-AVX2 hardware + Skip.IfNot(Avx2.IsSupported, "Requires AVX2."); float[] input = GenerateTestData(BlockSize * 8); byte[] quantScalar = new byte[KvQuantize.Q4_0BlockBytes * 8]; @@ -77,11 +77,10 @@ public void Q4_0_ZeroInput_AllZeroOutput() Assert.Equal(0f, output[i]); } - [Fact] + [SkippableFact] public void Q4_0_Dequant_Avx2_MatchesScalar() { - if (!System.Runtime.Intrinsics.X86.Avx2.IsSupported) - return; + Skip.IfNot(Avx2.IsSupported, "Requires AVX2."); float[] input = GenerateTestData(BlockSize * 4); byte[] quantized = new byte[KvQuantize.Q4_0BlockBytes * 4]; @@ -135,11 +134,10 @@ public void Q8_0_RoundTrip_WithinTolerance() $"Max round-trip error {maxErr} exceeds expected {expectedMaxErr}"); } - [Fact] + [SkippableFact] public void Q8_0_Dequant_Avx2_MatchesScalar() { - if (!System.Runtime.Intrinsics.X86.Avx2.IsSupported) - return; + Skip.IfNot(Avx2.IsSupported, "Requires AVX2."); float[] input = GenerateTestData(BlockSize * 4); byte[] quantized = new byte[KvQuantize.Q8_0BlockBytes * 4]; diff --git a/tests/DotLLM.Tests.Unit/Cpu/Kernels/MatMulKQuantTests.cs b/tests/DotLLM.Tests.Unit/Cpu/Kernels/MatMulKQuantTests.cs index 3e23559d..a946d4c9 100644 --- a/tests/DotLLM.Tests.Unit/Cpu/Kernels/MatMulKQuantTests.cs +++ b/tests/DotLLM.Tests.Unit/Cpu/Kernels/MatMulKQuantTests.cs @@ -56,10 +56,10 @@ public void QuantizeF32ToQ8_K_Scalar_RoundtripAccuracy() } } - [Fact] + [SkippableFact] public void QuantizeF32ToQ8_K_Avx2MatchesScalar() { - if (!Avx2.IsSupported) return; + Skip.IfNot(Avx2.IsSupported, "Requires AVX2."); const int k = 512; // 2 blocks var rng = new Random(42); @@ -153,10 +153,10 @@ public void VecDotQ4_K_Q8_K_CrossVerifyAgainstDequant() } } - [Fact] + [SkippableFact] public void VecDotQ4_K_Q8_K_ScalarMatchesAvx2() { - if (!Avx2.IsSupported) return; + Skip.IfNot(Avx2.IsSupported, "Requires AVX2."); const int superBlockCount = 4; var rng = new Random(42); @@ -216,10 +216,10 @@ public void VecDotQ6_K_Q8_K_CrossVerifyAgainstDequant() } } - [Fact] + [SkippableFact] public void VecDotQ6_K_Q8_K_ScalarMatchesAvx2() { - if (!Avx2.IsSupported) return; + Skip.IfNot(Avx2.IsSupported, "Requires AVX2."); const int superBlockCount = 4; var rng = new Random(42); @@ -280,10 +280,10 @@ public void VecDotQ5_K_Q8_K_CrossVerifyAgainstDequant() } } - [Fact] + [SkippableFact] public void VecDotQ5_K_Q8_K_ScalarMatchesAvx2() { - if (!Avx2.IsSupported) return; + Skip.IfNot(Avx2.IsSupported, "Requires AVX2."); const int superBlockCount = 4; var rng = new Random(42); diff --git a/tests/DotLLM.Tests.Unit/Cpu/Kernels/MatMulQ5_0Tests.cs b/tests/DotLLM.Tests.Unit/Cpu/Kernels/MatMulQ5_0Tests.cs index 9de5c68a..affbc4de 100644 --- a/tests/DotLLM.Tests.Unit/Cpu/Kernels/MatMulQ5_0Tests.cs +++ b/tests/DotLLM.Tests.Unit/Cpu/Kernels/MatMulQ5_0Tests.cs @@ -52,13 +52,13 @@ public void VecDotQ5_0Q8_0Scalar_CrossVerifyAgainstDequant(int blockCount) // ──────────────────── Q5_0 × Q8_0 scalar matches AVX2 ──────────────────── - [Theory] + [SkippableTheory] [InlineData(1)] [InlineData(4)] [InlineData(18)] public void VecDotQ5_0Q8_0_ScalarMatchesAvx2(int blockCount) { - if (!Avx2.IsSupported) return; + Skip.IfNot(Avx2.IsSupported, "Requires AVX2."); var rng = new Random(42); nint q5Ptr = AllocRandomQ5_0Blocks(blockCount, rng); diff --git a/tests/DotLLM.Tests.Unit/Cpu/Kernels/MatMulQ8_1Tests.cs b/tests/DotLLM.Tests.Unit/Cpu/Kernels/MatMulQ8_1Tests.cs index 58dbe8fd..cdbd1046 100644 --- a/tests/DotLLM.Tests.Unit/Cpu/Kernels/MatMulQ8_1Tests.cs +++ b/tests/DotLLM.Tests.Unit/Cpu/Kernels/MatMulQ8_1Tests.cs @@ -67,13 +67,13 @@ public void QuantizeF32ToQ8_1_PrecomputedSumIsCorrect(int elementCount) // ──────────────────── Q8_1 quantization: scalar matches AVX2 ──────────────────── - [Theory] + [SkippableTheory] [InlineData(32)] [InlineData(576)] [InlineData(1536)] public void QuantizeF32ToQ8_1_ScalarMatchesAvx2(int elementCount) { - if (!Avx2.IsSupported) return; + Skip.IfNot(Avx2.IsSupported, "Requires AVX2."); var rng = new Random(42); float* src = (float*)NativeMemory.AlignedAlloc((nuint)(elementCount * sizeof(float)), 64); @@ -168,13 +168,13 @@ public void VecDotQ5_0Q8_1Scalar_MatchesQ8_0Path(int blockCount) // ──────────────────── Q5_0 × Q8_1: scalar matches AVX2 ──────────────────── - [Theory] + [SkippableTheory] [InlineData(1)] [InlineData(4)] [InlineData(18)] public void VecDotQ5_0Q8_1_ScalarMatchesAvx2(int blockCount) { - if (!Avx2.IsSupported) return; + Skip.IfNot(Avx2.IsSupported, "Requires AVX2."); var rng = new Random(42); nint q5Ptr = AllocRandomQ5_0Blocks(blockCount, rng); @@ -195,10 +195,10 @@ public void VecDotQ5_0Q8_1_ScalarMatchesAvx2(int blockCount) // ──────────────────── Q5_0 × Q8_1: 4-row matches single row ──────────────────── - [Fact] + [SkippableFact] public void VecDotQ5_0Q8_1_4Row_MatchesSingleRow() { - if (!Avx2.IsSupported) return; + Skip.IfNot(Avx2.IsSupported, "Requires AVX2."); const int blockCount = 18; var rng = new Random(42); @@ -237,14 +237,14 @@ public void VecDotQ5_0Q8_1_4Row_MatchesSingleRow() // ──────────────────── Q5_0 × Q8_1 AVX2: 2-block unroll odd/even block counts ──────────────────── - [Theory] + [SkippableTheory] [InlineData(1)] // odd, single-block tail only [InlineData(3)] // odd, 1 unrolled pair + 1 tail [InlineData(18)] // even, 9 unrolled pairs, no tail [InlineData(19)] // odd, 9 unrolled pairs + 1 tail public void VecDotQ5_0Q8_1_2BlockUnroll_OddAndEvenCounts(int blockCount) { - if (!Avx2.IsSupported) return; + Skip.IfNot(Avx2.IsSupported, "Requires AVX2."); var rng = new Random(42); nint q5Ptr = AllocRandomQ5_0Blocks(blockCount, rng); diff --git a/tests/DotLLM.Tests.Unit/Cpu/Kernels/MatMulR4VnniTests.cs b/tests/DotLLM.Tests.Unit/Cpu/Kernels/MatMulR4VnniTests.cs index 59f121f2..226b5d74 100644 --- a/tests/DotLLM.Tests.Unit/Cpu/Kernels/MatMulR4VnniTests.cs +++ b/tests/DotLLM.Tests.Unit/Cpu/Kernels/MatMulR4VnniTests.cs @@ -29,11 +29,11 @@ public sealed unsafe class MatMulR4VnniTests public static TheoryData DiscriminatingBlockCounts() => [1, 2, 3, 8, 17, 18, 48, 128]; - [Theory] + [SkippableTheory] [MemberData(nameof(DiscriminatingBlockCounts))] public void VecDotQ8_0Vnni_4RowsR4_MatchesScalarR4(int blockCount) { - if (!VnniAvailable) return; // dispatch wiring is pinned by DispatchPrefersAvx2Tier + Skip.IfNot(VnniAvailable, "Requires AVX-512BW + AVX-VNNI."); // dispatch wiring is pinned by DispatchPrefersAvx2Tier RunSingleGroup(blockCount, seed: 42, out float[] vnni, out float[] scalar, out _); @@ -41,11 +41,11 @@ public void VecDotQ8_0Vnni_4RowsR4_MatchesScalarR4(int blockCount) AssertClose(scalar[r], vnni[r], blockCount, $"row {r}"); } - [Theory] + [SkippableTheory] [MemberData(nameof(DiscriminatingBlockCounts))] public void VecDotQ8_0Vnni_4RowsR4_MatchesAvx2R4(int blockCount) { - if (!VnniAvailable) return; // dispatch wiring is pinned by DispatchPrefersAvx2Tier + Skip.IfNot(VnniAvailable, "Requires AVX-512BW + AVX-VNNI."); // dispatch wiring is pinned by DispatchPrefersAvx2Tier RunSingleGroup(blockCount, seed: 7, out float[] vnni, out _, out float[] avx2); @@ -59,11 +59,11 @@ public void VecDotQ8_0Vnni_4RowsR4_MatchesAvx2R4(int blockCount) /// untouched — which also proves the kernel reads each row from its own interleaved offset /// rather than accidentally aliasing one row four times. /// - [Theory] + [SkippableTheory] [MemberData(nameof(DiscriminatingBlockCounts))] public void VecDotQ8_0Vnni_4RowsR4_DetectsPerturbedWeight(int blockCount) { - if (!VnniAvailable) return; // dispatch wiring is pinned by DispatchPrefersAvx2Tier + Skip.IfNot(VnniAvailable, "Requires AVX-512BW + AVX-VNNI."); // dispatch wiring is pinned by DispatchPrefersAvx2Tier const int perturbedRow = 2; int k = blockCount * Q8_0GroupSize; @@ -153,11 +153,11 @@ public void ComputeRowsQ8_0Interleaved_MatchesRowMajor(int m, int blockCount) /// dispatch changed, which given the measurements would be a performance regression rather /// than a correctness one, and would otherwise go unnoticed. /// - [Theory] + [SkippableTheory] [MemberData(nameof(DiscriminatingBlockCounts))] public void ComputeRowsQ8_0Interleaved_DispatchPrefersAvx2Tier(int blockCount) { - if (!Avx2.IsSupported) return; + Skip.IfNot(Avx2.IsSupported, "Requires AVX2."); int k = blockCount * Q8_0GroupSize; diff --git a/tests/DotLLM.Tests.Unit/Cpu/Kernels/MatMulTests.cs b/tests/DotLLM.Tests.Unit/Cpu/Kernels/MatMulTests.cs index 344a94cb..35e6a894 100644 --- a/tests/DotLLM.Tests.Unit/Cpu/Kernels/MatMulTests.cs +++ b/tests/DotLLM.Tests.Unit/Cpu/Kernels/MatMulTests.cs @@ -106,11 +106,10 @@ public void VecDotQ8_0Scalar_HandCalculated() } } - [Fact] + [SkippableFact] public void VecDotQ8_0_ScalarMatchesAvx2() { - if (!Avx2.IsSupported) - return; + Skip.IfNot(Avx2.IsSupported, "Requires AVX2."); var rng = new Random(42); const int blockCount = 16; @@ -137,7 +136,7 @@ public void VecDotQ8_0_ScalarMatchesAvx2() // ──────────────────── VecDot optimized AVX2 accuracy ──────────────────── - [Theory] + [SkippableTheory] [InlineData(1)] [InlineData(2)] [InlineData(7)] @@ -146,8 +145,7 @@ public void VecDotQ8_0_ScalarMatchesAvx2() [InlineData(344)] public void VecDotQ8_0_OptimizedAvx2_MatchesScalar(int blockCount) { - if (!Avx2.IsSupported) - return; + Skip.IfNot(Avx2.IsSupported, "Requires AVX2."); var rng = new Random(42); nuint totalBytes = (nuint)(blockCount * Q8_0BlockBytes); @@ -179,7 +177,7 @@ public void VecDotQ8_0_OptimizedAvx2_MatchesScalar(int blockCount) /// range (|x|,|w| <= 127 bounds each lane at 32258 < 32767), so results must match the /// non-VNNI kernel exactly — not merely within tolerance. /// - [Theory] + [SkippableTheory] [InlineData(1)] [InlineData(2)] [InlineData(3)] @@ -189,8 +187,7 @@ public void VecDotQ8_0_OptimizedAvx2_MatchesScalar(int blockCount) [InlineData(344)] public void VecDotQ8_0_Vnni4Rows_BitIdenticalToAvx512(int blockCount) { - if (!Avx512BW.IsSupported || !AvxVnni.IsSupported) - return; + Skip.IfNot(Avx512BW.IsSupported && AvxVnni.IsSupported, "Requires AVX-512BW + AVX-VNNI."); var rng = new Random(1234); nuint rowBytes = (nuint)(blockCount * Q8_0BlockBytes); @@ -227,7 +224,7 @@ public void VecDotQ8_0_Vnni4Rows_BitIdenticalToAvx512(int blockCount) } } - [Theory] + [SkippableTheory] [InlineData(1)] [InlineData(2)] [InlineData(3)] @@ -237,8 +234,7 @@ public void VecDotQ8_0_Vnni4Rows_BitIdenticalToAvx512(int blockCount) [InlineData(344)] public void VecDotQ8_0_Avx512_MatchesScalar(int blockCount) { - if (!Avx512BW.IsSupported) - return; + Skip.IfNot(Avx512BW.IsSupported, "Requires AVX-512BW."); var rng = new Random(42); nuint totalBytes = (nuint)(blockCount * Q8_0BlockBytes); @@ -264,14 +260,13 @@ public void VecDotQ8_0_Avx512_MatchesScalar(int blockCount) // ──────────────────── Multi-row (4-row) accuracy ──────────────────── - [Theory] + [SkippableTheory] [InlineData(16)] [InlineData(128)] [InlineData(344)] public void VecDotQ8_0_4Row_MatchesSingleRow(int blockCount) { - if (!Avx2.IsSupported) - return; + Skip.IfNot(Avx2.IsSupported, "Requires AVX2."); var rng = new Random(42); nuint rowBytes = (nuint)(blockCount * Q8_0BlockBytes); @@ -317,11 +312,10 @@ public void VecDotQ8_0_4Row_MatchesSingleRow(int blockCount) // ──────────────────── VecDot edge cases ──────────────────── - [Fact] + [SkippableFact] public void VecDotQ8_0_AllZeroScales() { - if (!Avx2.IsSupported) - return; + Skip.IfNot(Avx2.IsSupported, "Requires AVX2."); const int blockCount = 4; nuint totalBytes = (nuint)(blockCount * Q8_0BlockBytes); @@ -357,11 +351,10 @@ public void VecDotQ8_0_AllZeroScales() } } - [Fact] + [SkippableFact] public void VecDotQ8_0_MaxValues() { - if (!Avx2.IsSupported) - return; + Skip.IfNot(Avx2.IsSupported, "Requires AVX2."); const int blockCount = 4; nuint totalBytes = (nuint)(blockCount * Q8_0BlockBytes); @@ -396,15 +389,14 @@ public void VecDotQ8_0_MaxValues() } } - [Theory] + [SkippableTheory] [InlineData(1)] [InlineData(3)] [InlineData(5)] [InlineData(7)] public void VecDotQ8_0_OddBlockCount(int blockCount) { - if (!Avx2.IsSupported) - return; + Skip.IfNot(Avx2.IsSupported, "Requires AVX2."); var rng = new Random(42); nuint totalBytes = (nuint)(blockCount * Q8_0BlockBytes); @@ -421,6 +413,8 @@ public void VecDotQ8_0_OddBlockCount(int blockCount) Assert.Equal(scalar, avx2, 1e-2f); + // Extra assertion, not a gate: the AVX2 comparison above already ran, so this only + // adds AVX-512 coverage where the ISA exists rather than replacing anything. if (Avx512BW.IsSupported) { float avx512 = MatMul.VecDotQ8_0Avx512((byte*)aPtr, (byte*)bPtr, blockCount); @@ -497,11 +491,10 @@ public void QuantizeF32ToQ8_0_AllZeros_ProducesZeroScale() } } - [Fact] + [SkippableFact] public void QuantizeF32ToQ8_0_Avx2_MatchesScalar() { - if (!Avx2.IsSupported) - return; + Skip.IfNot(Avx2.IsSupported, "Requires AVX2."); var rng = new Random(42); const int k = 1024; // 32 blocks diff --git a/tests/DotLLM.Tests.Unit/Cpu/Kernels/OuterProductGemmTests.cs b/tests/DotLLM.Tests.Unit/Cpu/Kernels/OuterProductGemmTests.cs index 32922068..bc96e980 100644 --- a/tests/DotLLM.Tests.Unit/Cpu/Kernels/OuterProductGemmTests.cs +++ b/tests/DotLLM.Tests.Unit/Cpu/Kernels/OuterProductGemmTests.cs @@ -91,7 +91,7 @@ public void OuterProductScalar_4x3_MatchesPerTokenComputeRows(int blockCount) // ──────────────────── AVX2 microkernel ──────────────────── - [Theory] + [SkippableTheory] [InlineData(1)] [InlineData(2)] [InlineData(16)] @@ -100,8 +100,7 @@ public void OuterProductScalar_4x3_MatchesPerTokenComputeRows(int blockCount) [InlineData(128)] // 4096/32 public void OuterProductAvx2_4x3_MatchesScalar(int blockCount) { - if (!Avx2.IsSupported) - return; + Skip.IfNot(Avx2.IsSupported, "Requires AVX2."); var rng = new Random(42); int m = 4; diff --git a/tests/DotLLM.Tests.Unit/Cpu/Kernels/RoPETests.cs b/tests/DotLLM.Tests.Unit/Cpu/Kernels/RoPETests.cs index a5ee20c6..5ecca564 100644 --- a/tests/DotLLM.Tests.Unit/Cpu/Kernels/RoPETests.cs +++ b/tests/DotLLM.Tests.Unit/Cpu/Kernels/RoPETests.cs @@ -127,11 +127,10 @@ public void ApplyRotation_KnownRotation_HandCalculated() Assert.Equal(3f * MathF.Sin(0.01f) + 4f * MathF.Cos(0.01f), vec[3], 1e-5f); } - [Fact] + [SkippableFact] public void ApplyRotation_ScalarMatchesSIMD() { - if (!Avx2.IsSupported) - return; // SIMD path not available + Skip.IfNot(Avx2.IsSupported, "Requires AVX2."); const int headDim = 128; int halfDim = headDim / 2; @@ -230,11 +229,10 @@ public void Execute_DifferentPositions_DifferentRotations() Assert.NotEqual(q[4], q[8], 1e-3f); } - [Fact] + [SkippableFact] public void Execute_ScalarMatchesSIMD_LargeInput() { - if (!Avx2.IsSupported) - return; + Skip.IfNot(Avx2.IsSupported, "Requires AVX2."); const int headDim = 128; const int numHeads = 32; diff --git a/tests/DotLLM.Tests.Unit/Cuda/CudaGemmTest.cs b/tests/DotLLM.Tests.Unit/Cuda/CudaGemmTest.cs index e2457560..e4027e32 100644 --- a/tests/DotLLM.Tests.Unit/Cuda/CudaGemmTest.cs +++ b/tests/DotLLM.Tests.Unit/Cuda/CudaGemmTest.cs @@ -20,6 +20,9 @@ public class CudaGemmTest : IDisposable public CudaGemmTest(ITestOutputHelper output) { _out = output; + // Intentional: this is the fixture constructor, not a test. Leaving the handles null lets + // construction succeed on a machine without a GPU; each test then reports as skipped via + // its own Skip.IfNot(CudaDevice.IsAvailable(), ...). if (!CudaDevice.IsAvailable()) return; _ctx = CudaContext.Create(0); _stream = CudaStream.Create(); diff --git a/tests/DotLLM.Tests.Unit/Cuda/CudaKernelComparisonTests.cs b/tests/DotLLM.Tests.Unit/Cuda/CudaKernelComparisonTests.cs index f1237edd..159e5a74 100644 --- a/tests/DotLLM.Tests.Unit/Cuda/CudaKernelComparisonTests.cs +++ b/tests/DotLLM.Tests.Unit/Cuda/CudaKernelComparisonTests.cs @@ -30,6 +30,9 @@ public class CudaKernelComparisonTests : IDisposable public CudaKernelComparisonTests(ITestOutputHelper output) { _output = output; + // Intentional: this is the fixture constructor, not a test. Leaving the handles null lets + // construction succeed on a machine without a GPU; each test then reports as skipped via + // its own Skip.IfNot(CudaDevice.IsAvailable(), ...). if (!CudaDevice.IsAvailable()) return; _ctx = CudaContext.Create(0); diff --git a/tests/DotLLM.Tests.Unit/Cuda/CudaKernelTests.cs b/tests/DotLLM.Tests.Unit/Cuda/CudaKernelTests.cs index 3b6c1a9d..d47b21a7 100644 --- a/tests/DotLLM.Tests.Unit/Cuda/CudaKernelTests.cs +++ b/tests/DotLLM.Tests.Unit/Cuda/CudaKernelTests.cs @@ -18,6 +18,9 @@ public class CudaKernelTests : IDisposable public CudaKernelTests() { + // Intentional: this is the fixture constructor, not a test. Leaving the handles null lets + // construction succeed on a machine without a GPU; each test then reports as skipped via + // its own Skip.IfNot(CudaDevice.IsAvailable(), ...). if (!CudaDevice.IsAvailable()) return; _ctx = CudaContext.Create(0); diff --git a/tests/DotLLM.Tests.Unit/Cuda/CudaTensorTests.cs b/tests/DotLLM.Tests.Unit/Cuda/CudaTensorTests.cs index 3dd5cbe3..6e1bdb55 100644 --- a/tests/DotLLM.Tests.Unit/Cuda/CudaTensorTests.cs +++ b/tests/DotLLM.Tests.Unit/Cuda/CudaTensorTests.cs @@ -16,6 +16,9 @@ public class CudaTensorTests : IDisposable public CudaTensorTests() { + // Intentional: this is the fixture constructor, not a test. Leaving the context null lets + // construction succeed on a machine without a GPU; each test then reports as skipped via + // its own Skip.IfNot(CudaDevice.IsAvailable(), ...). if (CudaDevice.IsAvailable()) _ctx = CudaContext.Create(0); }