FastCompute.ImageProcessing 0.8.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package FastCompute.ImageProcessing --version 0.8.0
                    
NuGet\Install-Package FastCompute.ImageProcessing -Version 0.8.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="FastCompute.ImageProcessing" Version="0.8.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="FastCompute.ImageProcessing" Version="0.8.0" />
                    
Directory.Packages.props
<PackageReference Include="FastCompute.ImageProcessing" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add FastCompute.ImageProcessing --version 0.8.0
                    
#r "nuget: FastCompute.ImageProcessing, 0.8.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package FastCompute.ImageProcessing@0.8.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=FastCompute.ImageProcessing&version=0.8.0
                    
Install as a Cake Addin
#tool nuget:?package=FastCompute.ImageProcessing&version=0.8.0
                    
Install as a Cake Tool

FastCompute.NET

FastCompute.NET is a strongly named .NET 8 library for fast array processing. It provides one API for single-threaded CPU, multi-threaded CPU, SIMD, and ILGPU execution and supports float, double, and int arrays.

Version 0.8.0 is the current stable release. The assembly public key token is c76a60c96d65300c.

Quick start

1. Install the packages

dotnet add package FastCompute --version 0.8.0

The consuming project must target .NET 8 or a compatible later framework.

Image processing is a separate package that depends on the core:

dotnet add package FastCompute.ImageProcessing --version 0.8.0

The core package has no image dependency; install the image package only when an application needs Image<TPixel> and friends.

2. Build and execute an optimized pipeline

using FastCompute;

float[] source = [0.0f, 0.5f, 1.0f, 1.5f];

float[] result = source
    .AsCompute()
    .Select(value => value * 2.0f)
    .SelectInPlace(value => value + 1.0f)
    .Select(value => ComputeMath.Sin(value))
    .ToArray();

ComputeMath describes mathematical operations that every compatible backend can execute. Its use does not select or force GPU execution.

Nothing is executed before ToArray. FastCompute fuses the three selectors into one expression, selects a backend once, and avoids intermediate managed arrays.

AsCompute() uses Auto by default. FastCompute evaluates the complete optimized expression and array size and then selects Scalar CPU, Parallel CPU, SIMD, or GPU. Auto is transfer-conservative: merely having a GPU does not mean that every pipeline is sent to it.

3. Supply a reusable GPU context

ComputeDeviceInfo gpuDevice = ComputeContext.GetAccelerators()
    .First(device => !device.AcceleratorType.Contains(
        "CPU",
        StringComparison.OrdinalIgnoreCase));

using ComputeContext context = ComputeContext.Create(
    new ComputeContextOptions
    {
        AcceleratorIndex = gpuDevice.Index
    });

float[] result = source
    .AsCompute(context)
    .Select(value => value * 2.0f)
    .Select(value => ComputeMath.Sin(value))
    .ToArray();

Passing a context makes that accelerator available to Auto and reuses its compiled kernels. It still does not force GPU execution.

4. Force the selected GPU when required

float[] gpuResult = source
    .AsCompute(
        new ComputeOptions
        {
            Backend = ComputeBackendKind.Gpu,
            GpuContext = context
        })
    .Select(value => value * 2.0f)
    .Select(value => ComputeMath.Sin(value))
    .ToArray();

An explicitly selected backend is a strict request and never silently falls back. If the expression or machine does not support that backend, the operation throws an exception.

5. Call an existing custom method

Expressions intended for SIMD or GPU must use the supported expression subset. If the transformation needs unrestricted .NET code, use RunDelegate and select Scalar or Parallel CPU:

float GetApplicationCoefficient(float value) =>
    value < 0.5f ? 0.25f : 0.75f;

float CustomTransform(float value) =>
    MathF.Sin(value) + GetApplicationCoefficient(value);

float[] result = Compute.RunDelegate(
    source,
    CustomTransform,
    new ComputeOptions
    {
        Backend = ComputeBackendKind.ParallelCpu
    });

Detailed guide

Lazy optimized pipelines

AsCompute creates an immutable lazy pipeline for float[], double[], or int[]:

ComputePipeline<float> pipeline = source
    .AsCompute()
    .Select(value => value * 2.0f)
    .SelectInPlace(value => value + 1.0f)
    .Select(value => ComputeMath.Clamp(value, 0.0f, 1.0f));

// Planning, optimization, backend selection, and execution happen here.
float[] result = pipeline.ToArray();

At the terminal operation, the pipeline optimizer substitutes each selector into the next selector. Existing expression optimization then performs constant folding and IEEE-safe simplification. The resulting expression is executed as one Map operation, which means one Parallel/SIMD pass or one GPU Map kernel instead of one pass per Select.

A pipeline can record one binary Zip node. Selectors before and after it are substituted into one binary expression and execute as one Zip operation:

float[] combined = left
    .AsCompute()
    .Select(value => value * 2.0f)
    .Zip(right, (first, second) => first + second)
    .Select(value => ComputeMath.Clamp(value, 0.0f, 1.0f))
    .ToArray();

The right array remains lazy by reference and must have the same length as the left array when the terminal operation runs. ToArrayInPlace explicitly stores the fused Zip result in the left source array. A second Zip is rejected because it would require a multi-input graph with more than two source arrays.

Reduction terminals are fused as well. A selector chain followed by Sum, Min, Max, or Average transforms values while reducing them, without materializing a full-size mapped array. GPU execution applies the selector expression in the first reduction kernel stage, including chunked execution. The same terminals fuse a binary Zip graph directly into the reduction, so neither mapped nor zipped intermediate arrays are materialized. This applies to float, double, and int on every backend, including chunked GPU execution.

Pipelines can be configured in three ways:

// Auto with library defaults.
ComputePipeline<float> automatic = source.AsCompute();

// Auto or explicit backend with operation settings.
ComputePipeline<float> configured = source.AsCompute(
    new ComputeOptions
    {
        Backend = ComputeBackendKind.Auto,
        PreferredGpuAcceleratorIndex = preferredGpu.Index
    });

// Auto selection with a reusable GPU context when GPU wins.
ComputePipeline<float> withContext = source.AsCompute(context);

SelectInPlace allows the optimizer to reuse intermediate storage but does not change the original managed array. This makes normal ToArray execution safe to branch:

ComputePipeline<float> root = source.AsCompute();
ComputePipeline<float> doubled =
    root.SelectInPlace(value => value * 2.0f);
ComputePipeline<float> shifted =
    root.Select(value => value + 10.0f);

float[] first = doubled.ToArray();
float[] second = shifted.ToArray();

To explicitly overwrite source, use the mutating terminal:

float[] sameArray = source
    .AsCompute()
    .Select(value => value * 2.0f)
    .SelectInPlace(value => value + 1.0f)
    .ToArrayInPlace();

Debug.Assert(ReferenceEquals(source, sameArray));

Available terminals are ToArray, ToArrayInPlace, Sum, Min, Max, and Average. Selector fusion is currently unary. A reduction after selectors uses the optimized Map result and then the selected reduction backend; map-reduction kernel fusion is reserved for a later version.

Core operations

Map

Run applies a unary expression to every array element and returns a new array:

float[] mapped = Compute.Run(
    source,
    value => ComputeMath.Clamp(value * 1.25f, 0.0f, 1.0f));
Zip

Zip combines arrays element by element:

float[] left = [1.0f, 2.0f, 3.0f];
float[] right = [10.0f, 20.0f, 30.0f];

float[] zipped = Compute.Zip(
    left,
    right,
    (x, y) => x + y);

Both arrays must have the same length.

In-place processing

Use the in-place variants when the result may overwrite the input array:

Compute.RunInPlace(
    source,
    value => value * 2.0f + 1.0f);

Compute.ZipInPlace(
    target: left,
    right: right,
    (x, y) => x + y);

The returned reference is the same array as the target. This reduces managed allocations, but cancellation or an execution failure can leave an in-place array partially modified.

Reductions
float sum = Compute.Sum(source);
float minimum = Compute.Min(source);
float maximum = Compute.Max(source);
float average = Compute.Average(source);

The same operations are available for double[] and int[]. Average returns the array element type, so integer average uses integer semantics.

Supported element types and expressions

Type Map/Zip In-place Reductions Resident buffer Histogram
float Yes Yes Yes Yes Yes
double Yes Yes Yes Yes No
int Yes Yes Yes Yes No

Float expressions support arithmetic, comparisons, conditional expressions, captured primitive constants, and these ComputeMath methods:

  • Abs, Min, Max, and Clamp;
  • Sqrt, Pow, Exp, Log, and Log10;
  • Sin, Cos, and Tan;
  • Floor, Ceiling, and Round.

