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

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 and 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 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, 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 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:

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 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:

// 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: Defines CommandList with unique_ptr storage and the commit() method that produces a Commit object.
  • include/luisa/runtime/stream.h: Declares Stream::dispatch() which accepts Commit objects by rvalue reference.
  • include/luisa/core/fiber.h: Implements luisa::fiber::async, event, and future<T> for lightweight thread scheduling.
  • 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →