Editor/MotionBricks/TransformerEncoder.cs

Editor-side Transformer encoder implementation used by MotionBricks. It loads layer weight tensors from a WeightSet and implements a stacked TransformerEncoder with packed QKV projection, scaled dot-product attention with optional additive key mask, ReLU feed-forward, and layer normalization. Forward mutates the hidden buffer in place and returns it.

File Access
#nullable enable annotations

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace HumanoidMocap.EditorTools.MotionBricks;

/// <summary>
/// PyTorch <c>nn.TransformerEncoder</c> as released in MotionBricks: post-norm layers,
/// ReLU feed-forward, packed QKV projection, scaled dot-product attention with an optional
/// additive key mask. Hidden states are row-major <c>[sequence, width]</c>.
/// </summary>
public sealed class TransformerEncoder
{
    private readonly Layer[] _layers;
    private readonly int _width;
    private readonly int _heads;

    private sealed record Layer( Tensor InProj, Tensor InBias, Tensor OutProj, Tensor OutBias,
        Tensor Norm1, Tensor Norm1Bias, Tensor Linear1, Tensor Linear1Bias, Tensor Linear2, Tensor Linear2Bias,
        Tensor Norm2, Tensor Norm2Bias );

    public TransformerEncoder( WeightSet weights, string prefix, int layers, int width, int heads )
    {
        _width = width;
        _heads = heads;
        _layers = new Layer[layers];
        for ( var i = 0; i < layers; i++ )
        {
            var p = $"{prefix}.layers.{i}.";
            _layers[i] = new Layer(
                weights.Shaped( p + "self_attn.in_proj_weight", width, width * 3 ),
                weights[p + "self_attn.in_proj_bias"],
                weights.Shaped( p + "self_attn.out_proj.weight", width, width ),
                weights[p + "self_attn.out_proj.bias"],
                weights[p + "norm1.weight"], weights[p + "norm1.bias"],
                weights[p + "linear1.weight"], weights[p + "linear1.bias"],
                weights[p + "linear2.weight"], weights[p + "linear2.bias"],
                weights[p + "norm2.weight"], weights[p + "norm2.bias"] );
        }
    }

    /// <summary>
    /// Runs every layer over <paramref name="hidden"/> (<c>[sequence, width]</c>, replaced in
    /// place). <paramref name="keyMask"/> is added to attention scores per key (0 or -inf).
    /// </summary>
    public float[] Forward( float[] hidden, int sequence, float[]? keyMask, CancellationToken cancel )
    {
        foreach ( var layer in _layers )
        {
            cancel.ThrowIfCancellationRequested();
            var attention = Attention( hidden, sequence, layer, keyMask );
            NeuralOps.AddInPlace( hidden, attention );
            NeuralOps.LayerNorm( hidden, sequence, _width, layer.Norm1, layer.Norm1Bias );

            var feed = NeuralOps.Linear( hidden, sequence, layer.Linear1, layer.Linear1Bias );
            NeuralOps.Relu( feed );
            feed = NeuralOps.Linear( feed, sequence, layer.Linear2, layer.Linear2Bias );
            NeuralOps.AddInPlace( hidden, feed );
            NeuralOps.LayerNorm( hidden, sequence, _width, layer.Norm2, layer.Norm2Bias );
        }
        return hidden;
    }

    private float[] Attention( float[] hidden, int sequence, Layer layer, float[]? keyMask )
    {
        var qkv = NeuralOps.Linear( hidden, sequence, layer.InProj, layer.InBias );
        var headWidth = _width / _heads;
        var scale = 1f / MathF.Sqrt( headWidth );
        var merged = new float[sequence * _width];
        var stride = _width * 3;
        var scores = new float[sequence];
        for ( var h = 0; h < _heads; h++ )
        {
            var qOffset = h * headWidth;
            var kOffset = _width + h * headWidth;
            var vOffset = 2 * _width + h * headWidth;
            for ( var q = 0; q < sequence; q++ )
            {
                var query = qkv.AsSpan( q * stride + qOffset, headWidth );
                for ( var k = 0; k < sequence; k++ )
                {
                    scores[k] = NeuralOps.Dot( query, qkv.AsSpan( k * stride + kOffset, headWidth ) ) * scale;
                    if ( keyMask is not null )
                        scores[k] += keyMask[k];
                }
                NeuralOps.SoftmaxInPlace( scores.AsSpan( 0, sequence ) );
                var output = merged.AsSpan( q * _width + h * headWidth, headWidth );
                for ( var k = 0; k < sequence; k++ )
                {
                    var weight = scores[k];
                    if ( weight == 0f )
                        continue;
                    var value = qkv.AsSpan( k * stride + vOffset, headWidth );
                    for ( var d = 0; d < headWidth; d++ )
                        output[d] += weight * value[d];
                }
            }
        }
        return NeuralOps.Linear( merged, sequence, layer.OutProj, layer.OutBias );
    }
}