Editor/Inference/FlowLinear.cs
#nullable enable annotations
using System;
using System.Threading;
using System.Threading.Tasks;
namespace HumanoidMocapX.Inference;
internal sealed class FlowLinear
{
// Below this many multiply-adds, thread start-up costs more than it saves.
const long ParallelMultiplies = 1 << 22;
const int ChannelsPerBlock = 16;
static readonly ParallelOptions Parallelism = new() { MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount - 1) };
readonly Tensor weight, bias;
internal FlowLinear(FlowWeights weights, string name, int inputs, int outputs)
{
weight = weights.Get(name + ".weight", inputs, outputs);
bias = weights.Get(name + ".bias", outputs);
}
internal void Run(ReadOnlySpan<float> input, int rows, Span<float> output, CancellationToken cancellation = default)
=> Run(input, rows, weight, bias, output, cancellation);
/// <summary>Same result as <see cref="Run(ReadOnlySpan{float}, int, Span{float}, CancellationToken)"/>,
/// with output channels split across threads for large layers. Each output keeps the
/// same dot product, so values are identical. Small layers run serially without allocating.</summary>
internal void RunParallel(float[] input, int rows, float[] output, CancellationToken cancellation = default)
{
if ((long)rows * weight.Cols * weight.Rows < ParallelMultiplies) Run(input, rows, output, cancellation);
else RunBlocks(input, rows, output, cancellation);
}
// Separate method: its lambda's captured state is allocated on entry, which the serial path must avoid.
void RunBlocks(float[] input, int rows, float[] output, CancellationToken cancellation)
{
Validate(input, rows, weight, bias, output);
int width = weight.Cols, channels = weight.Rows;
// Cancellation is checked around the loop so callers see OperationCanceledException, not AggregateException.
cancellation.ThrowIfCancellationRequested();
Parallel.For(0, (channels + ChannelsPerBlock - 1) / ChannelsPerBlock, Parallelism, block =>
{
for (var channel = block * ChannelsPerBlock; channel < Math.Min(channels, (block + 1) * ChannelsPerBlock); channel++)
{
var weights = weight.Row(channel); var offset = bias?.Data[channel] ?? 0;
for (var row = 0; row < rows; row++)
output[row * channels + channel] = offset + NeuralOps.Dot(weights, input.AsSpan(row * width, width));
}
});
cancellation.ThrowIfCancellationRequested();
}
/// <summary>Uses the shared SIMD dot product with caller-owned output; rejects
/// mismatched dimensions and input/output aliasing. Does not allocate.</summary>
internal static void Run(ReadOnlySpan<float> input, int rows, Tensor weight, Tensor? bias, Span<float> output,
CancellationToken cancellation = default)
{
Validate(input, rows, weight, bias, output);
int width = weight.Cols, channels = weight.Rows;
for (var channel = 0; channel < channels; channel++)
{
cancellation.ThrowIfCancellationRequested();
var weights = weight.Row(channel);
var offset = bias?.Data[channel] ?? 0;
for (var row = 0; row < rows; row++)
output[row * channels + channel] = offset + NeuralOps.Dot(weights, input.Slice(row * width, width));
}
}
static void Validate(ReadOnlySpan<float> input, int rows, Tensor weight, Tensor? bias, Span<float> output)
{
ArgumentNullException.ThrowIfNull(weight);
int width = weight.Cols, channels = weight.Rows;
if (rows < 0 || input.Length != (long)rows * width || output.Length != (long)rows * channels)
throw new ArgumentException("Linear input/output shape does not match " + weight.Name);
if (bias is not null && bias.Data.Length != channels)
throw new ArgumentException("Linear bias shape does not match " + weight.Name);
if (input.Overlaps(output) || weight.Data.AsSpan().Overlaps(output) || (bias is not null && bias.Data.AsSpan().Overlaps(output)))
throw new ArgumentException("Linear output must not overlap its inputs.");
}
}