The name is intentionally backend-neutral: ComputeMath.Sin, for example, can run on Scalar CPU, Parallel CPU, SIMD, or GPU. The older GpuMath name remains available as a compatibility alias.

float multiplier = 0.75f;
bool clamp = true;

float[] result = Compute.Run(
    source,
    value => clamp
        ? ComputeMath.Clamp(ComputeMath.Sin(value) * multiplier, 0.0f, 1.0f)
        : ComputeMath.Sin(value) * multiplier);

Double and integer expressions use arithmetic and the applicable supported System.Math overloads:

double[] precise = Compute.Run(
    new[] { 0.0, 0.5, 1.0 },
    value => Math.Sin(value) * Math.Exp(-value));

int[] integers = Compute.Run(
    new[] { 1, 2, 3 },
    value => value * 2 + 1);

The expression API converts an expression tree to FastCompute's backend-independent instruction representation. Captured float, double, int, and bool values are supported. Calls through captured reference objects and arbitrary .NET methods are intentionally rejected because they cannot be translated to SIMD or GPU instructions.

Fast Fourier transform

FFT is a first-class array operation rather than a scalar ComputeMath expression. Complex32 uses two contiguous single-precision components and is native to Scalar, Parallel CPU, AVX SIMD, and GPU backends. One- and two-dimensional radix-2 transforms support both allocating and in-place APIs:

Complex32[] spectrum = Compute.Fft(samples, options: computeOptions);
Compute.FftInPlace(spectrum, FourierDirection.Inverse, computeOptions);

Complex32[] spectrum2D = Compute.Fft2D(pixels, width, height, options: computeOptions);

Forward transforms are unnormalized. Inverse transforms divide by the complete element count, so a forward/inverse pair reconstructs the original input. Dimensions must be positive powers of two. Explicit backend selection follows the same strict contract as other FastCompute operations and Auto uses the normal SIMD, parallel CPU, and heavy-GPU thresholds.

Signal processing, statistics, and convolution

The core API includes generic single-precision primitives that share the ComputeOptions contract and its backend selection:

float[] power = Compute.PowerSpectrum(spectrum);
float[] magnitudes = Compute.MagnitudeSpectrum(spectrum);
float[] phases = Compute.PhaseSpectrum(spectrum);
SignalPeak[] peaks = Compute.FindPeaks(values, minimumValue: 0.5f);

float[] smoothed = Compute.Convolve1D(values, kernel, ConvolutionBoundary.Clamp);
float[] windowed = Compute.ApplyHannWindow(values);

Phase spectrum uses Atan2, which has no SIMD instruction in the expression IR, so it runs on Scalar, Parallel CPU, or GPU and rejects explicit SIMD.

Statistics cover moments, covariance, correlation, and regression:

StatisticsResult moments = Compute.CalculateStatistics(values);
double covariance = Compute.Covariance(x, y);
double correlation = Compute.Correlation(x, y);
LinearRegressionResult regression = Compute.LinearRegression(x, y);
double entropy = Compute.ShannonEntropy(histogram);

Percentile/quantile/median sort a copy of the input because FastCompute does not yet expose a backend-native ordering primitive. Distribution moments, variance, covariance, and correlation are computed with the optimized pointer-free pipelines.

Thresholding and normalization are first-class as well:

float[] binary = Compute.Threshold(values, threshold: 0.5f);
MinMaxResult range = Compute.MinMax(values);
float[] unitRange = Compute.Normalize(values);
float[] safe = Compute.SafeDivide(numerator, denominator, zeroResult: 0.0f);

Composite values and image formats

Unmanaged structures can opt into FastCompute by implementing IComputeValue<T>. Descriptors validate a tightly packed homogeneous layout of float or byte components. Float-component transformations, including transformations between different structures, run on Scalar, Parallel CPU, SIMD, and GPU:

GrayF32[] luminance = pixels
    .AsCompute()
    .Select(Rgb.GrayscaleF32)
    .ToArray();

Mixed component types use the same Select<TDestination> API:

Gray8[] compact = rgb24
    .AsCompute()
    .Select(pixel => new Gray8(
        (byte)((pixel.Red + pixel.Green + pixel.Blue) / 3)))
    .ToArray();

