Lifecycle and Memory Model
This page is the authoritative memory/ownership contract for TinyLoop C++ callers.
Lifecycle entry points
Model* load_model(const std::string& path, int max_seq_len = 2048, int prefill_chunk = 0);
void free_model(Model* model);
size_t vram_usage_bytes(Model* model);
Internal allocation phases (load_model)
load_model is not a lightweight constructor. Internally it performs:
- header parse + format validation (
MAGIC,VERSION_MAX, dims) - quantized weight upload (
QuantWeight: packed bytes + FP16 scales/zeros) - optional FP16 caches for hot GEMM paths (env-controlled)
BufferPoolallocation:main(FP32 residual, always seq-wide)norm,scratch1,attn_down(FP16 scratch; seq-wide or chunk-wide)- lazy
logit_row
- optional reconstruction scratch for h-mode paths
Any allocation failure is fatal to model creation. Caller receives nullptr and must not continue with that model pointer.
Model-owned memory
Model owns long-lived GPU memory:
- quantized weights + metadata
- output/head tensors
BufferPoolreusable activations- optional FP16 weight caches
Model does not permanently own per-request decode caches. Those are created by generation/prefix/resume APIs.
Per-request / handle-owned memory
RuntimeKVCache
Allocated by generation-family calls and by prefix/warm-start builders. Layer storage mode depends on env/runtime flags:
- FP16 K/V
- INT8 K/V
- store-h FP16
- store-h INT8
- store-h INT4
PrefixCache / ResumeHandle / PrefixPool
Opaque handles that own cache state across calls.
PrefixCache: prefilled prefix state at a fixed loop depth.ResumeHandle: residual snapshot + over-allocated cache for depth upgrades.PrefixPool: multiple prefix caches + LRU behavior.
Mutable state and thread-safety implications
Model has mutable working buffers (buffers.main, buffers.norm, scratch buffers). Every forward mutates them.
Consequences:
- Concurrent forward calls on the same
Model*are unsafe unless externally serialized. - Handles tied to one model (especially
ResumeHandle) assume no foreign forward clobbers their expected residual/cache evolution. - Safer concurrency patterns:
- lock-per-model
- model-per-worker (higher VRAM)
- process-per-replica
prefill_chunk and scratch residency
prefill_chunk > 0 changes memory behavior:
main: stillmax_seq_len × D × sizeof(float)- scratch buffers (
norm,scratch1,attn_down): allocated at chunk rows instead of full sequence rows
Trade-off:
- lower VRAM footprint for long context
- higher launch/driver overhead during prefill due to chunked execution
- score-style APIs are constrained by chunk row budget
Why this memory design exists
TinyLoop prioritizes:
- stable high throughput after one model load
- avoiding repeated allocation churn in hot paths
- explicit control of VRAM-vs-latency tradeoffs via runtime mode flags
Compared with fully stateless APIs, this yields higher sustained performance but requires stronger caller discipline around ownership and concurrency.
Practical integration checklist
Before integrating in production C++:
- Decide one concurrency model (lock vs model-per-worker).
- Decide max sequence budget + chunk strategy from VRAM envelope.
- Decide cache mode (FP16/INT8/store-h variants) and document quality/latency policy.
- Treat all opaque handles as VRAM-bearing objects; release deterministically.
- Wire health probes around
load_modelfailures and OOM paths.