Editor/Gpu/SamGpuDecoder.cs
// C# adaptation of SAM 3D Body's promptable decoder. See LICENSE-SAM and THIRD_PARTY_NOTICES.md.
// Copyright (c) Meta Platforms, Inc. and affiliates.
using System;
using System.Threading;
using System.Threading.Tasks;
using Sandbox;
using HumanoidMocapX.TensorInference;
namespace HumanoidMocapX.Inference;
/// <summary>Runs the six decoder layers on the GPU and intermediate pose feedback
/// on a worker thread. Create, run and dispose on the main thread.</summary>
internal sealed class SamGpuDecoder : IDisposable
{
readonly FlowGpuStorage storage;
readonly SamDecoderShape shape;
readonly SamGpuDecoderBlock[] layers = new SamGpuDecoderBlock[SamDecoderShape.Layers];
readonly SamLayerNorm finalNorm;
readonly SamGpuDecoderWorkspace workspace;
readonly TensorBatch batch;
readonly GpuBuffer<float> tokens, image, positions, imagePositions;
readonly float[] cpuTokens, cpuPositions, normalized;
readonly Action prepare, record, update;
ReadOnlyMemory<float> inputTokens, inputImage, inputPositions, inputImagePositions;
Memory<float> destination;
SamPoseFeedback feedback;
CancellationToken cancellation;
int layer;
bool running, disposed;
internal SamGpuDecoder(FlowWeights weights, SamDecoderShape shape, bool hand = false, FlowGpuWeights shared = null)
{
ThreadSafe.AssertIsMainThread();
if (shape.Tokens == 0 || shape.HeadWidth is not (64 or 128)) throw new ArgumentException("GPU SAM needs 64 or 128 channels per head.");
this.shape = shape; var name = hand ? "decoder_hand" : "decoder"; storage = new(shared);
try
{
workspace = new(storage, shape);
tokens = storage.Create<float>(shape.TokenElements); positions = storage.Create<float>(shape.TokenElements);
image = storage.Create<float>(shape.ImageElements); imagePositions = storage.Create<float>(shape.ImageElements);
cpuTokens = new float[shape.TokenElements]; cpuPositions = new float[shape.TokenElements]; normalized = new float[shape.TokenElements];
for (var index = 0; index < layers.Length; index++) layers[index] = new(storage, weights, name + ".layers." + index, shape);
finalNorm = new(weights, name + ".norm_final", shape.Width);
prepare = Prepare; record = Record; update = Update; batch = new();
}
catch { storage.Dispose(); throw; }
}
/// <summary>Writes final normalized tokens. Feedback must be pure CPU code; it
/// receives normalized tokens after layers 0–4 and updates keypoint tokens and
/// positions. Keep input memory unchanged until completion. One call at a time.</summary>
internal async Task RunAsync(ReadOnlyMemory<float> tokenInput, ReadOnlyMemory<float> imageInput,
ReadOnlyMemory<float> tokenPositionInput, ReadOnlyMemory<float> imagePositionInput, Memory<float> output,
SamPoseFeedback poseFeedback, CancellationToken token = default)
{
ThreadSafe.AssertIsMainThread(); ObjectDisposedException.ThrowIf(disposed, this);
ArgumentNullException.ThrowIfNull(poseFeedback);
if (running) throw new InvalidOperationException("Await the current SAM decoder before starting another.");
token.ThrowIfCancellationRequested(); running = true; cancellation = token; feedback = poseFeedback;
inputTokens = tokenInput; inputImage = imageInput; inputPositions = tokenPositionInput; inputImagePositions = imagePositionInput; destination = output;
try
{
await Task.Run(prepare, token); await GameTask.MainThread();
image.SetData<float>(inputImage.Span); imagePositions.SetData<float>(inputImagePositions.Span);
for (layer = 0; layer < layers.Length; layer++)
{
token.ThrowIfCancellationRequested();
tokens.SetData<float>(cpuTokens); positions.SetData<float>(cpuPositions);
await batch.RunAsync(record, tokens, cpuTokens, token);
await Task.Run(update, token); await GameTask.MainThread();
}
token.ThrowIfCancellationRequested(); normalized.AsSpan().CopyTo(destination.Span);
}
finally
{
await GameTask.MainThread(); running = false; cancellation = default; feedback = null;
inputTokens = inputImage = inputPositions = inputImagePositions = default; destination = default;
}
}
void Prepare()
{
SamDecoder.Validate(shape, inputTokens.Span, inputImage.Span, inputPositions.Span, inputImagePositions.Span, destination.Span);
inputTokens.Span.CopyTo(cpuTokens); inputPositions.Span.CopyTo(cpuPositions);
}
void Record()
{ cancellation.ThrowIfCancellationRequested(); layers[layer].Run(workspace, tokens, image, positions, imagePositions, layer == 0); }
void Update()
{
cancellation.ThrowIfCancellationRequested(); finalNorm.Run(cpuTokens, normalized); SamDecoder.Finite(normalized);
if (layer == layers.Length - 1) return;
feedback(layer, normalized, cpuTokens, cpuPositions, cancellation); SamDecoder.Finite(cpuTokens); SamDecoder.Finite(cpuPositions);
}
public void Dispose()
{
ThreadSafe.AssertIsMainThread();
if (disposed) return;
if (running) throw new InvalidOperationException("Await SAM decoder work and readback before disposal.");
disposed = true; batch.Dispose(); storage.Dispose();
}
}