Homogeneous byte-component maps, projections, and conversions execute on Scalar, Parallel CPU, SIMD, and GPU while preserving C# integer promotion, integer division, and explicit unchecked byte narrowing. Mixed custom byte/float expression execution currently supports Scalar and Parallel CPU; the built-in image formats use their specialized SIMD/GPU converters instead of inserting a hidden full-size floating-point staging buffer.

Packed byte values with one through four components have native SIMD layout load/store kernels. Wider homogeneous values still use SIMD arithmetic with a generic layout adapter. An explicitly requested SIMD backend never silently falls back to a full scalar pixel-conversion pass: nonlinear Srgb/Linear transfer conversion must currently use Scalar, Parallel CPU, or GPU.

The separate FastCompute.ImageProcessing assembly depends on the numeric FastCompute core; the core has no image dependency. It provides Rgb24, floating-point Rgb, Gray8, GrayF32, and separate Srgb/Linear encoding metadata. Image<TPixel> can own an array or wrap contiguous Memory<TPixel> without copying. It provides row spans, CopyRow, clone, crop, color conversion, grayscale conversion, deterministic resize/downsampling, convolution-backed Gaussian/Sobel/Laplacian filters, residuals, local contrast/entropy, spectrum preparation, Bayer CFA, demosaicing, and camera simulation:

Image<Rgb24> decoded = Image<Rgb24>.Wrap(memory, width, height);
Image<GrayF32> linear = decoded.ToGrayscaleF32(ColorEncoding.Linear);
Image<GrayF32> lowPass = linear.BoxBlur(radius: 1);
Image<GrayF32> residual = linear.Subtract(lowPass);
Image<GrayF32> edges = linear.Sobel();
Image<GrayF32> resized = linear.Resize(width: 1024, height: 768);

All computational image operations accept the same ComputeOptions contract as the array API. Explicit GPU execution is available for conversions between Rgb24, Rgb, Gray8, and GrayF32, transfer-function conversion, convolution, box blur, subtraction, resize/downsampling, gradients, local contrast/entropy, radial spectra, Bayer/demosaicing, and noise application:

var gpuOptions = new ComputeOptions
{
    Backend = ComputeBackendKind.Gpu,
    GpuContext = gpu
};

Image<GrayF32> linearGpu = decoded.ToGrayscaleF32(
    ColorEncoding.Linear,
    gpuOptions);
Image<GrayF32> lowPassGpu = linearGpu.BoxBlur(
    radius: 1,
    options: gpuOptions);

Auto follows the normal FastCompute thresholds. Host-backed image operations use GpuSimpleThreshold, which is disabled by default because host/device transfer often costs more than CPU SIMD. Lower that threshold explicitly when profiling shows a benefit for the target accelerator and workload. Explicit Backend = ComputeBackendKind.Gpu always requests the GPU path.

For multi-stage GPU processing, upload once and keep intermediate images on the accelerator:

using ImageBuffer<Rgb24> resident = decoded.UploadToGpu(gpu);
using ImageBuffer<GrayF32> luminance = resident.ToGrayscaleF32(
    ColorEncoding.Linear);
using ImageBuffer<GrayF32> blur = luminance.BoxBlur(radius: 1);
using ImageBuffer<GrayF32> residual = luminance.Subtract(blur);

Image<GrayF32> result = residual.Download();

ImageBuffer<TPixel> owns its device allocation and must be disposed. Conversion, blur, subtraction, resize, and downsampling operate device-to-device; only UploadToGpu and Download cross the host/device boundary.

CPU area downsampling vectorizes accumulation across each source interval. GPU box blur uses parallel per-pixel kernels for small radii and switches to a linear-time sliding-window pass for radii greater than four.

Explicit backend selection remains strict. Local window entropy and phase spectrum do not currently have SIMD implementations because per-window integer histograms and vector Atan2 are not supported by the expression IR; explicit SIMD requests are rejected rather than executed by a hidden scalar loop. Both operations have Scalar, Parallel CPU, and native GPU paths. Percentile/quantile use the runtime in-place sort because FastCompute does not yet expose a backend-native ordering primitive.

Arbitrary user methods

When a transformation contains unrestricted application code, use RunDelegate. It executes a normal Func<float, float> on Scalar or Parallel CPU without translating it:

float GetApplicationCoefficient(float value) =>
    value < 0.5f ? 0.25f : 0.75f;

float CustomCalculation(float value) =>
    MathF.Sin(value) + GetApplicationCoefficient(value);

