From 6458f6740d3173591334f0852389e8ee120ca6b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Sun, 26 Oct 2025 20:30:44 +0100 Subject: [PATCH] Add GPU prime tester overload for preallocated buffers --- .../Gpu/PrimeTesterGpuTests.cs | 30 +++++ PerfectNumbers.Core/PrimeTester.cs | 114 +++++++++++++++--- 2 files changed, 127 insertions(+), 17 deletions(-) diff --git a/PerfectNumbers.Core.Tests/Gpu/PrimeTesterGpuTests.cs b/PerfectNumbers.Core.Tests/Gpu/PrimeTesterGpuTests.cs index 5e8faf9..0cce423 100644 --- a/PerfectNumbers.Core.Tests/Gpu/PrimeTesterGpuTests.cs +++ b/PerfectNumbers.Core.Tests/Gpu/PrimeTesterGpuTests.cs @@ -1,4 +1,6 @@ using FluentAssertions; +using ILGPU; +using ILGPU.Runtime; using PerfectNumbers.Core.Gpu; using Xunit; @@ -27,6 +29,34 @@ public class PrimeTesterGpuTests } } + [Fact] + public void IsPrimeBatchGpu_with_preallocated_resources_marks_expected_values() + { + GpuContextPool.ForceCpu = false; + using var context = Context.CreateDefault(); + using var accelerator = context.GetPreferredDevice(false).CreateAccelerator(context); + + try + { + ulong[] values = [31UL, 33UL, 37UL, 39UL]; + byte[] results = new byte[values.Length]; + + using var inputBuffer = accelerator.Allocate1D(values.Length); + using var outputBuffer = accelerator.Allocate1D(values.Length); + ulong[] staging = new ulong[values.Length]; + + PrimeTester.IsPrimeBatchGpu(values, results, accelerator, inputBuffer, outputBuffer, staging); + + results.Should().Equal(new byte[] { 1, 0, 1, 0 }); + } + finally + { + PrimeTester.ClearGpuCaches(accelerator); + GpuContextPool.ForceCpu = false; + GpuContextPool.DisposeAll(); + } + } + [Fact] public void IsPrimeGpu_rejects_composites() { diff --git a/PerfectNumbers.Core/PrimeTester.cs b/PerfectNumbers.Core/PrimeTester.cs index dd15c4e..ba46038 100644 --- a/PerfectNumbers.Core/PrimeTester.cs +++ b/PerfectNumbers.Core/PrimeTester.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Collections.Concurrent; using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Threading; using ILGPU; using ILGPU.Algorithms; @@ -112,7 +113,6 @@ public sealed class PrimeTester var gpu = PrimeTesterGpuContextPool.Rent(); var accelerator = gpu.Accelerator; var state = GpuKernelState.GetOrCreate(accelerator); - int totalLength = values.Length; int batchSize = Math.Max(1, GpuBatchSize); lock (gpu.ExecutionLock) @@ -123,30 +123,110 @@ public sealed class PrimeTester ArrayPool pool = ThreadStaticPools.UlongPool; ulong[] temp = pool.Rent(batchSize); - int pos = 0; - while (pos < totalLength) + try { - int remaining = totalLength - pos; - int count = remaining > batchSize ? batchSize : remaining; - - values.Slice(pos, count).CopyTo(temp); - input.View.CopyFromCPU(ref temp[0], count); - - state.Kernel(count, input.View, state.DevicePrimes.View, output.View); - accelerator.Synchronize(); - output.View.CopyToCPU(ref results[pos], count); - - pos += count; + IsPrimeBatchGpu(values, results, accelerator, input, output, temp); + } + finally + { + pool.Return(temp, clearArray: false); + state.ReturnScratch(scratch); } - - pool.Return(temp, clearArray: false); - state.ReturnScratch(scratch); } gpu.Dispose(); limiter.Dispose(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void IsPrimeBatchGpu( + ReadOnlySpan values, + Span results, + Accelerator accelerator, + MemoryBuffer1D deviceInput, + MemoryBuffer1D deviceOutput, + Span stagingBuffer) + { + if (accelerator is null) + { + throw new ArgumentNullException(nameof(accelerator)); + } + + if (deviceInput is null) + { + throw new ArgumentNullException(nameof(deviceInput)); + } + + if (deviceOutput is null) + { + throw new ArgumentNullException(nameof(deviceOutput)); + } + + if (results.Length < values.Length) + { + throw new ArgumentException("The results span must be at least as long as the input values.", nameof(results)); + } + + if (values.IsEmpty) + { + return; + } + + if (stagingBuffer.IsEmpty) + { + throw new ArgumentException("The staging buffer must contain at least one element.", nameof(stagingBuffer)); + } + + long inputLength = deviceInput.Length; + long outputLength = deviceOutput.Length; + if (inputLength <= 0) + { + throw new ArgumentException("The device input buffer must contain at least one element.", nameof(deviceInput)); + } + + if (outputLength <= 0) + { + throw new ArgumentException("The device output buffer must contain at least one element.", nameof(deviceOutput)); + } + + long stagingLength = stagingBuffer.Length; + long capacityLong = Math.Min(Math.Min(inputLength, outputLength), stagingLength); + if (capacityLong <= 0) + { + throw new ArgumentException("The provided buffers must be able to hold at least one element."); + } + + int capacity = (int)Math.Min(capacityLong, int.MaxValue); + + var state = GpuKernelState.GetOrCreate(accelerator); + var inputView = deviceInput.View; + var outputView = deviceOutput.View; + var primesView = state.DevicePrimes.View; + var stagingSpan = stagingBuffer; + ref ulong stagingRef = ref MemoryMarshal.GetReference(stagingSpan); + + int totalLength = values.Length; + int pos = 0; + while (pos < totalLength) + { + int remaining = totalLength - pos; + int count = remaining > capacity ? capacity : remaining; + + var stagingSlice = stagingSpan.Slice(0, count); + values.Slice(pos, count).CopyTo(stagingSlice); + + var inputSlice = inputView.SubView(0, count); + inputSlice.CopyFromCPU(ref stagingRef, count); + + var outputSlice = outputView.SubView(0, count); + state.Kernel(count, inputSlice, primesView, outputSlice); + accelerator.Synchronize(); + outputSlice.CopyToCPU(ref results[pos], count); + + pos += count; + } + } + internal static class PrimeTesterGpuContextPool { internal sealed class PooledContext : IDisposable