Status: current
Per-call scratch comes from an arena owned by each Queue, borrowed through WorkspaceLease. Sizing uses BumpAllocator::measuring() and required_bytes(), which replay the solve's allocation sequence. User summary: Workspaces come from the queue's arena. Implementation: WorkspaceArena in src/queue.hh, WorkspaceLease in src/util/queue-impl.cc.
A local UnifiedVector frees on return, while the kernels that use it are only enqueued. A lease frees nothing. The memory belongs to the Queue, so the pointer stays valid, but the next lease reuses released bytes, so in-flight work can be overwritten. That is safe on an in-order queue. For out-of-order queues see release on out-of-order queues.
Leases nest, and the arena never moves leased memory. Releasing in reverse acquisition order returns bytes at once. Out-of-order release is safe but wastes bytes until the later leases are released, and debug builds assert on it. A lease belongs to one Queue and is not thread-safe.
The old bytes are reclaimed only when the new lease dies, so a loop that reassigns stacks one loan per iteration. Call ws.release() first to reuse them. Move assignment does not diagnose this.
span(), the conversion to Span<std::byte>, and data() are lvalue-only. The rvalue overloads are deleted, since a pointer from a temporary lease is stale on the next line. These three compile cleanly under -Wall -Wextra -Wdangling and alias:
The correct spelling adds auto to the first line.
release() is idempotent and never throws. On an in-order queue it is free. On an out-of-order queue it blocks until the queue is idle, but only when the release hands bytes back (the innermost live lease). Otherwise the next borrow could be written while earlier work still reads those bytes. To avoid the stall, keep the lease alive until you have waited on the work, or pass your own span.
The drain covers only the queue the lease came from:
workspace.hh cannot include util/sycl-span.hh, which includes it back. Span and Queue are forward declared inside namespace batchlas, and the accessors that name Span are defined out of line. A forward declaration in another namespace names a different type, and the error appears far away.
BumpAllocator::measuring() runs the same bump arithmetic over a fictitious, maximally aligned, unbounded region. required_bytes() is the smallest real pool that serves the same sequence, with at most one alignment quantum to spare. This holds because every alignment is max(device_align, alignof(T)), one uniform value, so the layout depends only on offsets from the pool start.
allocate<T>() checks the rounded size against the bytes left from the unaligned cursor, but advances the cursor by the raw extent. Summing the advances therefore undersizes a pool. Sizing mode keeps the larger of the check's need and the data's need for each allocation, and takes the high-water mark.
required_bytes() rounds that mark to the coarsest alignment asked for, because callers add a callee's size to their own total and re-serve it with allocate<std::byte>(), which rounds again. Size with measuring() and required_bytes(), never by hand, and make a *_buffer_size and its run path branch identically.
Each algorithm describes its workspace once, in a *_layout function that both its *_buffer_size and its implementation call:
A layout function is pure with respect to the workspace. It reads the caller's views but never workspace memory, and never launches a kernel.
workspace_bytes takes only a lambda, so ADL cannot find it from another namespace. The BATCHLAS_NO_GLOBAL_NAMES shim re-exports it at global scope.