float[] result = Compute.RunDelegate(
    source,
    CustomCalculation,
    new ComputeOptions
    {
        Backend = ComputeBackendKind.ParallelCpu
    });

RunDelegate currently supports only float[] and the Scalar and Parallel CPU backends. It cannot execute arbitrary CLR methods on SIMD or GPU hardware.

Choosing a backend

var options = new ComputeOptions
{
    Backend = ComputeBackendKind.Auto,
    MaxDegreeOfParallelism = Environment.ProcessorCount
};

The available modes are:

Backend Behavior
Auto Selects a compatible backend using expression complexity, array size, transfer cost, and the GPU memory budget.
Scalar Runs a conventional single-threaded CPU loop.
ParallelCpu Splits work into CPU chunks and processes them on multiple threads.
Simd Uses hardware-accelerated CPU vectors and a scalar tail.
Gpu Executes through ILGPU on the selected accelerator.

SIMD is not another form of Parallel.For: it processes several values per CPU instruction on the calling thread. Parallel CPU and SIMD are separate backends.

Auto mode is appropriate when the library should make the decision. Use an explicit backend for reproducible performance tests, a known deployment environment, or full user control:

float[] result = source.RunExplicit(
    value => value * value,
    ComputeBackendKind.ParallelCpu);

RunExplicit is a LINQ-style extension for float[], double[], and int[]. It rejects Auto by design.

Selecting one of several GPUs

Accelerator indices are assigned by ILGPU and can include CPU accelerators. Always discover them on the target machine:

using FastCompute;

IReadOnlyList<ComputeDeviceInfo> accelerators =
    ComputeContext.GetAccelerators();

foreach (ComputeDeviceInfo device in accelerators)
{
    Console.WriteLine(
        $"{device.Index}: {device.Name} ({device.AcceleratorType})");
}

To prefer a particular hardware GPU without forcing its use, pass its index to Auto mode:

ComputeDeviceInfo preferredGpu = ComputeContext.GetAccelerators()
    .First(device => !device.AcceleratorType.Contains(
        "CPU",
        StringComparison.OrdinalIgnoreCase));

ComputeResult<float[]> result = Compute.RunWithDiagnostics(
    source,
    value => ComputeMath.Sin(value) * ComputeMath.Exp(-value * value),
    new ComputeOptions
    {
        Backend = ComputeBackendKind.Auto,
        PreferredGpuAcceleratorIndex = preferredGpu.Index
    });

PreferredGpuAcceleratorIndex means “use this GPU if the planner decides that GPU execution is beneficial.” Auto may still select SIMD, Parallel CPU, or Scalar. If the preferred index is unavailable or does not identify a hardware GPU, Auto continues with a CPU backend.

To require that accelerator, create a reusable context and explicitly request GPU execution:

using ComputeContext gpu = ComputeContext.Create(
    new ComputeContextOptions
    {
        AcceleratorIndex = preferredGpu.Index
    });

Console.WriteLine($"Selected accelerator: {gpu.DeviceName}");

float[] gpuResult = Compute.Run(
    source,
    value => ComputeMath.Sin(value),
    new ComputeOptions
    {
        Backend = ComputeBackendKind.Gpu,
        GpuContext = gpu
    });

GpuContext and PreferredGpuAcceleratorIndex are mutually exclusive. A context created without an index prefers a non-CPU accelerator and falls back to the ILGPU CPU accelerator if no hardware GPU is available.

Setting the default preferred GPU

For an application-wide preference, set the accelerator once during startup:

ComputeDeviceInfo preferredGpu = ComputeContext.GetAccelerators()
    .First(device => !device.AcceleratorType.Contains(
        "CPU",
        StringComparison.OrdinalIgnoreCase));

ComputeDefaults.PreferredGpuAcceleratorIndex = preferredGpu.Index;

The default applies to Auto and explicit GPU operations that do not provide their own GPU setting:

// Considers the default GPU, but can still select CPU or SIMD.
float[] automatic = Compute.Run(
    source,
    value => ComputeMath.Sin(value));

// Requires GPU and uses the default preferred accelerator.
float[] forcedGpu = Compute.Run(
    source,
    value => ComputeMath.Sin(value),
    new ComputeOptions
    {
        Backend = ComputeBackendKind.Gpu
    });

