feat(runtime): always-on TRB temporary variable buffer (internal, no user switch) - #1198
Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughAdds ChangesTRB Serial Tensor Buffer Pool Plan
Estimated code review effort🎯 2 (Simple) | ⏱️ ~15 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces an implementation plan for a TRB Temporary Variable Buffer to optimize memory allocation overhead. The review feedback highlights three key areas for improvement: simplifying the cleanup contract by leveraging the existing validate_runtime_impl gateway, resolving a design contradiction between the aggregate budget contract and segmented chunk allocation, and adding a lightweight active-run guard to prevent silent data corruption from concurrent runs.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
docs/trb-serial-tensor-buffer-pool-plan.md (3)
11-11: 📐 Maintainability & Code Quality | 🔵 TrivialConsider using English consistently throughout the document.
Line 11 uses Chinese characters "临时变量" while the rest of the document is entirely in English. For consistency and to avoid encoding issues in some toolchains, consider using only English: "This plan uses 'temporary variable buffer' for the same concept as 'linshi biancun buffer'."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/trb-serial-tensor-buffer-pool-plan.md` at line 11, The document mixes Chinese and English, so update the referenced text in the plan to use English consistently. Replace the Chinese phrase in the buffer description with an English equivalent in the same section of docs/trb-serial-tensor-buffer-pool-plan.md, keeping the wording aligned with the existing terminology used elsewhere in the document.
154-157: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winClarify finalize cleanup ordering when a run is active.
The text says to "log it, release retained chunks" if finalize sees an active run. For defensive correctness, explicitly state whether
end_temporary_buffer_run()should be called beforeclear()to reset theactive_flag, or ifclear()alone is sufficient because the runner is being destroyed. This prevents implementers from leaving the flag in an inconsistent state if any post-finalize diagnostics check it.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/trb-serial-tensor-buffer-pool-plan.md` around lines 154 - 157, Clarify the finalize cleanup order in finalize_common()/sim finalize when an active temporary-buffer run is present: specify whether end_temporary_buffer_run() must be called before clear() to reset active_, or whether clear() alone is sufficient because the runner is being torn down. Update the contract wording so implementers know how to handle the active run state consistently and avoid leaving diagnostics-visible flags stale.
526-552: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAdd unit tests for configuration edge cases.
Consider adding focused unit tests for:
configure_temporary_bufferwith same budget twice is a no-op (no reallocation);configure_temporary_bufferwhile a run is active fails with clear error;begin_temporary_buffer_runfailure path (e.g., OOM during chunk allocation) is handled correctly.These complement the existing test list and guard against reconfiguration bugs.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/trb-serial-tensor-buffer-pool-plan.md` around lines 526 - 552, Add focused unit tests for the temporary-buffer configuration edge cases: verify configure_temporary_buffer is a no-op when called twice with the same budget, verify configure_temporary_buffer rejects changes while a run is active with a clear error, and verify begin_temporary_buffer_run handles allocation failure/OOM correctly. Place these alongside the existing buffer unit tests and use the same temporary-buffer pool APIs and run lifecycle symbols so the new coverage matches the current single-active-run model.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/trb-serial-tensor-buffer-pool-plan.md`:
- Around line 448-460: The bind pseudocode currently assumes
begin_temporary_buffer_run() always succeeds and sets temp_run_active=true
unconditionally, but the HostApi callback returns a bool. Update the bind flow
to check the return value from begin_temporary_buffer_run() and fail the bind
path immediately with a clear error if it returns false, before any TensorLease
recording or device memory work; keep temp_run_active=true only after a
successful begin_temporary_buffer_run() call.
---
Nitpick comments:
In `@docs/trb-serial-tensor-buffer-pool-plan.md`:
- Line 11: The document mixes Chinese and English, so update the referenced text
in the plan to use English consistently. Replace the Chinese phrase in the
buffer description with an English equivalent in the same section of
docs/trb-serial-tensor-buffer-pool-plan.md, keeping the wording aligned with the
existing terminology used elsewhere in the document.
- Around line 154-157: Clarify the finalize cleanup order in
finalize_common()/sim finalize when an active temporary-buffer run is present:
specify whether end_temporary_buffer_run() must be called before clear() to
reset active_, or whether clear() alone is sufficient because the runner is
being torn down. Update the contract wording so implementers know how to handle
the active run state consistently and avoid leaving diagnostics-visible flags
stale.
- Around line 526-552: Add focused unit tests for the temporary-buffer
configuration edge cases: verify configure_temporary_buffer is a no-op when
called twice with the same budget, verify configure_temporary_buffer rejects
changes while a run is active with a clear error, and verify
begin_temporary_buffer_run handles allocation failure/OOM correctly. Place these
alongside the existing buffer unit tests and use the same temporary-buffer pool
APIs and run lifecycle symbols so the new coverage matches the current
single-active-run model.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: e992fafc-1d81-49ff-a3d7-3cea3aeb9bfd
📒 Files selected for processing (1)
docs/trb-serial-tensor-buffer-pool-plan.md
befb5c5 to
3bf8392
Compare
Two things: (1) replace explicit budget with a self-sizing AUTO buffer, (2) title/body vs. actual diff1. Reconsider the core Decision: drop the explicit budget, make the buffer self-sizing. This is a direction-level suggestion, not a tweak — it changes the plan's Decision, so flagging it up front. The current design pushes model-shape knowledge (hidden size, vocab pad, max batch/token) onto the serving/runner owner via Concretely:
I want to be honest about what this gives up, because these were presumably why you chose explicit budget:
The upside is that "zero-config, just make the churn go away" is a real product win, and it keeps model-shape knowledge out of the runtime entirely — which the plan already lists as a goal. 2. Title/body vs. diff. The PR says "documentation-only... Not run", but the diff carries ~2400 lines of implementation + C++/Python UTs beyond the 592-line plan doc. Those tests should be actually run / CI-green before merge, and the title probably wants |
Re-review after the AUTO rework — looks good, two loose endsThe AUTO switch landed well, and the Two loose ends before merge:
Neither is a design issue — the mechanism itself is in good shape now. |
|
Two more cleanups, both about "make it the cleanest version": 1. Drop the
if "max_temporary_buffer_bytes" in config:
raise ValueError("max_temporary_buffer_bytes has been removed; use temporary_buffer_mode='auto'")plus its UT in 2. I checked the CANN 9.0.0 runtime headers: the only device-memory primitives are Neither blocks the design — both are polish toward the minimal/cleanest form. |
ebce56d to
1f72a37
Compare
6ee7a45 to
59931b1
Compare
|
Pushed a commit directly (maintainer edit, Rationale: the AUTO buffer is always result-equivalent to the old per-run malloc/free path — only faster — and TRB runs are already serial per runner (the static arena pool enforces this), so there was no correctness reason to gate it behind opt-in. Exposing it through Python / pybind / C-ABI / ChipWorker pushed an internal allocation decision up the whole stack for no benefit. Removed: Docs (RUNTIME_LOGIC.md §2.4, task-flow.md) and tests retargeted to the always-on behavior; OFF-path test coverage now nulls the callbacks instead of toggling an enable flag. Verified locally: a2a3+a5 sim+onboard runtimes build clean; Python UT 92 passed; trb |
|
Pushed a follow-up maintainer commit ( The prior commit kept a ~330-line
Net −442 lines on top of the previous commit. No behavior change — same always-on retained-buffer reuse, just without the allocator-class indirection. Verified: a2a3+a5 sim+onboard build clean; Python UT 92 passed; |
73e85d5 to
2073441
Compare
Cut the per-run device_malloc / device_free of ordinary non-child tensors on
the tensormap_and_ringbuffer (TRB) bind path. TRB now stages them through a
single runner-scoped retained buffer that grows to the high-water packed size
and is reused across runs; steady-state (stable decode shapes) converges to
zero temporary allocations.
This is an internal allocation optimization with no user-facing switch — it is
always on for TRB and always result-equivalent to the old malloc/free path
(only faster). TRB runs are already serial per runner (enforced by the static
arena pool), so there is no correctness reason to gate it.
Mechanism (all host-side):
- HostApi exposes two slot accessors, get_retained_temp_buffer /
set_retained_temp_buffer. DeviceRunnerBase (onboard + sim) just remembers a
{addr, size} slot across runs and frees it once at finalize; it is not an
allocator.
- runtime_maker (a2a3 + a5) owns the whole mechanism in a file-local
RetainedTempBump: pack the run's non-child, non-empty tensors to a
1024-aligned required size, read the slot, grow (device_free old +
device_malloc new + write back) only when too small, then bump-slice each
tensor. 1024-byte alignment is what device_malloc already guarantees, so the
base and every slice stay aligned. Grow failure fails the run (no mid-run
malloc fallback); a backend that leaves the slot callbacks null falls back to
per-tensor device_malloc transparently.
- Ownership: TensorLease + TensorReleaseKind replace the implicit "every
recorded dev_ptr was malloc'd this run" assumption. Slice leases release as
no-ops; the Free kind covers the fallback path. child-memory and zero-byte
tensors are skipped (passed through unchanged).
Docs: RUNTIME_LOGIC.md §2.4 and task-flow.md describe the retained-buffer
mechanics. hbg is unaffected (host orchestration does not use the callbacks).
Verified: a2a3 + a5 sim + onboard build clean; Python worker UT 92 passed;
tensormap_and_ringbuffer prepared_callable + orch_so_cache a2a3sim ST pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2073441 to
61f49db
Compare
Fixes hw-native-sys#2151 host_build_graph's bind path did a device_malloc + H2D + device_free for every host-side tensor argument on every run. tensormap_and_ringbuffer stopped doing that in hw-native-sys#1198, which added a runner-scoped retained staging buffer that grows to the high-water packed size and is bump-sliced each run, so steady state converges to zero temporary device allocations. The platform side of that mechanism has never been TRB-specific — HostApi has exposed get/set_retained_temp_buffer generically since hw-native-sys#1198, and hbg already consumes three of DeviceRunnerBase's four per-pipeline-slot retained storages. This makes it consume the fourth. The mechanism now has one definition. RetainedTempBump moves out of the two TRB runtime_maker.cpp files into src/common/utils/retained_temp_bump.h, and the TensorLease ledger out of the three runtime.h files into src/common/utils/tensor_lease.h, with its release loop in utils/tensor_lease_release.h. The lease type is split from its release because both runtime.h files are included by AICore and AICPU translation units, which cannot see the host API. RetainedTempBump::begin() takes a byte count rather than ChipStorageTaskArgs, which keeps that header free of task_interface types and leaves each caller's packing loop next to the staging loop it has to mirror; the packing rule, the 1024-byte slice alignment and the grow rule are unchanged, so TRB behaves exactly as before. Diagnostics the two helpers used to log are now emitted by the callers, which have the tensor index and the logging backend. hbg's ledger takes TRB's shape: TensorPair becomes TensorLease and gains a TensorReleaseKind, and validate releases through release_tensor_leases instead of device_free'ing every recorded pointer — a bump slice is not a separate allocation and must not be freed. Slices are recorded as BufferNoop; the buffer itself is freed once per Worker in DeviceRunnerBase::clear_temporary_buffer. hbg's staging loop skips empty tensors exactly as TRB's does, so the packing predicate and the slicing predicate stay word for word identical: a tensor counted by one but not the other shifts every later slice off the offsets the buffer was sized from. bind now clears the ledger on entry. A run whose validate never runs otherwise leaves its entries for the next bind — on the onboard native path a failed prepare still reaches validate_runtime_impl through cleanup_failed_prepare, but a finalize whose attach_current_thread fails skips validation entirely. Stale entries were a leak before; with a reused buffer they name offsets the next bind re-slices, so validate would copy that run's bytes back to the earlier run's host pointer. The buffer is grown inside the args bind-phase span rather than before it, so the one device allocation the phase can make is attributed to it and "steady state allocates nothing here" is a measurement rather than a definition. Correctness rests on the retained slot being per pipeline_slot: a native run holds its slot from bind through validate, and a concurrent reservation is admitted only on a distinct slot, so no other run can re-slice a buffer whose slices are live. That is the same property hbg's Graph Definition blocks (hw-native-sys#1988) and SM mirror (hw-native-sys#2013) already rely on. The H2D of a staged tensor still precedes its registration with the run's host accessor, so a reused slice cannot expose the previous run's bytes to orchestration. test_depth_two_slots_own_separate_resources asserted that neither hbg pipeline slot held a retained buffer. It now asserts both hold one and that the two are distinct, which is the regression barrier for the change. test_retained_temp_bump.cpp covers the mechanism itself against a fake HostApi: first allocation, reuse without allocation, grow, slice alignment and disjointness, slice miss, and a failed grow leaving the slot cleared rather than naming the buffer it just freed.
Fixes hw-native-sys#2151 host_build_graph's bind path did a device_malloc + H2D + device_free for every host-side tensor argument on every run. tensormap_and_ringbuffer stopped doing that in hw-native-sys#1198, which added a runner-scoped retained staging buffer that grows to the high-water packed size and is bump-sliced each run, so steady state converges to zero temporary device allocations. The platform side of that mechanism has never been TRB-specific — HostApi has exposed get/set_retained_temp_buffer generically since hw-native-sys#1198, and hbg already consumes three of DeviceRunnerBase's four per-pipeline-slot retained storages. This makes it consume the fourth. The mechanism now has one definition. RetainedTempBump moves out of the two TRB runtime_maker.cpp files into src/common/utils/retained_temp_bump.h, and the TensorLease ledger out of the three runtime.h files into src/common/utils/tensor_lease.h, with its release loop in utils/tensor_lease_release.h. The lease type is split from its release because both runtime.h files are included by AICore and AICPU translation units, which cannot see the host API. RetainedTempBump::begin() takes a byte count rather than ChipStorageTaskArgs, which keeps that header free of task_interface types and leaves each caller's packing loop next to the staging loop it has to mirror; the packing rule, the 1024-byte slice alignment and the grow rule are unchanged, so TRB behaves exactly as before. Diagnostics the two helpers used to log are now emitted by the callers, which have the tensor index and the logging backend. hbg's ledger takes TRB's shape: TensorPair becomes TensorLease and gains a TensorReleaseKind, and validate releases through release_tensor_leases instead of device_free'ing every recorded pointer — a bump slice is not a separate allocation and must not be freed. Slices are recorded as BufferNoop; the buffer itself is freed once per Worker in DeviceRunnerBase::clear_temporary_buffer. hbg's staging loop skips empty tensors exactly as TRB's does, so the packing predicate and the slicing predicate stay word for word identical: a tensor counted by one but not the other shifts every later slice off the offsets the buffer was sized from. A slice is bounded as `bytes > capacity_ - aligned` rather than as `aligned + bytes > capacity_`. A caller's byte count comes from ChipTensor::nbytes(), an unchecked uint64_t product, so that sum can wrap and compare small — the slice would then be handed out as a pointer past the buffer for copy_to_device to write through. Every overflow upstream, in a size, in a packed total, or in align_up itself, now lands as an ordinary slice miss instead. align_up stays unchecked: it is the same failure one step earlier, and checking it would thread an error path through both packing loops. bind now clears the ledger on entry. A run whose validate never runs otherwise leaves its entries for the next bind — on the onboard native path a failed prepare still reaches validate_runtime_impl through cleanup_failed_prepare, but a finalize whose attach_current_thread fails skips validation entirely. Stale entries were a leak before; with a reused buffer they name offsets the next bind re-slices, so validate would copy that run's bytes back to the earlier run's host pointer. The buffer is grown inside the args bind-phase span rather than before it, so the one device allocation the phase can make is attributed to it and "steady state allocates nothing here" is a measurement rather than a definition. Correctness rests on the retained slot being per pipeline_slot: a native run holds its slot from bind through validate, and a concurrent reservation is admitted only on a distinct slot, so no other run can re-slice a buffer whose slices are live. That is the same property hbg's Graph Definition blocks (hw-native-sys#1988) and SM mirror (hw-native-sys#2013) already rely on. The H2D of a staged tensor still precedes its registration with the run's host accessor, so a reused slice cannot expose the previous run's bytes to orchestration. A pure OUT tensor is staged but never copied in and never zero-filled, so on a run that reuses the buffer unchanged and packs to the same offsets it reads the previous round's own bytes; a first or grown allocation carries uninitialized allocator residue as before, since begin() neither preserves nor initializes a buffer it replaces. Both runtimes' RUNTIME_LOGIC.md now say so. test_depth_two_slots_own_separate_resources asserted that neither hbg pipeline slot held a retained buffer. It now asserts both hold one and that the two are distinct, which is the regression barrier for the change. test_retained_temp_bump.cpp covers the mechanism itself against a fake HostApi: first allocation, reuse without allocation, grow, slice alignment and disjointness, slice miss, an oversized request that would have wrapped past the buffer, and a failed grow leaving the slot cleared rather than naming the buffer it just freed.
Fixes hw-native-sys#2151 host_build_graph's bind path did a device_malloc + H2D + device_free for every host-side tensor argument on every run. tensormap_and_ringbuffer stopped doing that in hw-native-sys#1198, which added a runner-scoped retained staging buffer that grows to the high-water packed size and is bump-sliced each run, so steady state converges to zero temporary device allocations. The platform side of that mechanism has never been TRB-specific — HostApi has exposed get/set_retained_temp_buffer generically since hw-native-sys#1198, and hbg already consumes three of DeviceRunnerBase's four per-pipeline-slot retained storages. This makes it consume the fourth. The mechanism now has one definition. RetainedTempBump moves out of the two TRB runtime_maker.cpp files into src/common/utils/retained_temp_bump.h, and the TensorLease ledger out of the three runtime.h files into src/common/utils/tensor_lease.h, with its release loop in utils/tensor_lease_release.h. The lease type is split from its release because both runtime.h files are included by AICore and AICPU translation units, which cannot see the host API. RetainedTempBump::begin() takes a byte count rather than ChipStorageTaskArgs, which keeps that header free of task_interface types and leaves each caller's packing loop next to the staging loop it has to mirror; the packing rule, the 1024-byte slice alignment and the grow rule are unchanged, so TRB behaves exactly as before. Diagnostics the two helpers used to log are now emitted by the callers, which have the tensor index and the logging backend. hbg's ledger takes TRB's shape: TensorPair becomes TensorLease and gains a TensorReleaseKind, and validate releases through release_tensor_leases instead of device_free'ing every recorded pointer — a bump slice is not a separate allocation and must not be freed. Slices are recorded as BufferNoop; the buffer itself is freed once per Worker in DeviceRunnerBase::clear_temporary_buffer. hbg's staging loop skips empty tensors exactly as TRB's does, so the packing predicate and the slicing predicate stay word for word identical: a tensor counted by one but not the other shifts every later slice off the offsets the buffer was sized from. RetainedTempBump aligns the base it hands out rather than assuming the backend aligned it. Kernels need 1024-byte-aligned device pointers; onboard device_malloc happens to return them, but the sim backend is std::malloc, which guarantees only max_align_t, so aligning slice offsets alone would have left every sim slice as misaligned as its base. begin() over-allocates by kAlignment - 1 and aligns inside the allocation, keeping the raw pointer in the slot because that is what device_free must receive. A slice is bounded as `bytes > capacity_ - aligned` rather than as `aligned + bytes > capacity_`. A caller's byte count comes from ChipTensor::nbytes(), an unchecked uint64_t product, so that sum can wrap and compare small — the slice would then be handed out as a pointer past the buffer for copy_to_device to write through. Every overflow upstream, in a size, in a packed total, or in align_up itself, now lands as an ordinary slice miss instead. align_up stays unchecked: it is the same failure one step earlier, and checking it would thread an error path through both packing loops. bind now clears the ledger on entry. A run whose validate never runs otherwise leaves its entries for the next bind — on the onboard native path a failed prepare still reaches validate_runtime_impl through cleanup_failed_prepare, but a finalize whose attach_current_thread fails skips validation entirely. Stale entries were a leak before; with a reused buffer they name offsets the next bind re-slices, so validate would copy that run's bytes back to the earlier run's host pointer. The buffer is grown inside the args bind-phase span rather than before it, so the one device allocation the phase can make is attributed to it and "steady state allocates nothing here" is a measurement rather than a definition. Correctness rests on the retained slot being per pipeline_slot: a native run holds its slot from bind through validate, and a concurrent reservation is admitted only on a distinct slot, so no other run can re-slice a buffer whose slices are live. That is the same property hbg's Graph Definition blocks (hw-native-sys#1988) and SM mirror (hw-native-sys#2013) already rely on. The H2D of a staged tensor still precedes its registration with the run's host accessor, so a reused slice cannot expose the previous run's bytes to orchestration. A pure OUT tensor is staged but never copied in and never zero-filled, so on a run that reuses the buffer unchanged and packs to the same offsets it reads the previous round's own bytes; a first or grown allocation carries uninitialized allocator residue as before, since begin() neither preserves nor initializes a buffer it replaces. Both runtimes' RUNTIME_LOGIC.md now say so. test_depth_two_slots_own_separate_resources asserted that neither hbg pipeline slot held a retained buffer. It now asserts both hold one and that the two are distinct, which is the regression barrier for the change. test_retained_temp_bump.cpp covers the mechanism itself against a fake HostApi: first allocation, reuse without allocation, grow, slice alignment and disjointness, slice miss, an oversized request that would have wrapped past the buffer, and a failed grow leaving the slot cleared rather than naming the buffer it just freed. Both that fake and the TRB one allocate with plain std::malloc, so a base the bump failed to align would show up rather than being hidden by an over-aligning test double. test_hbg_bind_ledger.cpp is the barrier for the ledger clear: it binds one Runtime twice with no validate between, as a finalize whose attach_current_thread failed leaves it, and asserts the second bind's ledger holds only its own lease and that validate leaves the first run's host buffer untouched. It drives the real bind because bind takes the resolved host-orch entry points as a parameter, so the test supplies its own two function pointers instead of an orchestration .so.
Fixes hw-native-sys#2151 host_build_graph's bind path did a device_malloc + H2D + device_free for every host-side tensor argument on every run. tensormap_and_ringbuffer stopped doing that in hw-native-sys#1198, which added a runner-scoped retained staging buffer that grows to the high-water packed size and is bump-sliced each run, so steady state converges to zero temporary device allocations. The platform side of that mechanism has never been TRB-specific — HostApi has exposed get/set_retained_temp_buffer generically since hw-native-sys#1198, and hbg already consumes three of DeviceRunnerBase's four per-pipeline-slot retained storages. This makes it consume the fourth. The mechanism now has one definition. RetainedTempBump moves out of the two TRB runtime_maker.cpp files into src/common/utils/retained_temp_bump.h, and the TensorLease ledger out of the three runtime.h files into src/common/utils/tensor_lease.h, with its release loop in utils/tensor_lease_release.h. The lease type is split from its release because both runtime.h files are included by AICore and AICPU translation units, which cannot see the host API. RetainedTempBump::begin() takes a byte count rather than ChipStorageTaskArgs, which keeps that header free of task_interface types and leaves each caller's packing loop next to the staging loop it has to mirror; the packing rule, the 1024-byte slice alignment and the grow rule are unchanged, so TRB behaves exactly as before. Diagnostics the two helpers used to log are now emitted by the callers, which have the tensor index and the logging backend. hbg's ledger takes TRB's shape: TensorPair becomes TensorLease and gains a TensorReleaseKind, and validate releases through release_tensor_leases instead of device_free'ing every recorded pointer — a bump slice is not a separate allocation and must not be freed. Slices are recorded as BufferNoop; the buffer itself is freed once per Worker in DeviceRunnerBase::clear_temporary_buffer. The packing that sizes the buffer is utils/staged_tensor_plan.h, one copy for both runtimes and both architectures. It is the half of a pair that must not drift — a tensor it counts but the staging loop does not slice, or the reverse, shifts every later slice off the offsets the size was computed from, and nothing detects that until a kernel reads the wrong bytes. A predicate whose failure is silent is the wrong thing to keep four copies of. It is a header of its own rather than part of retained_temp_bump.h so that one keeps needing only <cstddef>, and its unit test keeps compiling without the task_interface include path. hbg's staging loop now skips empty tensors as TRB's does, and that is a change in what hbg accepts, not only a predicate alignment. A zero-byte non-child tensor that was not a pure OUT used to fail the bind: the loop handed it to HostTensorAccessor::add, which rejects an empty region, and the bind reported "no host view for tensor N". It is now passed through with a null address, which is what TRB has always done. Both hbg RUNTIME_LOGIC.md files say so, and a unit test pins it. RetainedTempBump aligns the base it hands out rather than assuming the backend aligned it. Kernels need 1024-byte-aligned device pointers; onboard device_malloc happens to return them, but the sim backend is std::malloc, which guarantees only max_align_t, so aligning slice offsets alone would have left every sim slice as misaligned as its base. begin() over-allocates by kAlignment - 1 and aligns inside the allocation, keeping the raw pointer in the slot because that is what device_free must receive. A slice is bounded as `bytes > capacity_ - aligned` rather than as `aligned + bytes > capacity_`. A caller's byte count comes from ChipTensor::nbytes(), an unchecked uint64_t product, so that sum can wrap and compare small — the slice would then be handed out as a pointer past the buffer for copy_to_device to write through. Every overflow upstream, in a size, in a packed total, or in align_up itself, now lands as an ordinary slice miss instead. align_up stays unchecked: it is the same failure one step earlier, and checking it would thread an error path through both packing loops. bind now clears the ledger on entry. A run whose validate never runs otherwise leaves its entries for the next bind — on the onboard native path a failed prepare still reaches validate_runtime_impl through cleanup_failed_prepare, but a finalize whose attach_current_thread fails skips validation entirely. Stale entries were a leak before; with a reused buffer they name offsets the next bind re-slices, so validate would copy that run's bytes back to the earlier run's host pointer. The buffer is grown inside the args bind-phase span rather than before it, so the one device allocation the phase can make is attributed to it and "steady state allocates nothing here" is a measurement rather than a definition. Correctness rests on the retained slot being per pipeline_slot: a native run holds its slot from bind through validate, and a concurrent reservation is admitted only on a distinct slot, so no other run can re-slice a buffer whose slices are live. That is the same property hbg's Graph Definition blocks (hw-native-sys#1988) and SM mirror (hw-native-sys#2013) already rely on. The H2D of a staged tensor still precedes its registration with the run's host accessor, so a reused slice cannot expose the previous run's bytes to orchestration. A pure OUT tensor is staged but never copied in and never zero-filled, so on a run that reuses the buffer unchanged and packs to the same offsets it reads the previous round's own bytes; a first or grown allocation carries uninitialized allocator residue as before, since begin() neither preserves nor initializes a buffer it replaces. Both runtimes' RUNTIME_LOGIC.md now say so. test_depth_two_slots_own_separate_resources asserted that neither hbg pipeline slot held a retained buffer. It now asserts both hold one and that the two are distinct, which is the regression barrier for the change. test_retained_temp_bump.cpp covers the mechanism itself against a fake HostApi: first allocation, reuse without allocation, grow, slice alignment and disjointness, slice miss, an oversized request that would have wrapped past the buffer, and a failed grow leaving the slot cleared rather than naming the buffer it just freed. Both that fake and the TRB one allocate with plain std::malloc, so a base the bump failed to align would show up rather than being hidden by an over-aligning test double. hbg's validate no longer logs "Freed %d device allocations" at INFO: the release is now the shared helper's LOG_DEBUG tally, so that line is absent at the default verbosity. test_hbg_bind_ledger.cpp is the barrier for the ledger clear and for the empty-tensor pass-through: it binds one Runtime twice with no validate between, as a finalize whose attach_current_thread failed leaves it, and asserts the second bind's ledger holds only its own lease and that validate leaves the first run's host buffer untouched. It drives the real bind because bind takes the resolved host-orch entry points as a parameter, so the test supplies its own two function pointers instead of an orchestration .so.
Fixes hw-native-sys#2151 host_build_graph's bind path did a device_malloc + H2D + device_free for every host-side tensor argument on every run. tensormap_and_ringbuffer stopped doing that in hw-native-sys#1198, which added a runner-scoped retained staging buffer that grows to the high-water packed size and is bump-sliced each run, so steady state converges to zero temporary device allocations. The platform side of that mechanism has never been TRB-specific — HostApi has exposed get/set_retained_temp_buffer generically since hw-native-sys#1198, and hbg already consumes three of DeviceRunnerBase's four per-pipeline-slot retained storages. This makes it consume the fourth. The mechanism now has one definition. RetainedTempBump moves out of the two TRB runtime_maker.cpp files into src/common/utils/retained_temp_bump.h, and the TensorLease ledger out of the three runtime.h files into src/common/utils/tensor_lease.h, with its release loop in utils/tensor_lease_release.h. The lease type is split from its release because both runtime.h files are included by AICore and AICPU translation units, which cannot see the host API. RetainedTempBump::begin() takes a byte count rather than ChipStorageTaskArgs, which keeps that header free of task_interface types and leaves each caller's packing loop next to the staging loop it has to mirror; the packing rule, the 1024-byte slice alignment and the grow rule are unchanged, so TRB behaves exactly as before. Diagnostics the two helpers used to log are now emitted by the callers, which have the tensor index and the logging backend. hbg's ledger takes TRB's shape: TensorPair becomes TensorLease and gains a TensorReleaseKind, and validate releases through release_tensor_leases instead of device_free'ing every recorded pointer — a bump slice is not a separate allocation and must not be freed. Slices are recorded as BufferNoop; the buffer itself is freed once per Worker in DeviceRunnerBase::clear_temporary_buffer. The packing that sizes the buffer is utils/temp_buffer_plan.h, one copy for both runtimes and both architectures. It is the half of a pair that must not drift — a tensor it counts but the staging loop does not slice, or the reverse, shifts every later slice off the offsets the size was computed from, and nothing detects that until a kernel reads the wrong bytes. A predicate whose failure is silent is the wrong thing to keep four copies of. It is a header of its own rather than part of retained_temp_bump.h so that one keeps needing only <cstddef>, and its unit test keeps compiling without the task_interface include path. Its predicate is not the set hw-native-sys#2213 named h2d=: a pure OUT tensor takes a slice without a copy-in. hbg's argument loop now skips empty tensors as TRB's does, and that is a change in what hbg accepts, not only a predicate alignment. A zero-byte non-child tensor that was not a pure OUT used to fail the bind: the loop handed it to HostTensorAccessor::add, which rejects an empty region, and the bind reported "no host view for tensor N". It is now passed through with a null address, which is what TRB has always done. Both hbg RUNTIME_LOGIC.md files say so, and a unit test pins it. RetainedTempBump aligns the base it hands out rather than assuming the backend aligned it. Kernels need 1024-byte-aligned device pointers; onboard device_malloc happens to return them, but the sim backend is std::malloc, which guarantees only max_align_t, so aligning slice offsets alone would have left every sim slice as misaligned as its base. begin() over-allocates by kAlignment - 1 and aligns inside the allocation, keeping the raw pointer in the slot because that is what device_free must receive. A slice is bounded as `bytes > capacity_ - aligned` rather than as `aligned + bytes > capacity_`. A caller's byte count comes from ChipTensor::nbytes(), an unchecked uint64_t product, so that sum can wrap and compare small — the slice would then be handed out as a pointer past the buffer for copy_to_device to write through. Every overflow upstream, in a size, in a packed total, or in align_up itself, now lands as an ordinary slice miss instead. align_up stays unchecked: it is the same failure one step earlier, and checking it would thread an error path through both packing loops. bind now clears the ledger on entry. A run whose validate never runs otherwise leaves its entries for the next bind — on the onboard native path a failed prepare still reaches validate_runtime_impl through cleanup_failed_prepare, but a finalize whose attach_current_thread fails skips validation entirely. Stale entries were a leak before; with a reused buffer they name offsets the next bind re-slices, so validate would copy that run's bytes back to the earlier run's host pointer. The buffer is grown inside the args bind-phase span rather than before it, so the one device allocation the phase can make is attributed to it and "steady state allocates nothing here" is a measurement rather than a definition. Correctness rests on the retained slot being per pipeline_slot: a native run holds its slot from bind through validate, and a concurrent reservation is admitted only on a distinct slot, so no other run can re-slice a buffer whose slices are live. That is the same property hbg's Graph Definition blocks (hw-native-sys#1988) and SM mirror (hw-native-sys#2013) already rely on. The H2D of a staged tensor still precedes its registration with the run's host accessor, so a reused slice cannot expose the previous run's bytes to orchestration. A pure OUT tensor is staged but never copied in and never zero-filled, so on a run that reuses the buffer unchanged and packs to the same offsets it reads the previous round's own bytes; a first or grown allocation carries uninitialized allocator residue as before, since begin() neither preserves nor initializes a buffer it replaces. Both runtimes' RUNTIME_LOGIC.md now say so. test_depth_two_slots_own_separate_resources asserted that neither hbg pipeline slot held a retained buffer. It now asserts both hold one and that the two are distinct, which is the regression barrier for the change. test_retained_temp_bump.cpp covers the mechanism itself against a fake HostApi: first allocation, reuse without allocation, grow, slice alignment and disjointness, slice miss, an oversized request that would have wrapped past the buffer, and a failed grow leaving the slot cleared rather than naming the buffer it just freed. Both that fake and the TRB one allocate with plain std::malloc, so a base the bump failed to align would show up rather than being hidden by an over-aligning test double. hbg's validate no longer logs "Freed %d device allocations" at INFO: the release is now the shared helper's LOG_DEBUG tally, so that line is absent at the default verbosity. test_hbg_bind_ledger.cpp is the barrier for the ledger clear and for the empty-tensor pass-through: it binds one Runtime twice with no validate between, as a finalize whose attach_current_thread failed leaves it, and asserts the second bind's ledger holds only its own lease and that validate leaves the first run's host buffer untouched. It drives the real bind because bind takes the resolved host-orch entry points as a parameter, so the test supplies its own two function pointers instead of an orchestration .so.
Fixes hw-native-sys#2151 host_build_graph's bind path did a device_malloc + H2D + device_free for every host-side tensor argument on every run. tensormap_and_ringbuffer stopped doing that in hw-native-sys#1198, which added a runner-scoped retained staging buffer that grows to the high-water packed size and is bump-sliced each run, so steady state converges to zero temporary device allocations. The platform side of that mechanism has never been TRB-specific — HostApi has exposed get/set_retained_temp_buffer generically since hw-native-sys#1198, and hbg already consumes three of DeviceRunnerBase's four per-pipeline-slot retained storages. This makes it consume the fourth. The mechanism now has one definition. RetainedTempBump moves out of the two TRB runtime_maker.cpp files into src/common/utils/retained_temp_bump.h, and the TensorLease ledger out of the three runtime.h files into src/common/utils/tensor_lease.h, with its release loop in utils/tensor_lease_release.h. The lease type is split from its release because both runtime.h files are included by AICore and AICPU translation units, which cannot see the host API. RetainedTempBump::begin() takes a byte count rather than ChipStorageTaskArgs, which keeps that header free of task_interface types and leaves each caller's packing loop next to the staging loop it has to mirror; the packing rule, the 1024-byte slice alignment and the grow rule are unchanged, so TRB behaves exactly as before. Diagnostics the two helpers used to log are now emitted by the callers, which have the tensor index and the logging backend. hbg's ledger takes TRB's shape: TensorPair becomes TensorLease and gains a TensorReleaseKind, and validate releases through release_tensor_leases instead of device_free'ing every recorded pointer — a bump slice is not a separate allocation and must not be freed. Slices are recorded as BufferNoop; the buffer itself is freed once per Worker in DeviceRunnerBase::clear_temporary_buffer. The packing that sizes the buffer is utils/temp_buffer_plan.h, one copy for both runtimes and both architectures. It is the half of a pair that must not drift — a tensor it counts but the staging loop does not slice, or the reverse, shifts every later slice off the offsets the size was computed from, and nothing detects that until a kernel reads the wrong bytes. A predicate whose failure is silent is the wrong thing to keep four copies of. It is a header of its own rather than part of retained_temp_bump.h so that one keeps needing only <cstddef>, and its unit test keeps compiling without the task_interface include path. Its predicate is not the set hw-native-sys#2213 named h2d=: a pure OUT tensor takes a slice without a copy-in. hbg's argument loop now skips empty tensors as TRB's does, and that is a change in what hbg accepts, not only a predicate alignment. A zero-byte non-child tensor that was not a pure OUT used to fail the bind: the loop handed it to HostTensorAccessor::add, which rejects an empty region, and the bind reported "no host view for tensor N". It is now passed through with a null address, which is what TRB has always done. Both hbg RUNTIME_LOGIC.md files say so, and a unit test pins it. RetainedTempBump aligns the base it hands out rather than assuming the backend aligned it. Kernels need 1024-byte-aligned device pointers; onboard device_malloc happens to return them, but the sim backend is std::malloc, which guarantees only max_align_t, so aligning slice offsets alone left every sim slice as misaligned as its base. That was already true of TRB before this change and no test could see it, because a sim "device" pointer is host memory and never faults. begin() now over-allocates by kAlignment - 1 and aligns inside the allocation, keeping the raw pointer in the slot because that is what device_free must receive. TRB therefore does change: it allocates 1023 bytes more and re-aligns, which is why three assertions in test_trb_runtime_temp_buffer.cpp move with it. A slice is bounded as `bytes > capacity_ - aligned` rather than as `aligned + bytes > capacity_`. A caller's byte count comes from ChipTensor::nbytes(), an unchecked uint64_t product, so that sum can wrap and compare small — the slice would then be handed out as a pointer past the buffer for copy_to_device to write through. Every overflow upstream, in a size, in a packed total, or in align_up itself, now lands as an ordinary slice miss instead. align_up stays unchecked: it is the same failure one step earlier, and checking it would thread an error path through both packing loops. bind now clears the ledger on entry. A run whose validate never runs otherwise leaves its entries for the next bind — on the onboard native path a failed prepare still reaches validate_runtime_impl through cleanup_failed_prepare, but a finalize whose attach_current_thread fails skips validation entirely. Stale entries were a leak before; with a reused buffer they name offsets the next bind re-slices, so validate would copy that run's bytes back to the earlier run's host pointer. The buffer is grown inside the args bind-phase span rather than before it, so the one device allocation the phase can make is attributed to it and "steady state allocates nothing here" is a measurement rather than a definition. Correctness rests on the retained slot being per pipeline_slot: a native run holds its slot from bind through validate, and a concurrent reservation is admitted only on a distinct slot, so no other run can re-slice a buffer whose slices are live. That is the same property hbg's Graph Definition blocks (hw-native-sys#1988) and SM mirror (hw-native-sys#2013) already rely on. The H2D of a staged tensor still precedes its registration with the run's host accessor, so a reused slice cannot expose the previous run's bytes to orchestration. A pure OUT tensor is staged but never copied in and never zero-filled, so on a run that reuses the buffer unchanged and packs to the same offsets it reads the previous round's own bytes; a first or grown allocation carries uninitialized allocator residue as before, since begin() neither preserves nor initializes a buffer it replaces. Both runtimes' RUNTIME_LOGIC.md now say so. test_depth_two_slots_own_separate_resources asserted that neither hbg pipeline slot held a retained buffer. It now asserts both hold one and that the two are distinct, which is the regression barrier for the change. test_retained_temp_bump.cpp covers the mechanism itself against a fake HostApi: first allocation, reuse without allocation, grow, slice alignment and disjointness, slice miss, an oversized request that would have wrapped past the buffer, and a failed grow leaving the slot cleared rather than naming the buffer it just freed. Both that fake and the TRB one allocate with plain std::malloc, so a base the bump failed to align would show up rather than being hidden by an over-aligning test double. hbg's validate no longer logs "Freed %d device allocations" at INFO: the release is now the shared helper's LOG_DEBUG tally, so that line is absent at the default verbosity. test_hbg_bind_ledger.cpp is the barrier for the ledger clear and for the empty-tensor pass-through: it binds one Runtime twice with no validate between, as a finalize whose attach_current_thread failed leaves it, and asserts the second bind's ledger holds only its own lease and that validate leaves the first run's host buffer untouched. It drives the real bind because bind takes the resolved host-orch entry points as a parameter, so the test supplies its own two function pointers instead of an orchestration .so.
#2168) Fixes #2151 host_build_graph's bind path did a device_malloc + H2D + device_free for every host-side tensor argument on every run. tensormap_and_ringbuffer stopped doing that in #1198, which added a runner-scoped retained staging buffer that grows to the high-water packed size and is bump-sliced each run, so steady state converges to zero temporary device allocations. The platform side of that mechanism has never been TRB-specific — HostApi has exposed get/set_retained_temp_buffer generically since #1198, and hbg already consumes three of DeviceRunnerBase's four per-pipeline-slot retained storages. This makes it consume the fourth. The mechanism now has one definition. RetainedTempBump moves out of the two TRB runtime_maker.cpp files into src/common/utils/retained_temp_bump.h, and the TensorLease ledger out of the three runtime.h files into src/common/utils/tensor_lease.h, with its release loop in utils/tensor_lease_release.h. The lease type is split from its release because both runtime.h files are included by AICore and AICPU translation units, which cannot see the host API. RetainedTempBump::begin() takes a byte count rather than ChipStorageTaskArgs, which keeps that header free of task_interface types and leaves each caller's packing loop next to the staging loop it has to mirror; the packing rule, the 1024-byte slice alignment and the grow rule are unchanged, so TRB behaves exactly as before. Diagnostics the two helpers used to log are now emitted by the callers, which have the tensor index and the logging backend. hbg's ledger takes TRB's shape: TensorPair becomes TensorLease and gains a TensorReleaseKind, and validate releases through release_tensor_leases instead of device_free'ing every recorded pointer — a bump slice is not a separate allocation and must not be freed. Slices are recorded as BufferNoop; the buffer itself is freed once per Worker in DeviceRunnerBase::clear_temporary_buffer. The packing that sizes the buffer is utils/temp_buffer_plan.h, one copy for both runtimes and both architectures. It is the half of a pair that must not drift — a tensor it counts but the staging loop does not slice, or the reverse, shifts every later slice off the offsets the size was computed from, and nothing detects that until a kernel reads the wrong bytes. A predicate whose failure is silent is the wrong thing to keep four copies of. It is a header of its own rather than part of retained_temp_bump.h so that one keeps needing only <cstddef>, and its unit test keeps compiling without the task_interface include path. Its predicate is not the set #2213 named h2d=: a pure OUT tensor takes a slice without a copy-in. hbg's argument loop now skips empty tensors as TRB's does, and that is a change in what hbg accepts, not only a predicate alignment. A zero-byte non-child tensor that was not a pure OUT used to fail the bind: the loop handed it to HostTensorAccessor::add, which rejects an empty region, and the bind reported "no host view for tensor N". It is now passed through with a null address, which is what TRB has always done. Both hbg RUNTIME_LOGIC.md files say so, and a unit test pins it. RetainedTempBump aligns the base it hands out rather than assuming the backend aligned it. Kernels need 1024-byte-aligned device pointers; onboard device_malloc happens to return them, but the sim backend is std::malloc, which guarantees only max_align_t, so aligning slice offsets alone left every sim slice as misaligned as its base. That was already true of TRB before this change and no test could see it, because a sim "device" pointer is host memory and never faults. begin() now over-allocates by kAlignment - 1 and aligns inside the allocation, keeping the raw pointer in the slot because that is what device_free must receive. TRB therefore does change: it allocates 1023 bytes more and re-aligns, which is why three assertions in test_trb_runtime_temp_buffer.cpp move with it. A slice is bounded as `bytes > capacity_ - aligned` rather than as `aligned + bytes > capacity_`. A caller's byte count comes from ChipTensor::nbytes(), an unchecked uint64_t product, so that sum can wrap and compare small — the slice would then be handed out as a pointer past the buffer for copy_to_device to write through. Every overflow upstream, in a size, in a packed total, or in align_up itself, now lands as an ordinary slice miss instead. align_up stays unchecked: it is the same failure one step earlier, and checking it would thread an error path through both packing loops. bind now clears the ledger on entry. A run whose validate never runs otherwise leaves its entries for the next bind — on the onboard native path a failed prepare still reaches validate_runtime_impl through cleanup_failed_prepare, but a finalize whose attach_current_thread fails skips validation entirely. Stale entries were a leak before; with a reused buffer they name offsets the next bind re-slices, so validate would copy that run's bytes back to the earlier run's host pointer. The buffer is grown inside the args bind-phase span rather than before it, so the one device allocation the phase can make is attributed to it and "steady state allocates nothing here" is a measurement rather than a definition. Correctness rests on the retained slot being per pipeline_slot: a native run holds its slot from bind through validate, and a concurrent reservation is admitted only on a distinct slot, so no other run can re-slice a buffer whose slices are live. That is the same property hbg's Graph Definition blocks (#1988) and SM mirror (#2013) already rely on. The H2D of a staged tensor still precedes its registration with the run's host accessor, so a reused slice cannot expose the previous run's bytes to orchestration. A pure OUT tensor is staged but never copied in and never zero-filled, so on a run that reuses the buffer unchanged and packs to the same offsets it reads the previous round's own bytes; a first or grown allocation carries uninitialized allocator residue as before, since begin() neither preserves nor initializes a buffer it replaces. Both runtimes' RUNTIME_LOGIC.md now say so. test_depth_two_slots_own_separate_resources asserted that neither hbg pipeline slot held a retained buffer. It now asserts both hold one and that the two are distinct, which is the regression barrier for the change. test_retained_temp_bump.cpp covers the mechanism itself against a fake HostApi: first allocation, reuse without allocation, grow, slice alignment and disjointness, slice miss, an oversized request that would have wrapped past the buffer, and a failed grow leaving the slot cleared rather than naming the buffer it just freed. Both that fake and the TRB one allocate with plain std::malloc, so a base the bump failed to align would show up rather than being hidden by an over-aligning test double. hbg's validate no longer logs "Freed %d device allocations" at INFO: the release is now the shared helper's LOG_DEBUG tally, so that line is absent at the default verbosity. test_hbg_bind_ledger.cpp is the barrier for the ledger clear and for the empty-tensor pass-through: it binds one Runtime twice with no validate between, as a finalize whose attach_current_thread failed leaves it, and asserts the second bind's ledger holds only its own lease and that validate leaves the first run's host buffer untouched. It drives the real bind because bind takes the resolved host-orch entry points as a parameter, so the test supplies its own two function pointers instead of an orchestration .so.
Summary
Cut the per-run
device_malloc/device_freeof ordinary non-child tensors on thetensormap_and_ringbuffer(TRB) bind path. TRB now stages them through a runner-scoped retained temporary buffer that grows to the high-water plan size and is reused across runs — steady-state (stable decode shapes) converges to zero temporary allocations.This is an internal allocation optimization, always on for TRB — there is no user-facing switch. It is always result-equivalent to the old malloc/free path (only faster), and TRB runs are already serial per runner (enforced by the static arena pool), so there is no correctness reason to gate it. The buffer is a single retained allocation per
DeviceRunnerBase;begin_temporary_buffer_run(plan)sizes it from the run's non-child tensors,acquireslices from it,validateends the run. Grow = free-old + alloc-new (no data preserved — per-run scratch); it never shrinks; on-exhaustion the run fails (no mid-run malloc fallback).Design notes
max_temporary_buffer_bytes, notemporary_buffer_mode, noconfigure_*entry across Python / pybind / C-ABI / ChipWorker.TensorLease+TensorReleaseKindreplace the implicit "every recorded dev_ptr was malloc'd this run" assumption: buffer slices release as no-ops (whole buffer reused at run end); theFreekind covers any backend that leaves the temp callbacks unwired and falls back todevice_malloc.See the runtime's
RUNTIME_LOGIC.md§2.4 anddocs/task-flow.mdfor mechanics.Testing
test_chip_worker,test_host_worker).tensormap_and_ringbuffer/prepared_callablea2a3sim — 7 passed (end-to-end register→run→validate with the buffer always on).test_temporary_variable_buffer,test_trb_runtime_temp_buffer) cover grow / reuse / shrink-noop / grow-failure / plan-sizing / child-memory pass-through / copy-back / lease-release — to be exercised by CI.Follow-up on the optimization direction discussed in #1186.