# How to Handle Resource Lifetimes to Avoid Dangling References in Async Command Submission in Luisa Compute

> Avoid dangling references in Luisa Compute async command submission. Learn to manage resource lifetimes with CommandList::commit and Luisa compute fibers for safe GPU dispatch.

- Repository: [LuisaGroup/luisacompute](https://github.com/luisagroup/luisacompute)
- Tags: how-to-guide
- Published: 2026-03-06

---

**Use `CommandList::commit()` to transfer ownership to a `Commit` object, capture it by move in a `luisa::fiber::async` lambda, and dispatch via `Stream::dispatch()` to ensure resources live until GPU completion.**

Luisa Compute separates command creation from command submission to enable high-performance GPU programming. When submitting commands asynchronously, you must guarantee that every buffer, texture, and command object referenced by a `CommandList` remains valid until the GPU finishes execution. The library provides specific ownership mechanisms in [`include/luisa/runtime/command_list.h`](https://github.com/luisagroup/luisacompute/blob/main/include/luisa/runtime/command_list.h) and [`include/luisa/core/fiber.h`](https://github.com/luisagroup/luisacompute/blob/main/include/luisa/core/fiber.h) to make this safe.

## Understanding the Async Submission Model

### Command Creation vs. Command Dispatch

In Luisa Compute, you build a `CommandList` on the CPU side without immediately sending it to the GPU. This list holds `luisa::unique_ptr<Command>` objects that own both the command data and any embedded resource handles. Only when you call `Stream::dispatch()` does the runtime take control of the list and submit it to the device driver.

### The Role of Stream and CommandList

The `Stream` class defined in [`include/luisa/runtime/stream.h`](https://github.com/luisagroup/luisacompute/blob/main/include/luisa/runtime/stream.h) acts as the submission queue. It accepts either a `CommandList` directly or a `Delegate` lambda that produces one. When operating asynchronously, you move the `CommandList` into a `Commit` object, which the `Stream` holds until the GPU signals completion.

## Ownership Mechanisms for Safe Resource Management

### CommandList Ownership

Every `CommandList` stores commands as `luisa::unique_ptr<Command>` instances. This design ensures that the list exclusively owns its commands and any resource handles they contain. The class is non-copyable (enforcing `concepts::Noncopyable`), preventing accidental duplication that could lead to double-free or use-after-move errors.

### Transferring Ownership with CommandList::Commit

The `CommandList::commit()` method, defined in [`include/luisa/runtime/command_list.h`](https://github.com/luisagroup/luisacompute/blob/main/include/luisa/runtime/command_list.h), moves the entire command list into a `Commit` object. This transfer shifts ownership from your local scope to the runtime. You can return this `Commit` from a lambda or pass it directly to `Stream::dispatch()`, ensuring the list survives until the GPU consumes it.

### Async Execution via luisa::fiber::async

The `luisa::fiber::async` function in [`include/luisa/core/fiber.h`](https://github.com/luisagroup/luisacompute/blob/main/include/luisa/core/fiber.h) launches a lambda on a worker thread and returns a lightweight `event` or `future<T>`. When you capture the `Commit` object by move inside this lambda, the fiber system guarantees the object stays alive until the lambda finishes execution. This bridges the gap between CPU command building and GPU submission without blocking the main thread.

## Complete Async Submission Workflow

The following pattern demonstrates safe asynchronous command submission using the ownership transfer mechanisms:

```cpp
using namespace luisa::compute;

// 1. Build and submit commands asynchronously
auto async_fence = luisa::fiber::async([device = dev, stream = stream] {
    // a) Create a command list with reserved capacity
    auto cmdlist = CommandList::create(/*reserve*/ 64, /*callback*/ 0);

    // b) Fill it with commands referencing resources
    auto buffer = device->create_buffer<float>(1024);
    cmdlist << buffer.copy_from(host_data);

    // c) Transfer ownership to a Commit object
    auto commit = cmdlist.commit();

    // d) Dispatch on the stream; move semantics keep it alive
    stream->dispatch(std::move(commit));
});

// 2. Synchronize later when results are needed
async_fence.wait();

```

In this example, `buffer` is created inside the lambda and captured by the command list. The `Commit` object owns the list, and the fiber captures the commit by move. Until `async_fence.wait()` returns, the runtime guarantees that `buffer` and all commands remain valid in GPU memory.

## Why This Prevents Dangling References

Three specific design choices eliminate dangling references during async submission:

- **Exclusive Ownership Transfer**: `CommandList::commit()` moves the entire command storage into a `Commit`. No raw pointers to the command data remain in user code, preventing use-after-free when the original scope exits.

- **Move Capture in Fibers**: `luisa::fiber::async` requires capturing the `Commit` by value (which invokes the move constructor). The fiber runtime holds the lambda until execution completes, extending the lifetime of all captured resources.

- **Runtime-Managed GPU Submission**: `Stream::dispatch` accepts the `Commit` by rvalue reference and forwards it to the device backend (implemented in [`src/runtime/stream.cpp`](https://github.com/luisagroup/luisacompute/blob/main/src/runtime/stream.cpp) and backend-specific `src/backends/*/stream.h`). The runtime retains ownership until the GPU signals command completion, ensuring resources outlive GPU execution.

## Handling Resources Across Multiple Submissions

When a resource must survive across several asynchronous command batches, use `luisa::shared_ptr` to maintain reference counting:

```cpp
// Create a shared buffer that outlives individual submissions
auto shared_buffer = luisa::make_shared<Buffer<float>>(device->create_buffer<float>(4096));

// First async submission
auto fence1 = luisa::fiber::async([stream, shared_buffer] {
    auto cmd = CommandList::create();
    cmd << shared_buffer->copy_from(data1);
    stream->dispatch(cmd.commit());
});

// Second async submission can reuse the same buffer
auto fence2 = luisa::fiber::async([stream, shared_buffer] {
    auto cmd = CommandList::create();
    cmd << shared_buffer->copy_from(data2);
    stream->dispatch(cmd.commit());
});

// Wait for both; buffer stays alive until both lambdas complete
fence1.wait();
fence2.wait();

```

The `shared_ptr` ensures the `Buffer` remains allocated until the last referencing lambda finishes, preventing premature destruction between submissions.

## Common Pitfalls and Solutions

| Pitfall | Symptom | Solution |
|---------|---------|----------|
| **Capturing CommandList by reference** | Use-after-free or double-free when the stack frame exits before the lambda runs. | Capture the list **by move** inside the fiber lambda, or convert it to a `Commit` first. |
| **Storing raw pointers to Buffers** | GPU accesses freed memory, causing crashes or data corruption. | Use `luisa::shared_ptr<Buffer>` and capture the shared pointer in the lambda. |
| **Modifying CommandList after creating Commit** | Race conditions or assertion failures due to non-copyable semantics. | Build the list completely before calling `commit()`; the list is non-copyable (`concepts::Noncopyable`). |
| **Ignoring the async event** | Host code proceeds before GPU finishes, reading invalid data. | Always call `event.wait()` or `future<T>.wait()` at synchronization points. |

## Key Source Files Reference

Understanding the implementation details helps verify lifetime guarantees:

- **[`include/luisa/runtime/command_list.h`](https://github.com/luisagroup/luisacompute/blob/main/include/luisa/runtime/command_list.h)**: Defines `CommandList` with `unique_ptr` storage and the `commit()` method that produces a `Commit` object.
- **[`include/luisa/runtime/stream.h`](https://github.com/luisagroup/luisacompute/blob/main/include/luisa/runtime/stream.h)**: Declares `Stream::dispatch()` which accepts `Commit` objects by rvalue reference.
- **[`include/luisa/core/fiber.h`](https://github.com/luisagroup/luisacompute/blob/main/include/luisa/core/fiber.h)**: Implements `luisa::fiber::async`, `event`, and `future<T>` for lightweight thread scheduling.
- **[`src/runtime/stream.cpp`](https://github.com/luisagroup/luisacompute/blob/main/src/runtime/stream.cpp)**: Contains `_dispatch` implementation that forwards the command list to device backends while maintaining ownership.
- **`src/backends/*/stream.h`**: Backend-specific implementations (Vulkan, DirectX, Metal) that handle the actual GPU submission and signal completion.

## Summary

- **Transfer ownership explicitly**: Use `CommandList::commit()` to move command ownership into a `Commit` object before async submission.
- **Capture by move**: Always capture `Commit` or `CommandList` by value (move) inside `luisa::fiber::async` lambdas to extend lifetime until execution completes.
- **Use smart pointers for resources**: Wrap persistent `Buffer` or `Texture` objects in `luisa::shared_ptr` when referencing them across multiple async submissions.
- **Synchronize properly**: Wait on the `event` or `future<T>` returned by `luisa::fiber::async` to ensure GPU completion before accessing results.

## Frequently Asked Questions

### What happens if I capture a CommandList by reference in an async lambda?

Capturing by reference creates a dangling reference risk. The `CommandList` is typically a stack variable in the calling function; if the calling scope exits before the worker thread executes the lambda, the reference becomes invalid. This leads to undefined behavior, including use-after-free or double-free errors when the lambda tries to commit or destroy the list. Always capture by value to trigger the move constructor.

### How do I keep a Buffer alive across multiple async submissions?

Store the `Buffer` in a `luisa::shared_ptr<Buffer>` and capture this shared pointer by value in each `luisa::fiber::async` lambda. The reference count increments for each lambda capture and decrements when each lambda completes. The underlying GPU buffer remains allocated until the final reference is released, preventing premature destruction between submissions.

### When should I use luisa::shared_ptr versus unique ownership?

Use `luisa::unique_ptr` (or the move-only `CommandList`/`Commit` pattern) when a resource has a single, linear lifetime tied to one async operation. Use `luisa::shared_ptr` when multiple async submissions, host threads, or GPU operations need concurrent access to the same buffer or texture. Shared pointers add minimal overhead via atomic reference counting but provide the flexibility needed for complex dependency graphs.

### How does Stream::dispatch ensure the CommandList stays alive until GPU completion?

`Stream::dispatch` accepts a `Commit` object by rvalue reference (`Commit&&`), transferring ownership from the caller into the runtime. The implementation in [`src/runtime/stream.cpp`](https://github.com/luisagroup/luisacompute/blob/main/src/runtime/stream.cpp) forwards this to the device backend (e.g., Vulkan, DirectX, or Metal backends in `src/backends/*/stream.h`). The backend retains the `Commit` until the GPU signals command completion via a fence or semaphore. Only then does the destructor of `Commit` (and the contained `CommandList`) run, releasing all referenced resources safely.