The precedence order is:

  1. ComputeOptions.GpuContext for the current operation;
  2. ComputeOptions.PreferredGpuAcceleratorIndex for the current operation;
  3. ComputeDefaults.PreferredGpuAcceleratorIndex;
  4. FastCompute's automatic accelerator selection.

Set ComputeDefaults.PreferredGpuAcceleratorIndex = null to restore automatic selection. The property is process-wide; configure it during application startup rather than changing it between concurrent operations.

When GPU kernels are compiled

For the first operation in a ComputeContext, FastCompute:

  1. validates and lowers the expression;
  2. obtains or compiles the required ILGPU kernel template;
  3. caches the lowered expression and compiled template in that context;
  4. uploads data, starts the kernel, and downloads the result.

Repeated compatible operations on the same context reuse those caches. This is why the first GPU call is normally slower than subsequent calls. A new context has its own caches.

Use PrecompileAll during application warm-up to compile every implemented kernel template:

using ComputeContext gpu = ComputeContext.Create(
    new ComputeContextOptions
    {
        AcceleratorIndex = preferredGpu.Index
    });

IReadOnlyList<ComputeCompilationResult> templates = gpu.PrecompileAll();

Console.WriteLine(
    $"Prepared: {templates.Count}; " +
    $"cache hits: {templates.Count(item => item.CacheHit)}");

Use Precompile<T> to validate, lower, and cache a particular expression:

ComputeCompilationResult compilation =
    gpu.Precompile<float>(
        value => ComputeMath.Sin(value) * ComputeMath.Exp(-value));

Console.WriteLine($"Cache hit: {compilation.CacheHit}");
Console.WriteLine($"Compile time: {compilation.CompilationTime}");

For an operation that will be called repeatedly, create a prepared operation:

PreparedCompute<float> prepared =
    gpu.Prepare<float>(value => ComputeMath.Sin(value) * 2.0f);

float[] first = prepared.Run(source);
float[] second = prepared.Run(otherSource);

Precompilation removes kernel compilation from the first business operation. It does not upload the future input array and cannot remove normal GPU transfer cost.

Arrays larger than available GPU memory

GPU Map, Zip, in-place operations, reductions, and Histogram can run in sequential chunks. Chunking is enabled by default and the effective working set is limited by the context safety limit and optional operation budget:

float[] result = Compute.Run(
    source,
    value => ComputeMath.Sin(value),
    new ComputeOptions
    {
        Backend = ComputeBackendKind.Gpu,
        GpuContext = gpu,
        GpuMemoryBudgetBytes = 512L * 1024 * 1024,
        GpuChunkElementCount = 4_000_000
    });

GpuChunkElementCount is an optional upper bound. Without it, FastCompute calculates a chunk size from the memory budget. Setting EnableGpuChunking = false makes insufficient memory a hard error.

Explicit out-of-place float Map can optionally overlap transfers and execution using two accelerator streams:

float[] result = Compute.Run(
    source,
    value => ComputeMath.Sin(value),
    new ComputeOptions
    {
        Backend = ComputeBackendKind.Gpu,
        GpuContext = gpu,
        GpuChunkElementCount = 4_000_000,
        EnableGpuStreaming = true
    });

Streaming is opt-in because its benefit depends on the accelerator, bus, and expression. Other operations use sequential chunks.

Keeping data on the accelerator

For several consecutive GPU operations, upload once and use ComputeBuffer<T>:

using ComputeBuffer<float> input = gpu.Upload(source);
using ComputeBuffer<float> scaled =
    input.Select(value => value * 0.75f);
using ComputeBuffer<float> transformed =
    scaled.Select(value => ComputeMath.Sin(value));

float sum = transformed.Sum();

float[] output = new float[transformed.Length];
transformed.Download(output);

Console.WriteLine(transformed.Context.DeviceName);
Console.WriteLine(transformed.Location); // Host or Device

Available resident operations include Select, SelectInPlace, Zip, ZipInPlace, Sum, Min, Max, Average, and download to an array or Span<T>. Chained float selections use a lazy, copy-on-write execution graph where applicable. Dispose buffers and their context to release accelerator resources.

The context also owns a bounded, thread-safe transient float-buffer pool:

using ComputeContext gpu = ComputeContext.Create(
    new ComputeContextOptions
    {
        AcceleratorIndex = preferredGpu.Index,
        MemoryPoolLimitBytes = 256L * 1024 * 1024
    });

