Editor/Inference/FlowAttention.cs
// C# adaptation of FlowHMR's narrowband_v2m attention. See THIRD_PARTY_NOTICES.md.
using System;
using System.Threading;

namespace HumanoidMocapX.Inference;

/// <summary>Batch-one attention for motion, image and pose transformers.
/// Buffers belong to the instance; concurrent calls are unsupported.</summary>
internal sealed class FlowAttention
{
    const int MotionWindow = 60;
    const int VideoWindow = 3;
    readonly int tokens, keyTokens, width, heads, headWidth;
    readonly float scale;
    readonly float[] scores;
    readonly (int Start, int Count, int VideoStart, int VideoCount)[] ranges;

    /// <param name="frames">Frames per stream, without padding.</param>
    /// <param name="jointStreams">True for concatenated motion/video; false for the full context-refiner attention.</param>
    internal FlowAttention(int frames, int width, int heads, bool jointStreams)
    {
        if (frames < 1 || frames > FlowMotion.MaximumFrames || width < 1 || heads < 1 || width % heads != 0)
            throw new ArgumentException("Invalid FlowHMR attention frame count or head dimensions.");
        tokens = checked(frames * (jointStreams ? 2 : 1));
        keyTokens = tokens;
        this.width = width; this.heads = heads; headWidth = width / heads;
        scale = 1f / MathF.Sqrt(headWidth);
        scores = new float[jointStreams ? Math.Min(frames, MotionWindow * 2 + 1) + Math.Min(frames, VideoWindow * 2 + 1) : frames];
        ranges = new (int, int, int, int)[tokens];
        for (var query = 0; query < tokens; query++)
        {
            if (!jointStreams) { ranges[query] = (0, frames, 0, 0); continue; }
            var isMotion = query < frames;
            var frame = query % frames;
            var motionStart = Math.Max(0, frame - MotionWindow);
            var motionEnd = Math.Min(frames, frame + MotionWindow + 1);
            var videoStart = Math.Max(0, frame - VideoWindow);
            var videoEnd = Math.Min(frames, frame + VideoWindow + 1);
            ranges[query] = (motionStart, isMotion ? motionEnd - motionStart : 0,
                frames + videoStart, videoEnd - videoStart);
        }
    }

    /// <summary>Full cross-attention with independently sized query and context sequences.</summary>
    internal FlowAttention(int queries, int keys, int width, int heads)
    {
        if (queries < 1 || keys < 1 || queries > FlowMotion.MaximumFrames || keys > FlowMotion.MaximumFrames
            || width < 1 || heads < 1 || width % heads != 0 || (long)Math.Max(queries, keys) * width > int.MaxValue)
            throw new ArgumentException("Invalid cross-attention token count or head dimensions.");
        tokens = queries; keyTokens = keys; this.width = width; this.heads = heads; headWidth = width / heads;
        scale = 1f / MathF.Sqrt(headWidth); scores = new float[keys]; ranges = new (int, int, int, int)[queries];
        for (var query = 0; query < queries; query++) ranges[query] = (0, keys, 0, 0);
    }

    /// <summary>Copies two (start,count) ranges per query for the GPU attention kernel.</summary>
    internal void CopyKeyRanges(Span<uint> output)
    {
        if (output.Length != (long)tokens * 4) throw new ArgumentException("Attention key ranges need four integers per token.");
        for (var token = 0; token < tokens; token++)
        {
            var range = ranges[token];
            output[token * 4] = (uint)range.Start; output[token * 4 + 1] = (uint)range.Count;
            output[token * 4 + 2] = (uint)range.VideoStart; output[token * 4 + 3] = (uint)range.VideoCount;
        }
    }

    /// <summary>Accepts [token,width] Q/K/V after normalization and position encoding.
    /// Cross-attention uses its context count for K/V. Writes [query,width] output,
    /// rejecting shape mismatches and input/output aliasing.</summary>
    internal void Run(ReadOnlySpan<float> query, ReadOnlySpan<float> key, ReadOnlySpan<float> value,
        Span<float> output, CancellationToken cancellation = default)
    {
        if (query.Length != (long)tokens * width || key.Length != (long)keyTokens * width || value.Length != key.Length || output.Length != query.Length)
            throw new ArgumentException("Attention tensors do not match their query/context dimensions.");
        if (query.Overlaps(output) || key.Overlaps(output) || value.Overlaps(output))
            throw new ArgumentException("Attention output must not overlap its inputs.");
        for (var token = 0; token < tokens; token++)
        {
            cancellation.ThrowIfCancellationRequested();
            var range = ranges[token];
            var count = range.Count + range.VideoCount;
            for (var head = 0; head < heads; head++)
            {
                var q = query.Slice(token * width + head * headWidth, headWidth);
                for (var i = 0; i < count; i++)
                {
                    var source = i < range.Count ? range.Start + i : range.VideoStart + i - range.Count;
                    scores[i] = NeuralOps.Dot(q, key.Slice(source * width + head * headWidth, headWidth)) * scale;
                }
                NeuralOps.SoftmaxInPlace(scores.AsSpan(0, count));
                var destination = output.Slice(token * width + head * headWidth, headWidth);
                destination.Clear();
                for (var i = 0; i < count; i++)
                {
                    var source = i < range.Count ? range.Start + i : range.VideoStart + i - range.Count;
                    var v = value.Slice(source * width + head * headWidth, headWidth);
                    var probability = scores[i];
                    for (var channel = 0; channel < headWidth; channel++) destination[channel] += probability * v[channel];
                }
            }
        }
    }
}