---
title: "Chain dependent GPU compute passes in C#"
url: https://recursiveai.org/onegpu/cookbook/chain-dependent-compute-passes/
description: "Record dependent GPU compute passes in C#, submit once, count passes and dispatches, and read the final matrix with a runnable OneGpu example."
version: "1.0.0"
---

# Chain dependent GPU compute passes in C#

**OneGpu** is a free, open-source GPU compute and rendering library for .NET from Recursive AI, on Vulkan, Metal and WebGPU. MIT or Apache-2.0, no bundled native binaries.

[NuGet](https://www.nuget.org/packages/RecursiveAI.OneGpu.Backends) · [GitHub](https://github.com/recursive-ai-oss/onegpu-csharp) · [Docs](https://recursiveai.org/onegpu/docs/) · [Download example (.zip)](https://recursiveai.org/onegpu/cookbook/chain-dependent-compute-passes/chain-dependent-compute-passes.zip)

Feed one GPU matrix product into another without intermediate readback. Requires Vulkan 1.3 on Windows or Linux.

```shell
dotnet add package \
    RecursiveAI.OneGpu.Backends
dotnet add package \
    RecursiveAI.OneGpu.Kernels
```

Requires [.NET 10](https://dotnet.microsoft.com/download/dotnet/10.0) (net10.0); .NET 9 and earlier aren’t supported.

```csharp
using RecursiveAI.OneGpu;
using RecursiveAI.OneGpu.Kernels;

using var device =
    GpuBackends.CreateDevice();
using var a = device.CreateBuffer<float>(
    [1, 2, 3, 4]);
using var b = device.CreateBuffer<float>(
    [5, 6, 7, 8]);
using var intermediate = device.CreateBuffer<float>(4);
using var result = device.CreateBuffer<float>(4);
using var gemm =
    new Gemm(device, GemmElementType.F32);
var commands = device.BeginCommands();
var shape = new GemmProblem(2, 2, 2);
gemm.Encode(commands, shape, a, b, intermediate);
gemm.Encode(commands, shape, intermediate, b, result);
Console.WriteLine(commands.ComputePassCount);
Console.WriteLine(commands.DispatchCount);
device.Submit(commands);
var values = new float[4];
await result.ReadAsync(values);
Console.WriteLine(string.Join(", ", values));
```

Computes `(A × B) × B`; this run prints:

```text
2
2
249, 290, 565, 658
```

Unzip [the example](https://recursiveai.org/onegpu/cookbook/chain-dependent-compute-passes/chain-dependent-compute-passes.zip), then `dotnet run`.

## The full program

```csharp
using RecursiveAI.OneGpu;
using RecursiveAI.OneGpu.Kernels;

using var device = GpuBackends.CreateDevice(new() { Adapter = "discrete" });
using var a = device.CreateBuffer<float>([1, 2, 3, 4]);
using var b = device.CreateBuffer<float>([5, 6, 7, 8]);
using var intermediate = device.CreateBuffer<float>(4);
using var output = device.CreateBuffer<float>(4);
using var gemm = new Gemm(device, GemmElementType.F32);
var commands = device.BeginCommands();
gemm.Encode(commands, new GemmProblem(2, 2, 2), a, b, intermediate);
gemm.Encode(commands, new GemmProblem(2, 2, 2), intermediate, b, output);
Console.WriteLine($"Passes: {commands.ComputePassCount}, dispatches: {commands.DispatchCount}");
device.Submit(commands);
var values = new float[4];
await output.ReadAsync(values);
Console.WriteLine(string.Join(", ", values));
if (!values.SequenceEqual(new float[] { 249, 290, 565, 658 }))
    throw new InvalidOperationException("Unexpected matrix product");
```

Output on Linux with an NVIDIA GeForce RTX 4070 via Vulkan:

```text
Passes: 2, dispatches: 2
249, 290, 565, 658
```

`Adapter = "discrete"` selects the first matching discrete GPU; if none is available, device creation throws `GpuUnsupportedException`.

`GemmProblem(M, N, K)` takes output rows, output columns, then the shared inner dimension: A is M × K, B is K × N, and the result is M × N.

The full program checks every final value and throws `InvalidOperationException` if the product differs. A missing capability or exceeded device limit raises `GpuUnsupportedException`.

## Variations

For your own shaders, use the pipeline and bind-group setup in [Run a compute shader in C#](https://recursiveai.org/onegpu/cookbook/run-a-compute-shader-in-csharp/), then record the second compute pass in the same encoder with the first pass’s output buffer bound as its input.

To compute `(A × B) × A` instead, replace the second `Encode` call in the full program:

```csharp
gemm.Encode(commands, new GemmProblem(2, 2, 2), intermediate, a, output);
```

Update its expected-value array to `[85, 126, 193, 286]`. This run prints:

```text
Passes: 2, dispatches: 2
85, 126, 193, 286
```

## Related recipes

- [Multiply matrices on the GPU](https://recursiveai.org/onegpu/cookbook/multiply-matrices-on-the-gpu/)
- [Read a GPU buffer back to the CPU](https://recursiveai.org/onegpu/cookbook/read-back-a-gpu-buffer/)
- [Collect Vulkan validation messages](https://recursiveai.org/onegpu/cookbook/collect-gpu-validation-messages/)

## The short answer

Record both operations in one `GpuCommandEncoder`, with the first operation’s output buffer bound as an input to the second. Submit that encoder once. OneGpu’s ordering contract says a compute pass sees every write from the passes before it in the same encoder. You do not need to read the intermediate buffer back to C# between the two passes.

The example computes two ordinary matrix products with the released kernel package. `A` is `[[1, 2], [3, 4]]` and `B` is `[[5, 6], [7, 8]]`. The intermediate result is `[[19, 22], [43, 50]]`; the final result is `[[249, 290], [565, 658]]`.

## How do you chain GPU compute passes in C#?

Allocate the intermediate and final buffers before recording. They have different roles: the intermediate holds the first product, and the final buffer receives the second. Keep both alive through submission and readback. In this example, each contains four float32 elements because both products have a two-by-two output shape.

`Gemm.Encode` records work into the encoder you pass it; it does not submit that encoder. This lets the caller build a dependency chain without a separate submission between operations. The second call receives `intermediate` where the first call received `a`. Both calls use `b` as the right-hand matrix. Swapping that argument is a mathematical change, not a synchronization setting.

The [passes and ordering documentation](https://recursiveai.org/onegpu/docs/compute/#passes-and-ordering) defines the relevant guarantee: passes recorded one after another run in that order, each sees the earlier writes, and a later write to a buffer wins. On Vulkan, the backend records the passes into the same command buffer and separates them with memory barriers. The library handles that barrier boundary; this recipe does not expose raw Vulkan barrier calls.

That guarantee is about successive passes. It is not permission to introduce a race inside one shader dispatch, or to alias inputs and outputs of an operation that does not support in-place execution. Separate intermediate and output buffers make the dependency explicit. If the algorithm needs a global stage boundary between workgroups, keep the stages as separate passes rather than assuming all invocations of one dispatch synchronize with each other.

Read `ComputePassCount` and `DispatchCount` before calling `Submit`. They count recorded compute passes and recorded `Dispatch` or `DispatchIndirect` calls, respectively. In this run the values are two and two. They are not GPU execution times or a count of completed operations. The documented counters even include an empty grid, although that grid runs no work.

Finally, read back only the final buffer with `ReadAsync`. The awaited readback produces the CPU array that is printed and compared. A chain that only records commands but never submits them has not performed these products. Likewise, counters alone cannot establish that the data dependency or the matrix dimensions were correct; compare the resulting values against the calculation you intended.