Console.WriteLine(gpu.MemoryPoolStatistics.RetainedBytes);
Console.WriteLine(gpu.MemoryPoolStatistics.EvictedBuffers);

The limit controls idle memory retained for reuse. Active operations may use more. Set MemoryPoolLimitBytes = 0 to disable idle-buffer retention.

Histogram

Histogram splits a numeric range into equal-width bins:

float[] samples = [0.0f, 0.1f, 0.5f, 0.9f, 1.0f];

int[] histogram = Compute.Histogram(
    samples,
    binCount: 256,
    minimum: 0.0f,
    maximum: 1.0f);

Finite values outside the range are clamped to the first or last bin by default. NaN is always ignored. To ignore all out-of-range values:

int[] histogram = Compute.Histogram(
    samples,
    binCount: 256,
    minimum: 0.0f,
    maximum: 1.0f,
    new HistogramOptions
    {
        OutOfRangeMode = HistogramOutOfRangeMode.Ignore
    });

Histogram supports Scalar, Parallel CPU, and GPU. Automatic GPU selection is opt-in through ComputeThresholdOptions.GpuHistogramThreshold.

Diagnostics

Diagnostic APIs return the value together with planning and execution details:

ComputeResult<float[]> result = Compute.RunWithDiagnostics(
    source,
    value => value * value,
    new ComputeOptions
    {
        Backend = ComputeBackendKind.Auto
    });

ComputeDiagnostics d = result.Diagnostics;

Console.WriteLine($"Backend:       {d.Backend}");
Console.WriteLine($"Device:        {d.DeviceName ?? "CPU"}");
Console.WriteLine($"Planning:      {d.PlanningTime}");
Console.WriteLine($"Compilation:   {d.CompilationTime}");
Console.WriteLine($"Execution:     {d.ExecutionTime}");
Console.WriteLine($"Upload bytes:  {d.UploadedBytes}");
Console.WriteLine($"Download bytes:{d.DownloadedBytes}");
Console.WriteLine($"Chunks:        {d.ChunkCount}");
Console.WriteLine($"Streaming:     {d.IsStreaming}");
Console.WriteLine($"Cache hit:     {d.KernelCacheHit}");

Variants include RunWithDiagnostics, RunInPlaceWithDiagnostics, ZipWithDiagnostics, ZipInPlaceWithDiagnostics, HistogramWithDiagnostics, and diagnostic reduction methods such as SumWithDiagnostics.

Async-compatible API and cancellation

using var cancellationSource = new CancellationTokenSource();

float[] mapped = await Compute.RunAsync(
    source,
    value => value * 2.0f,
    new ComputeOptions
    {
        CancellationToken = cancellationSource.Token
    });

using ComputeBuffer<float> buffer =
    await gpu.UploadAsync(source, cancellationSource.Token);

float[] downloaded =
    await buffer.DownloadAsync(cancellationSource.Token);

ILGPU currently exposes synchronous completion primitives. These methods therefore return completed tasks and do not hide blocking work in Task.Run. They are async-compatible API boundaries, not guaranteed non-blocking GPU execution.

Performance guidance

  • Use Auto for general-purpose calls, but verify the selected backend with diagnostics for important workloads.
  • A direct loop is normally best for very small arrays.
  • GPU execution is most useful when enough computation compensates for upload and download cost.
  • Reuse ComputeContext to reuse compiled kernels.
  • Use resident buffers for multi-step GPU pipelines.
  • Use in-place methods when overwriting the source is acceptable.
  • Benchmark on the deployment hardware; GPU model, memory bandwidth, CPU SIMD, and array size all affect the result.

The opt-in performance gate compares Auto with an equivalent single-threaded loop on large simple, heavy, and in-place Map workloads:

dotnet run --project benchmarks/FastCompute.Benchmarks `
  --configuration Release -- `
  --assert-performance

It exits with code 1 if FastCompute is more than 5% slower than the loop. Run it on otherwise idle hardware. The complete backend and operation matrix can be run with:

dotnet run --project benchmarks/FastCompute.Benchmarks `
  --configuration Release -- `
  --filter "*SpecificationMatrixBenchmarks*"

Common problems

GPU is slower than Parallel CPU.

This is expected when transfer and compilation costs exceed the kernel work. Reuse a context, precompile, keep intermediate data in resident buffers, or let Auto choose CPU.

The wrong GPU was selected.

Print ComputeContext.GetAccelerators(), select the runtime index, and either set PreferredGpuAcceleratorIndex for Auto or create a context with AcceleratorIndex for strict GPU execution.

An explicit backend throws instead of using CPU.

This is the intended contract. Explicit modes never silently fall back. Use Auto when fallback is required.

An expression cannot be translated.

Use supported arithmetic and math methods, or use RunDelegate for unrestricted float CPU code.

The first GPU call is slow.

The first call includes expression lowering and kernel compilation. Reuse the context and call PrecompileAll, Precompile<T>, or Prepare<T> during warm-up.

Sample application

The console sample demonstrates Auto selection, diagnostics, captured constants, precompilation, and a resident GPU pipeline:

dotnet run --project samples/FastCompute.Sample.Console `
  --configuration Release

Build, test, and package

dotnet build FastCompute.sln --configuration Release
dotnet test FastCompute.sln --configuration Release --no-build
./pack.ps1 -Version 0.8.0

pack.ps1 builds and tests the solution, creates .nupkg and .snupkg artifacts for both the FastCompute and FastCompute.ImageProcessing packages, verifies the strong-name identity of both assemblies, and runs a package-only consumer smoke test. On a Windows or Linux CI machine without a hardware GPU:

./pack.ps1 -Version 0.8.0 -SkipGpuTests

Further documentation

Authors

  • staszx — project author and maintainer.
  • OpenAI Codex — implementation and documentation assistance.
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.8.1 106 8/14/2026
0.8.0 94 8/14/2026

# FastCompute.NET Release Notes

## 0.8.0 - 2026-08-14

Native signal, statistics, and image processing primitives. The image and
forensics capabilities now live in their own assembly, and the generic numeric
primitives behind them moved into the core package.

### Added

- One- and two-dimensional radix-2 FFT for `Complex32[]` with allocating and
 in-place APIs (`Fft`, `FftInPlace`, `Fft2D`, `Fft2DInPlace`, inverse
 variants) on Scalar, Parallel CPU, AVX SIMD, and GPU backends.
- `Complex32` native composite value with `PowerSpectrum`,
 `MagnitudeSpectrum`, and `PhaseSpectrum` helpers; `FindPeaks`,
 `PeakToMedianRatio`, `MeanAbsoluteDifference`, `Percentile`, `Quantile`,
 `Median`, and Hann/Hamming/Blackman window functions in
 `Compute.Signal`.
- 1D and 2D convolution (`Convolve1D`, `Convolve2D`) with the same
 `ComputeOptions` contract.
- Statistics: `CalculateStatistics`, `Mean`, `Variance`, `StandardDeviation`,
 `Skewness`, `Kurtosis`, `SumOfSquares`, `Covariance`, `Correlation`,
 `AutoCorrelation`, `LinearRegression`, and `ShannonEntropy`.
- Threshold, `MinMax`, `Normalize`, and `SafeDivide` utilities.
- Homogeneous `byte`-component packed values with native SIMD layout
 load/store kernels and byte-composite GPU execution for one through four
 components.

### Changed

- Image and forensics functionality was moved into the new
 `FastCompute.ImageProcessing` assembly and NuGet package. The core
 `FastCompute` package has no image dependency; it ships with the generic
 primitives above.
- The negative image forensics pipeline now runs on the generic primitives
 instead of its own copies of FFT, statistics, convolution, Bayer handling,
 and camera simulation. See
 `docs/ai-image-forensics-algorithm-migration.md` for the ownership table.
- `Image<TPixel>` gained convolution-backed Gaussian/Sobel/Laplacian filters,
 residuals, local contrast and entropy, spectrum preparation, deterministic
 area resize, Bayer CFA sampling, and demosaicing on Scalar, Parallel CPU,
 SIMD, and GPU.

### Compatibility

- `FastCompute.ImageProcessing` 0.8.0 depends on `FastCompute` 0.8.0 and is
 strongly named with the same public key token `c76a60c96d65300c`.
- The original lazy pipeline, reduction fusion, `ComputeMath` (and the
 `GpuMath` alias), resident buffers, and chunked/streaming GPU execution
 remain unchanged.
- Explicit SIMD requests for local window entropy and phase spectrum are
 rejected instead of falling back to a hidden scalar loop; both operations
 have Scalar, Parallel CPU, and native GPU paths.