RAMforge is a local inference runtime designed to run AI models that may be significantly larger than the available RAM or VRAM by treating RAM, VRAM, and storage as a hierarchical memory system.
Graph-aware residency (2026-10-25): RAMforge now builds a unified model-scoped memory ledger, in-memory execution profiles from bounded graph traces, deduplicated bounded staging plans, and deterministic
conservative/memory-bound/reuse-awarepolicy profiles in addition tooff/observe/active. Operation reports include explicit provenance, reconciliation, requested versus native-accepted versus observed bytes, timing, and an output fingerprint;--residency-report-json PATHemits schema version 1. Release mutations remain restricted to the post-operation safe boundary. Intra-decode eviction is still unsupported because the pinned callback does not prove resource lifetime. See MEMORY_ACCOUNTING.md, GRAPH_SAFETY.md, and BENCHMARKS.md.GPT-NeoX/Pythia execution and enforced budgets (2026-10-25):
gptneoxGGUFs are executable through the explicit--backend forgecorepath, which delegates to the pinned llama.cpp/ggml implementation. The legacy RAMforge executor still rejects the architecture before reading weights, with an actionable error naming the required backend; it never falls back or approximates GPT-NeoX math. Real Pythia-70M Q8_0 runs match the native tokenization and greedy continuation, and an independent NumPy forward pass confirms the final-token logits (top-1 exact, cosine similarity 1.000000046). The 20B GPT-NeoX GGUF (13.13 GB tensor bytes) now executes under an 8 GiB hard cgroup v2 cap with--enforce-ram --no-repack --no-prefetch: 7.64 GiB of the model mapping resident at the end-of-run sample, 7,979 limit events, no OOM kill, and one greedy tokenParisin 7.72 s. The same 8 GiB run with prefetching took 16.94 s. This is out-of-core execution under a kernel-enforced cap, not a throughput or quality guarantee. See Enforced host-memory budgets.Usable Out-of-Core Inference:
ramforge runnow streams decoded assistant text to stdout with UTF-8 byte-token accumulation. Optional--profilereports measured GGUF I/O, layer load/compute/release, quantized/F32 matvec, allocation, sampling, token latency, bytes read, and peak managed memory.--memory-reportseparates RAMforge-managed memory, process RSS, and system memory.ramforge supportexposes the architecture/quantization registry; qwen35 remains inspectable/plannable but intentionally not executable.Milestone 7.2 Status (Fast Float Load): F32 tensor payloads now read directly into their final
Vec<f32>allocation without a raw-byte intermediate, destination zero-fill, or little-endian per-element decode loop on little-endian hosts. Full tensors and streamed F32 chunks/rows use the direct path; the truthful load charge is now 1× file bytes. F16/BF16 remain decoded F32 with their 3× load transient, and quantized representations are unchanged.Milestone 7.1 Baseline (Accounting Hardening): Resident persistent tensors establish their full load-transient charge before file I/O, settle atomically to the actual owned representation, and roll back all startup charges on failure. At M7.1 the load factors were quantized 1×, F32 2×, and F16/BF16 3×; M7.2 supersedes only F32 with its direct 1× representation. The 25% retention policy uses decoded/compact resident bytes rather than file bytes. Final hidden state is caller-owned under a lifetime-matched
tmp:hiddencharge.Milestone 6.1 Baseline (Correctness & Accounting Fixes):
generate()is cleanly repeatable on one engine (explicit KV reset + failure-proof budget); RoPE uses the correct llama/qwen2 half-split pair convention; F16/BF16 resident RAM is booked at its true decoded size (4 B/elem, with a 3×-file-byte load transient); qwen2 Q/K/V biases are loaded, budgeted, validated (all-or-none + exact shape), and applied after the projections. The historical M6.1 baseline was verified by 132 synthetic tests; current validation is summarized below.Milestone 6 Status (Managed Out-of-Core Path): The legacy inference path charges its named, RAMforge-managed allocations via scoped accounting helpers; the cache is budget-charged per entry; matrix layout is the explicit GGML/GGUF convention (no orientation guessing, no full-F32 fallbacks); logits use a single budget-charged buffer with a budget-aware chunked streamed projection; the KV cache grows chunk-wise without prefix copies; the F32 matvec hot path uses AVX2/rayon. CPU-only, llama/qwen2 dense models. GPU, MoE, HTTP not implemented. This ledger is not a process-RSS or OS-memory cap; allocator overhead, native/dependency allocations, page cache, and some legacy returned buffers remain outside it.
- GGUF parsing without loading payloads
- File-backed tensor access via
GgufDataSource - Checked RAMforge-managed allocation accounting via
MemoryBudget(not an RSS or OS-memory cap) - Bounded LRU cache via
BoundedCache - Real CPU inference with the retained legacy layer-streaming path
- Native quantized inference without full F32 expansion
MemoryBudget is a RAMforge-managed logical ledger, not a process RSS cap or a hard operating-system memory limit. The first safe accounting tranche covers named persistent weights, decoded layer ownership, the generation KV cache (including old/new growth overlap), caller-owned generation buffers, staged K/V workspace, and bounded streamed read/decode buffers. Reservations are checked before the managed buffers are allocated, and charges remain live until those owned buffers are dropped.
The ledger does not claim complete process-memory coverage. Allocator metadata and fragmentation, Rust runtime/container overhead not included in a named charge, native/dependency allocations, file-system page cache, tokenizer/output growth, and legacy APIs that return newly owned Vec values (including the raw-cache get_tensor/remove_budgeted and persistent-weight get_embedding/to_f32_vec compatibility paths) remain outside or only coarsely represented. A standalone StreamingLlamaModel receives a borrowed budget, so callers must invoke its terminal release_persistent method before dropping it; the owning InferenceEngine performs that ordering automatically. Standalone core compute/reference helpers and calibration/profiling containers are likewise not automatically tied to the inference ledger. RSS and OS limits are reported separately where available; they are not enforced by MemoryBudget.
RAMforge now also exposes an explicit CPU-only ForgeCore path:
ramforge run MODEL.gguf --ram 8G --prompt "Hello" --backend forgecore
→ forge_core::Model
→ forge_core::Context
→ forge_core::Batch
→ Context::decode()
→ forge_core::Logits
→ RAMforge tokenizer/sampling/output
Use ramforge run ... --backend forgecore to select it. The default
--backend legacy path remains available as the comparison/reference
InferenceEngine; ForgeCore failures never fall back to it implicitly. The
adapter is pinned to verified ForgeCore commit
8d67cb466dba1ee013efc13ee152391602752473 and keeps direct
forge-sys and llama.cpp/ggml FFI out of RAMforge. That ForgeCore revision
exposes ModelOptions::use_extra_bufts, which RAMforge uses to disable CPU
weight repacking when requested.
Phase 2 does not claim GPU, ForgeCore-native per-allocation accounting, or
migration of the legacy numerical modules. RAMforge GGUF inspection, planning,
diagnostics, cache, residency, and accounting infrastructure remains retained;
ForgeCore native model, context, KV, and backend allocations remain outside
RAMforge's MemoryBudget, while safe device facts are reported only where the
adapter can observe them. The separate --enforce-ram supervisor described
below provides an operating-system hard cap; it is not a claim that
MemoryBudget controls native allocations. Runtime linking follows ForgeCore's
native runbook; arrange LD_LIBRARY_PATH=$FORGE_LLAMA_DIR/build/bin (or an
equivalent final-binary rpath) for the CPU shared libraries.
--ram by itself remains a RAMforge-managed/planning value. Add
--enforce-ram to turn the same byte value into a hard kernel cap for the
supervised process group:
ramforge run MODEL.gguf --ram 512M --enforce-ram --ctx 256 --no-repack \
--prompt "The capital of France is" --max-tokens 8 --temperature 0 \
--backend forgecore --memory-report --profileThe supervisor creates an unprivileged child cgroup v2 below the caller's
delegated user subtree, writes memory.max=<bytes> and memory.swap.max=0,
sets memory.oom.group=1, re-execs the same command, and reports
memory.peak, memory.events, selected memory.stat counters, wall time, and
the child exit status. The cap applies to charged memory of that process
group: anonymous allocations, file-backed mapped pages charged to the group,
and kernel memory. It does not cap memory outside the group, and it does not
make RSS equal to the limit.
Weight residency: llama.cpp maps the GGUF file (load_mode=mmap) and currently
prefaults it with MAP_POPULATE. RAMforge does not copy the weights into its
own buffers, and it does not implement per-layer eviction on the forgeCORE
path. Under the cgroup cap, the kernel evicts clean file-backed weight pages and
refaults them on demand. --no-repack disables upstream CPU weight-repacking
buffers, which otherwise create a large anonymous copy of quantized weights.
For Qwen2.5-1.5B Q4_0 this reduced anonymous residency from 858.60 MiB to
155.74 MiB.
Cold-cache priming: when enforcement is active, RAMforge calls
posix_fadvise(POSIX_FADV_DONTNEED) for the model shards before load. This
makes the measured run pay for the pages it faults and makes the cgroup
accounting meaningful. It is a hint, not a guarantee; it cannot drop pages
that are still mapped or files on tmpfs. Keep large test models on disk-backed
storage.
Flags:
--enforce-ram: hard cgroup v2 limit for the run.--enforce-timeout SECONDS: wall-clock cap (default 900). On timeout the supervisor writescgroup.kill, reaps the child, and exits 124.--ctx TOKENS: ForgeCore context override. The legacy executor rejects it. Context size controls the KV cache and is essential for small budgets.--no-repack: disable llama.cpp CPU weight repacking.--no-prefetch: selectModelLoadMode::MmapNoPrefetch, which skips the native whole-fileMAP_POPULATE. This requires the documented forgeCORE native patch; default--backend forgecorebehavior remains prefaulted mmap.--release-mapping-pages: ask the native model to release resident pages from exactly its own file-backed mappings after load. On Linux this usesmadvise(MADV_DONTNEED); it does not unmap memory or invalidate tensor pointers. Unsupported platforms fail explicitly. This is independent of--no-prefetchand is a no-op when the mapping is already nonresident.--inspect-layer N: print exact native mapping metadata for tensors namedblk.N.*. Shows file-backed versus anonymous tensors, mapping index, offset, size, and page bounds. It does not release pages.--release-layer N: request page release for the file-backed tensors in layerNafter load. Pages shared with unselected file-backed tensors are skipped conservatively. This is tensor/range-scoped, not per-layer execution scheduling, and cannot guarantee a lower load-time peak. It conflicts with--release-mapping-pagesand--inspect-layer.--residency-policy off|observe|conservative|memory-bound|reuse-aware|active: execution-aware residency policy.offis the default.observebuilds inventory/plans and reports without mutation.conservativereleases only tensors with no shared page bytes.memory-boundtargets the configured soft budget.reuse-awareuses a valid in-memory graph profile to order cold resources first.activeis the compatibility alias for memory-bound behavior. All mutation happens at a safe post-operation boundary; none occurs during decode.--residency-target SIZE: soft process residency target. Defaults to--ramfor mutating policies. This is not a hard limit.--residency-reserve SIZE: safety reserve subtracted from the soft target.--residency-report: print the structured ledger, reconciliation, admission, plans, staging/application results, timing, and before/after observations.--residency-report-json PATH: write the same operation as a versioned JSON document (schema_version: 1). A write failure is a command error.--residency-prefetch SIZE: best-effort bounded tensor-page prefetch (MADV_WILLNEEDhint). The plan deduplicates overlapping page ranges; requested, native-accepted, and observed mapping changes are reported separately.--graph-trace: opt-in bounded scheduler eval trace. It forces observed node groups to be computed separately and synchronized; expect overhead.--graph-trace-events N: retained event bound (default 4096).--graph-trace-detail: print individual trace events.
Prompt The capital of France is, 1 greedy token, --ctx 256,
--no-repack, --backend forgecore, --enforce-ram, cold-cache primed.
Exact bytes: 8G = 8,589,934,592; 4G = 4,294,967,296; 1G =
1,073,741,824; 100M = 104,857,600.
| Model | Budget | Exact bytes | Exit | Peak charged | max events |
OOM kills | Model mapping resident | Output |
|---|---|---|---|---|---|---|---|---|
| Qwen2.5-1.5B Q4_0 | 8G | 8,589,934,592 | 0 | 1,243,525,120 | 0 | 0 | 1,011.16 MiB (100%) | Paris |
| Qwen2.5-1.5B Q4_0 | 4G | 4,294,967,296 | 0 | 1,244,364,800 | 0 | 0 | 1,011.16 MiB (100%) | Paris |
| Qwen2.5-1.5B Q4_0 | 1G | 1,073,741,824 | 0 | 1,073,741,824 | 1,457 | 0 | 857.93 MiB (84.8%) | Paris |
| Qwen2.5-1.5B Q4_0 | 100M | 104,857,600 | 137 | 104,857,600 | 7,814 | 2 | n/a (OOM) | none |
| Pythia-70M Q8_0 | 8G | 8,589,934,592 | 0 | 137,224,192 | 0 | 0 | 71.48 MiB (100%) | the |
| Pythia-70M Q8_0 | 4G | 4,294,967,296 | 0 | 137,224,192 | 0 | 0 | 71.48 MiB (100%) | the |
| Pythia-70M Q8_0 | 1G | 1,073,741,824 | 0 | 137,224,192 | 0 | 0 | 71.48 MiB (100%) | the |
| Pythia-70M Q8_0 | 100M | 104,857,600 | 0 | 104,857,600 | 373 | 0 | 33.24 MiB (46.5%) | the |
| GPT-NeoX-20B Q4_K_M (prefetch) | 8G | 8,589,934,592 | 0 | 8,589,934,592 | 54,721 | 0 | 7.65 GiB (62.5%) | Paris |
GPT-NeoX-20B Q4_K_M (--no-prefetch) |
8G | 8,589,934,592 | 0 | 8,589,934,592 | 7,979 | 0 | 7.64 GiB (62.4%) | Paris |
"Model mapping resident" is a point-in-time /proc/self/smaps sample taken
after decode, not a peak. "Peak charged" is the cgroup's memory.peak.
The Qwen 100M case is a controlled failure: its minimum anonymous working set
(155.74 MiB plus kernel overhead) is larger than 100 MiB, so the kernel OOM-kills
the process group. The supervisor reports exit 137, oom_kill=2, and
Result: cgroup OOM kill; it does not report success. Pythia-70M at 100M is
different: its smaller anonymous working set fits, and the kernel reclaims
about half of its 71.48 MiB weight mapping while the run still completes.
Qwen2.5-1.5B Q4_0, same prompt, 8 greedy tokens, --ctx 256, cold-cache primed.
The unbounded row is a separate warm/no-cgroup reference run; the enforced rows
are the meaningful comparison. Warm and cold no-enforce runs were nearly
identical on this SSD (TTFT 230 ms, 11.8 tok/s), so the slowdown below is
dominated by cgroup reclaim, not cache temperature.
| Configuration | Budget | Peak charged | Model resident | TTFT | Decode | Wall | Max RSS |
|---|---|---|---|---|---|---|---|
Unbounded, --no-repack |
1G requested (diagnostic) | n/a | 1,011.16 MiB (100%) | 230 ms | 11.84 tok/s | 1.50 s | 1.16 GiB |
| Enforced, repack enabled | 1G | 1 GiB | 147.67 MiB (14.6%) | 430.9 ms | 3.43 tok/s | 3.86 s | 1.005 GiB |
Enforced, --no-repack, prefetch |
1G | 1 GiB | 845.59 MiB (83.6%) | 345.1 ms | 3.63 tok/s | 3.07 s | 1.006 GiB |
Enforced, --no-repack, --no-prefetch |
1G | 1 GiB | 843.42 MiB (83.4%) | 709.3 ms | 3.29 tok/s | 3.32 s | 1.01 GiB |
Enforced, --no-repack |
512M | 512 MiB | 334.74 MiB (33.1%) | 656 ms | 1.59 tok/s | 5.87 s | 530 MB |
Enforced, --no-repack |
256M | 256 MiB | 84.36 MiB (8.3%) | 790 ms | 1.53 tok/s | 6.17 s | 270 MB |
Disabling repacking at 1G reduced TTFT from 430.9 ms to 345.1 ms and wall time from 3.86 s to 3.07 s while cutting anonymous residency by about 5.5x. The 512M and 256M runs keep only 33.1% and 8.3% of the weight mapping resident and pay for refaults; they are correctness and fit demonstrations, not throughput targets. All successful Qwen rows produced the same text.
/usr/bin/time -v file-system input counts show the refault amplification:
5,211,776 512-byte blocks at 1G (about 2.7 GB), 17,160,536 at 512M (about
8.8 GB), and 18,262,968 at 256M (about 9.4 GB), against a 1.07 GB model file.
The 20B prefetch run reported 50,728,408 blocks (about 26 GB) for a 13.1 GB
file. These are kernel block counters, not model bytes, and confirm that the
bounded runs trade I/O for resident memory.
The --no-prefetch mode removes the initial whole-file fault:
Qwen2.5-1.5B at 1G loaded in 342.3 ms instead of 706.7 ms, with process RSS
after load of 80.35 MiB instead of 1,017.01 MiB. TTFT rose from 348 ms to
709 ms because prompt processing then faulted the weights on demand; decode was
3.29 tok/s instead of 3.71 tok/s and wall was 3.32 s instead of 3.07 s. The
cgroup peak stayed exactly 1 GiB, the output was identical, and major faults
rose from 552 to 834. This is controlled demand paging, not per-layer
residency.
For GPT-NeoX-20B at 8G, prefetch took 16.94 s wall, 6.64 s setup, and
50,728,408 input blocks; --no-prefetch took 7.72 s wall, 0.126 s setup, and
25,383,576 input blocks. Load-time RSS fell from 7.95 GiB to 35.36 MiB, and
cgroup max events fell from 54,721 to 7,979. TTFT was 7.385 s, mapping
residency after decode was 7.64 GiB (62.4%), cgroup peak was exactly 8 GiB,
and the output was the same Paris.
--release-mapping-pages calls the new native
llama_model_release_pages() after load, before context creation. The native
operation walks only model->pimpl->mappings, releases their mapped fragments
with Linux madvise(MADV_DONTNEED), never unmaps, and returns ENOTSUP on
unsupported platforms. It is not pathname matching and cannot touch another
model's mappings.
Qwen2.5-1.5B, 1G, --no-repack, 8 greedy tokens:
| Mode | Mapping before | Mapping after | TTFT | Decode | Wall | Peak cgroup | Input blocks |
|---|---|---|---|---|---|---|---|
| prefetch + release | 936.70 MiB | 0 B | 498.0 ms | 3.52 tok/s | 3.36 s | 1 GiB | 6,158,504 |
| no-prefetch + release | 0 B | 0 B | 668.2 ms | 3.31 tok/s | 3.22 s | 1 GiB | 4,795,560 |
Both produced the same text. Release after a prefetch load removes the resident mapping before decode, but the later prompt refaults the weights; it is not a throughput optimization by itself.
Small fixture, Pythia-70M, 192M enforced: mapping 71.48 MiB -> 0 B, cgroup
peak 137,744,384 B, output the, exit 0.
GPT-NeoX-20B, 8G, 300 s timeout, --no-repack --release-mapping-pages
(default prefetch): mapping 7.93 GiB -> 0 B; TTFT 6.823 s; wall 12.58 s;
model mapping resident after decode 7.64 GiB (62.5%); cgroup peak exactly
8 GiB; 51,732 max events; 44,184,776 input blocks; no OOM; output Paris.
The native test loads two tiny models and reports A 8192->0, B 8192->8192,
then decodes successfully against A, confirming model scoping and pointer
validity.
--inspect-layer N and --release-layer N use native tensor metadata, not
pathname matching. RAMforge groups tensors by the tested models' blk.N.*
naming; this is layer policy, not a native layer index. If no tensors match,
the request fails explicitly.
Qwen2.5-1.5B layer 0:
--no-repack: 12 tensors, 12 file-backed, 0 anonymous.- Repacking enabled: 12 tensors, 5 file-backed, 7 anonymous. Anonymous repacked tensors are reported, not misclassified.
--release-layer 0at 1G: released 25.12 MiB, skipped 8.00 KiB shared; mapping residency936.84 -> 911.72 MiB; TTFT 351.6 ms; decode 3.55 tok/s; wall 3.16 s; cgroup peak 1 GiB; output unchanged. No-release at the same settings was TTFT 345.1 ms, 3.63 tok/s, 3.07 s, so layer release was close to neutral here.
Pythia-70M layer 0 at 192M: released 3.21 MiB, skipped 8.00 KiB; mapping
71.48 -> 68.27 MiB; wall 0.209 s; output the; exit 0.
GPT-NeoX-20B layer 0 at 8G/300 s: 12 tensors, all file-backed; 293.93 MiB
submitted to release, 0 shared pages skipped; mapping residency was
7.92 GiB -> 7.92 GiB, so no measurable RSS drop was observed because
those pages were already nonresident under the 8G cap. TTFT 9.207 s; wall
15.18 s; cgroup peak exactly 8 GiB; 56,479 max events; output Paris.
released_bytes reports page bytes submitted, not proof of an RSS drop.
This is range-scoped page advice, not execution scheduling. It does not release pages during decode, does not implement per-layer eviction, and does not guarantee a lower load-time peak.
The runtime now has a RAMforge-owned residency engine
(ramforge-runtime/src/residency_engine.rs) connected to the real forgeCORE
decode path:
- Build a model-scoped
ResourceInventoryfrom native tensor metadata. - Classify file-backed, anonymous/repacked, and unknown resources.
- Infer layer identity only from the tested
blk.N.*naming convention; an unrecognized name remains unknown. - Compute page ranges and conservative shared-page bytes.
- Apply a deterministic policy with soft target, safety reserve, required/pinned exclusions, and estimated reclaimable capacity.
- Enforce lifecycle transitions around decode:
ModelReady -> PreparingOperation -> Executing -> Synchronizing -> SafeBoundary -> Planning -> Applying -> SafeBoundary. - Apply releases only after the context is dropped at a safe boundary.
- Report estimates, native results, and before/after observations separately.
decode_tokens copies logits through llama_get_logits_ith, which synchronizes
native work; the adapter also calls synchronize() explicitly before the
lifecycle enters SafeBoundary. No release is attempted while a decode is
active. There is no intra-decode layer callback or layer eviction scheduler.
Qwen2.5-1.5B Q4_0, 1 GiB enforced, --no-repack, prompt
The capital of France is, 8 greedy tokens, --ctx 256:
| Policy | TTFT | Decode | Wall | Mapping before/after | Process RSS before/after | Peak cgroup |
|---|---|---|---|---|---|---|
| off | 360.3 ms | 3.46 tok/s | 3.21 s | 856.64 MiB / 856.64 MiB | 1.00 GiB / 1.00 GiB | 1 GiB |
| observe, target 512M | 364.0 ms | 3.20 tok/s | 3.41 s | 862.54 / 862.54 MiB | 1.00 / 1.00 GiB | 1 GiB |
| active, target 512M | 353.1 ms | 3.41 tok/s | 3.31 s | 855.68 / 476.57 MiB | 1021.21 / 642.10 MiB | 1 GiB |
Active post-operation result: 30 selected tensors, 514.44 MiB submitted,
84.00 KiB shared pages skipped; mapping delta -397,520,896 bytes; process RSS
delta -397,525,760 bytes; cgroup current unchanged at 1022.50 MiB; peak
1 GiB; no OOM. The plan's estimated_after was 506.84 MiB and
estimated_budget_met=true, but the observed process RSS remained 642.10 MiB
above the 512 MiB target. The estimate is not a guarantee: anon/KV/scratch
memory and unsubmitted/shared pages remain. The cgroup remains the enforcement
mechanism; the soft target is policy only.
Observe mode selected 31 tensors in its post-operation plan and performed no
native release; mapping and process residency were unchanged. Admission for
both target-512M runs was AdmittedWithWarning, because current process
memory plus reserve exceeded the soft target.
--graph-trace installs the pinned public scheduler eval callback. For each
observed node group, the scheduler computes the group and calls
ggml_backend_synchronize before the ask == false callback. forgeCORE
records node name, op name, source names/transient IDs, size, phase, and an
explicit BackendSynchronized completion. Tracing disabled uses the original
scheduler path.
Qwen2.5-1.5B Q4_0, 1 GiB, one token, --graph-trace-events 4096:
- 958 requested and 958 synchronized-completion events; 1,916 retained; 0 dropped.
- Manifest: 958 nodes, 1,050 tensor resources,
edges_known=true,completion_proven=true. - Last-use analysis: 1,050 graph-order relationships, 0 unknown.
intra_decode_safe=false: graph-order last use does not prove the next graph/token will not reuse the weight.- Wall 1.108 s; cgroup peak 1 GiB; output
Paris.
With --graph-trace-events 512, 1,404 events were dropped and only 512
retained; the manifest covered 256 nodes/292 resources. Bounded tracing is
explicitly incomplete at overflow.
--residency-prefetch 128M --no-prefetch on Qwen:
- Staging plan selected 95 file-backed tensors, 128.00 MiB; 884.48 MiB skipped by the staging limit.
- Native
MADV_WILLNEEDaccepted 127.72 MiB. - Mapping RSS before/after:
0 -> 0; no measurable residency change was observed. The request is a hint, not a synchronous read or a guarantee.
ramforge support reports residency capabilities: tensor metadata, release,
prefetch, trace, and between-operation residency are supported; graph
dependency/last-use proof is partial; intra-decode residency and GPU are
unsupported. It also lists the policy profiles, the ledger/reconciliation
surface, JSON report schema, and the fact that execution profiles are in-memory
diagnostic inputs rather than persisted state.
The runtime now has a model-scoped memory ledger
(ramforge-runtime/src/memory_ledger.rs) with explicit categories and
provenance. It keeps file-backed logical bytes, anonymous weights, mapping
resident bytes, mapping PSS, process RSS/peak, cgroup current/peak, staging
requested/accepted bytes, and unknown KV/scratch/runtime components distinct.
Unknown components remain None with a reason; they are not silently added as
zero. Reconciliation reports RSS - mapping resident and
cgroup current - RSS as signed differences, not as a single “overhead”
number. See MEMORY_ACCOUNTING.md.
Graph manifests now feed an in-memory execution profile
(ramforge-runtime/src/execution_profile.rs). A capture containing multiple
graph executions is split by execution id before analysis, so transient native
node ids are never merged across graphs. Profiles store stable tensor names,
graph-order access ranges, observation counts, completeness, and invalidation
state. Incomplete, cyclic, or ambiguous captures are rejected as profile
evidence. Profiles are validated against model identity and inventory
generation before reuse-aware planning. No profile persistence is
implemented. See
GRAPH_SAFETY.md.
SmolLM2-135M Q4_K_M, 272 file-backed tensors, context 256, 4 greedy tokens, target 128 MiB, no cgroup enforcement, median of two runs unless noted:
| Policy | Selected | Mapping RSS delta | Process RSS before/after | Cgroup current delta | TTFT |
|---|---|---|---|---|---|
| off | 0 | 0 B | 163.73 / 163.73 MiB | ~0 B | 24.7 ms |
| observe | 12 | 0 B | 163.64 / 163.64 MiB | ~0 B | 24.8 ms |
| conservative | 0 | 0 B | 163.61 / 163.61 MiB | ~0 B | 24.6 ms |
| memory-bound | 12 | -38,027,264 B | 163.76 / 127.50 MiB | ~0 B | 25.1 ms |
| active | 12 | -38,027,264 B | 163.78 / 127.52 MiB | ~0 B | 24.7 ms |
| reuse-aware (valid profile) | 133 | -46,702,592 B | 171.80 / 127.26 MiB | ~0 B | 26.6 ms |
The reuse-aware result required a complete 16,384-event profile; with the
default 4,096-event cap a four-token capture was truncated, the profile was
rejected as incomplete, and the plan reported profile_used=false. That is
intentional. conservative selected nothing because this model’s aligned
tensor ranges all share pages; the policy refuses to claim those pages as
uniquely reclaimable. The native release call still protected shared pages in
the other mutating policies. A 64 MiB bounded staging plan selected 227 tensors
and requested 63.98 MiB of deduplicated page ranges; the native accepted
63.98 MiB and mapping RSS stayed at 98.87 MiB, an honest zero-change hint.
The report now includes prompt tokens, generated tokens, TTFT, decode/total wall time, stop reason, and an output token fingerprint. It remains diagnostic: the soft target is not a hard limit, release submission is not physical reclamation, and cgroup usage can stay flat while mapping/process RSS falls. A reproducible harness is documented in BENCHMARKS.md.
The 20B GGUF is 13,135,032,320 B on disk and 13,133,230,080 tensor bytes,
SHA-256 1a35c13535c26bb925a2379d9f021de67a0390124ffe2051aa94412002652d6d. It
was executed with:
ramforge run models/gpt-neox-20b.i1-Q4_K_M/gpt-neox-20b.i1-Q4_K_M.gguf \
--ram 8G --enforce-ram --enforce-timeout 300 --ctx 128 --no-repack \
--no-prefetch --prompt "The capital of France is" --max-tokens 1 \
--temperature 0 --backend forgecore --memory-report --profileRemove --no-prefetch for the prefetch comparison.
With the default prefetch mapping: exit 0; actual context 256; 5 prompt
tokens; output Paris; wall 16.94 s; model load plus tokenization 6.64 s;
TTFT 10.03 s; model mapping 12.23 GiB with 7.65 GiB resident (62.5%); cgroup
peak exactly 8 GiB; anonymous 319.08 MiB; file-backed 7.65 GiB; kernel
36.45 MiB; 54,721 max events; 190 major faults; cgroup swap peak 0; max RSS
7.97 GiB.
With --no-prefetch, the same model, prompt, context, budget, and timeout
completed in 7.72 s wall with 0.126 s setup and 7.385 s TTFT. Process RSS after
load was 35.36 MiB; mapping residency after decode was 7.64 GiB (62.4%);
cgroup peak was exactly 8 GiB; anonymous 329.79 MiB; file-backed 7.64 GiB;
7,979 max events; 159 major faults; cgroup swap peak 0; max RSS 7.97 GiB;
input blocks 25,383,576. Output was the same Paris.
These are one greedy token from one run each. They are evidence that a model larger than the cap can execute with bounded charged memory; they are not throughput, quality, or bit-exact numerical parity evidence. During the tests the host's overall swap usage moved within roughly 3.5-5.4 GiB (other processes under page-cache pressure); the supervised cgroup reported zero swap and no swap configuration was changed.
- The cgroup caps charged memory, not all host page cache. Shared file pages charged to another cgroup are outside this cap; mapping RSS/PSS is reported separately.
- The default pinned loader prefaults the whole mapping at load. Large models
therefore pay a long load and reclaim/refault cost even when peak residency
is bounded.
--no-prefetchavoids the initial whole-file fault and moves the cost to prompt/decode demand faults; it does not reduce total I/O when the model is fully exercised. - There is no per-layer execution scheduler or staging queue on the forgeCORE
path.
--release-layeroperates on exact tensor ranges and is called between operations, not during decode; layer grouping uses the testedblk.N.*naming convention. The kernel's demand paging remains the per-page mechanism. --enforce-ramrequires cgroup v2 memory delegation. If unavailable, the command fails with an actionable error and does not silently fall back to diagnostic-only mode. cgroup v1 is unsupported.memory.swap.max=0is written when the swap controller exposes it, andPeak swapis reported. If that file is absent,memory.maxstill caps charged RAM but swap is not additionally excluded.- The enforced timeout path is tested: Qwen2.5-1.5B at 512M with
--enforce-timeout 1exits 124, reportsResult: timeout; cgroup killed, and leaves no cgroup behind. - The 20B model was measured only at 8G. It was not run at 4G, 1G, or 100M.
- Not run in this sprint: Qwen2.5-7B, DeepSeek-Coder-V2-Lite, and
Qwen2.5-Coder-32B. DeepSeek-Coder-V2-Lite is a 10,364,416,768 B
deepseek2model that inspects/plans but is not in the validated forgeCORE execution registry. The 32B model was not attempted without a measured plan. - GPU/VRAM execution remains out of scope.
RAMforge tracks two execution backends separately. inspect and plan are
generic GGUF operations; a yes in one execution column is not a claim about
the other backend.
| Architecture | Aliases | Inspect | Plan | Legacy executor | forgeCORE/libllama | Real-model evidence |
|---|---|---|---|---|---|---|
llama |
— | yes | yes | yes | yes | SmolLM2-135M, Q4_K_M |
qwen2 |
qwen2.5 |
yes | yes | yes | yes | Qwen2.5-1.5B, Q4_0 |
gptneox |
gpt_neox |
yes | yes | not-yet | yes | Pythia-70M-deduped, Q8_0 |
mistral |
— | yes | yes | not-yet | not-yet | literal mistral tag is not a validated execution path; Mistral GGUFs normally declare llama |
qwen3 |
— | yes | yes | not-yet | not-yet | not validated |
qwen35 |
qwen3.5, qwen3_5 |
yes | yes | not-yet | not-yet | hybrid attention/SSM; intentionally not aliased to qwen2 |
gemma |
gemma2, gemma3 |
yes | yes | not-yet | not-yet | not validated |
phi |
phi2, phi3, phi4 |
yes | yes | not-yet | not-yet | not validated |
Quantization is also backend-specific:
| GGUF type | Legacy RAMforge executor | forgeCORE/libllama |
|---|---|---|
F32, F16, BF16 |
yes | delegated to the pinned native runtime |
Q4_0, Q5_0, Q8_0 |
yes | delegated; real end-to-end evidence for Q4_0 and Q8_0 |
Q2_K, Q3_K, Q4_K, Q5_K, Q6_K, Q8_K |
yes | delegated; real end-to-end evidence for Q4_K_M |
Q4_1, Q5_1, Q8_1, IQ*, other types |
no (explicit preflight error) | delegated to llama.cpp; RAMforge makes no enumeration claim |
The legacy column is backed by the compact TensorData variants and the
block-wise kernels in ramforge-core/src/quant.rs. The forgeCORE column is
deliberately not an enumeration: once a model reaches the forgeCORE backend,
llama.cpp owns tensor decoding. RAMforge only claims the type/architecture
combinations for which it has real execution evidence.
The registry is visible with:
ramforge support
ramforge inspect MODEL.gguf
ramforge plan MODEL.gguf --ram 1Ginspect and plan print separate legacy-run and forgecore-run states.
The legacy preflight for a GPT-NeoX model fails with an actionable message:
architecture 'gptneox' is not supported by RAMforge's legacy executor, but it
is validated for execution through the forgeCORE/libllama backend:
inspection=true planning=true legacy-execution=not-yet forgecore-execution=yes
(...); re-run with `--backend forgecore`.
No weights are read on that path, and no backend switch is implicit.
Fixture: mradermacher/pythia-70m-deduped-GGUF,
pythia-70m-deduped.Q8_0.gguf, 76,722,400 B, SHA-256
94788a58bb81431b7058837ec2371c303edb20384b615f0afabfa81539294e0d,
GGUF v3, 6 layers, 512 embedding, 8 heads, 2048 FFN, 16 rotary dimensions,
parallel residual enabled.
With the fixture outside the repository:
ramforge run /path/to/pythia-70m.Q8_0.gguf --ram 1G \
--prompt "The capital of France is" --max-tokens 8 \
--temperature 0 --backend forgecore --memory-reportResult: exit 0, 5 prompt tokens [510, 5347, 273, 6181, 310], output
the capital of the French Empire., 8 tokens, 0.14 s wall, max RSS
167,668 KiB, 1 GiB requested (this particular run did not use
--enforce-ram; see the enforced matrix above), logical GGUF tensor bytes
74,948,608 B, Swaps: 0. The native llama.cpp probe encodes the same prompt as
the same five ids and produces the same greedy prefix. A second greedy run
produces identical ids.
The tokenizer fix that made this possible: when
tokenizer.ggml.add_bos_token is absent, BPE/GPT-2 vocabularies now default to
no BOS, matching llama.cpp; SentencePiece vocabularies still default to
BOS. The previous unconditional true inserted token 0 into Pythia prompts.
Independent numerical check: a NumPy GPT-NeoX forward pass implemented from the GGUF metadata and dequantized Q8_0 weights, with the same five prompt ids, reproduces the native final-token logits:
- top-1 token id: 253 in both;
- top-10 overlap: 9/10;
- cosine similarity: 1.000000046;
- max absolute logit difference: 0.929 on logits of magnitude ~1,579 (about 0.06% relative), consistent with ggml's f16 GELU lookup table and floating-point reduction order.
This is an independent architecture/graph check, not a claim of bit-exact parity. It covers one 70M fixture; it is not broad model-quality validation.
The real 20B GPT-NeoX GGUF
(models/gpt-neox-20b.i1-Q4_K_M/..., 13,135,032,320 B, 13,133,230,080 tensor
bytes) is inspectable and plannable at 8G, 4G, 1G, and 100M. It was executed
under the 8 GiB cgroup cap described above: exit 0, 7.65 GiB of the 12.23 GiB
mapping resident at the end-of-run sample, 54,721 limit events, no OOM kill,
and one greedy token Paris in 16.94 s. This supersedes the earlier inspect/plan-only status.
It is one controlled run, not a claim of acceptable throughput or quality; the
pinned loader still prefaults the whole file, and there is no per-layer staging
on the forgeCORE path. The 4G, 1G, and 100M 20B cases were not run.
Phase 4 adds a RAMforge-owned static execution boundary without changing the
legacy default. The safe ForgeCore device registry is exposed through
DeviceRegistry::discover(), with explicit DeviceId, BackendId,
DeviceKind, capability, selection, and ExecutionConfig types. The CLI
accepts:
--backend forgecore --device cpu
--backend forgecore --device id:<device-id>
--backend forgecore --device backend:<backend-id>:<index>
CPU is selectable and remains first-class. Native GPU/accelerator facts and
safe free/total memory values are preserved when ForgeCore reports them.
The pinned ForgeCore revision exposes public static model-load/offload options,
but this RAMforge refresh intentionally does not enable non-CPU model
execution, scheduling, or placement policy. Non-CPU selections therefore fail
explicitly with no CPU fallback. Zero/unavailable VRAM and native allocation
values remain unknown; device selection is not residency proof. See
docs/handoff/PHASE_4_ENGINEERING_REPORT.md
for the boundary and validation record.
- Magic, header, metadata KV, tensor descriptors, file offsets, byte lengths
MemoryBudget,parse_memory_size()(8G,8GiB,8192M,512MiB, etc.)GgufDataSourcerange readsBoundedCacheLRUramforge planwith runtime-aligned persistent/largest-layer memory preflight for executable architectures
- Legacy architectures
llama,qwen2; the explicit forgeCORE/libllama backend additionally executes validatedgptneox(GPT-NeoX/Pythia) - F32/F16/BF16, tokenizer, RMSNorm, RoPE, attention, SwiGLU, KV cache, sampling
ramforge run
- Only persistent weights resident initially; layers loaded on demand → compute → release
ResidencyStatsproves total > budget while peak resident < total and peak managed ≤ budget
Supported formats:
Q4_0: block 32, 18 bytes (2B half scaled+ 16B packed 4-bit quants, dequantd*(q-8))Q5_0: block 32, 22 bytes (2B half scaled, 4B fifth-bit mask, 16B packed 4-bit quants; dequantd*(q-16))Q8_0: block 32, 34 bytes (2B half scaled+ 32B int8 quants, dequantd*q)Q4_K: block 256, 144 bytes (2B halfd, 2B halfdmin, 12B scales (8 scales + 8 mins packed 6-bit), 128B 4-bit quants; unpack viaget_scale_min_k4(j), dequantd*sc*q - dmin*m)- Since M5.6.1:
Q2_K,Q3_K,Q5_K,Q6_K,Q8_Kblock layouts,dequantize_row_*, andmatvec_*are also implemented inquant.rswith the same resident-compact representation.Q5_0was added after real-model validation againstSmolLM2-135M-Instruct-Q4_K_M.gguf.Q4_1,Q5_1,Q8_1,IQ*remain unsupported.
Representation:
TensorDataenum:F32,F16,BF16,Q4_0(QuantizedTensor),Q5_0,Q8_0,Q2_K,Q3_K,Q4_K,Q5_K,Q6_K,Q8_KQuantizedTensor { ggml_type, shape, num_elements, raw_data: Vec<u8> }keeps quantized compact while residentresident_bytes()= raw_data.len() (e.g. 144B for 256 values Q4_K vs 1024B F32) – shows memory saving- Runtime quantized
matvec()uses compact block-wise kernels and bounds dequantization to block workspaces; the storage-independent reference helper uses one reusable decoded row buffer and remains outside the runtime budget ledger. get_embedding()for quantized token_embd dequantizes only the requested row.
Memory accounting:
- Budget accounts actual resident representation: quantized bytes + temporary block buffers, NOT full F32 expansion.
- F32 payloads read directly into final F32 storage, so both load and settled residency are 1× file bytes; no raw tensor copy coexists.
- F16/BF16 weights: decoded to F32 in RAM at load time; the budget books the true decoded residency (4 B/elem), with a 3× file-byte transient during layer load (hardened in M6.1 – see below).
- Example: Q4_K 256 elements F32 equiv 1024B, quantized resident 144B (7.1× smaller). For model with 4 layers n_embd 256 ffn 512:
- Quantized total 1.5MB, F32 equiv 10MB
- Budget 800KB, total quantized 1.5MB > budget, per-layer quantized 370KB fits, peak managed 429KB ≤ budget → inference succeeds with streaming
- Persistent weights (
token_embd,output_norm,output) use actual post-decode residency for the 25% retention policy. Resident loads charge before I/O, then settle to decoded F32 bytes or compact quantized bytes; quantized persistents remain compact.
Layer streaming integration:
Layer descriptor → GgufDataSource::read_tensor() (quantized bytes) → TensorData::Q4_K/Q8_0/Q4_0 resident → quantized matvec (block dequant) → release layer
Entire quantized model never resident simultaneously. Layers released after compute.
Compute backend:
ComputeBackendtrait F32matvecnow follows the explicit ggml layout and is wired into the inference hot path (matvec_backendinstreaming_model.rs): resident F32 weights use runtime-detected AVX2 (simd.rs) + rayon row-parallelism; quantized weights keep the compact block-wise kernels fromquant.rs
- Streaming output: generation exposes a text callback backed by a stateful tokenizer decoder; split UTF-8 byte tokens are buffered until displayable, EOS remains suppressed, and CLI diagnostics stay on stderr
- No unused terminal forward: once the final requested token is selected and emitted, RAMforge does not stream all layers again unless another token's logits are required
- Real profiling:
--profilemeasures generation wall time, prompt/prefill, prompt/decode forward counts, logical per-tensor reads/bytes, physical GGUF reads/bytes, actual/avoided seeks, coalesced ranges/gap overhead, read-buffer reuse/growth, the GGUF read path, layer loading/compute/release, tensor construction, explicit dequantization/copies, F32 and quantized matvec, allocations, logits, sampling, callback time, token latency, and layer-cache hits/misses/evictions/current/peak residency - Grouped quantized copy detail:
grouped_quantized_copy_count,grouped_quantized_copy_bytes, andgrouped_quantized_copy_timeisolate grouped quantized slice-to-owned payload copies - Memory visibility:
--memory-reportlabels RAMforge current/peak/budget separately from Linux process RSS and system memory; MemoryBudget does not claim control over RSS or page cache - Capability registry:
ramforge supportdistinguishes generic GGUF inspection/planning from execution, tokenizer, and quantization support, and reports the legacy executor and forgeCORE/libllama columns separately. Legacy execution remains limited tollamaandqwen2; validated forgeCORE/libllama execution coversllama,qwen2, andgptneox. Mistral is runnable only when represented by a validated llama-compatible GGUF;qwen35is explicitly inspect/plan-only. Legacy GPT-NeoX rejection names--backend forgecoreand never switches backends implicitly. - Execution memory preflight: for runnable architectures,
ramforge planapplies the runtime’s persistent-retention, decoded-residency, and per-tensor load-transient rules to report the largest layer and a necessary managed-memory lower bound. It explicitly excludes prompt-dependent KV and activation workspaces, so it is not presented as a sufficient runtime guarantee. - Bounded streamed-layer cache: recently used decoded layer representations may remain resident under an explicit byte capacity derived from the selected budget and largest-layer headroom. Every cache entry keeps its MemoryBudget charge, LRU eviction releases it exactly, and mandatory KV/activation/output workspaces can force safe eviction. Hits avoid GGUF rereads; planner capacity is an estimate and does not guarantee hits.
- Bounded streamed-layer read coalescing: descriptor-sorted tensors that are adjacent or separated by at most 4 KiB may share a physical read only while the full span stays at or below 64 MiB and its grouped raw-plus-resident workspace fits MemoryBudget. Arbitrary ordering and explicit tensor boundaries remain authoritative; large gaps/spans and low-headroom cases use the original individual read path. No mmap or persistent raw payload is involved.
- Cursor-aware retained handle: physical reads reuse the synchronized GGUF handle’s known absolute cursor. A seek is skipped only when the preceding successful read ended exactly at the next requested offset; failures invalidate the cursor and force the next seek.
- Per-layer grouped-buffer reuse: when a layer has multiple fully coalesced ranges and the complete scratch-plus-resident peak fits MemoryBudget, one buffer sized to the largest range is charged and reused for all ranges. If the stronger lifetime cannot fit, the existing per-range grouped path remains active; no viable model is rejected solely for this optimization.
A Mistral 7B Q4_K_M GGUF (about 4.37 GB of weights, represented as llama-compatible GGUF) completed a one-token out-of-core run with a 4 GiB RAMforge budget. This is a single environment-specific diagnostic result, not a performance guarantee: generation took about 409 s, read 11.70 GiB over 864 GGUF reads, loaded/released 96 layers, spent about 125 s in the GGUF read path and 282 s in layer compute, and peaked near 306.6 MiB of RAMforge-managed memory (about 189 MiB process RSS at report time). Prompt/prefill took about 272 s. The emitted text was ikt; execution and streaming output are validated, but model-output quality is not yet validated. Profile categories include documented overlapping subsets and must not be summed as an exclusive breakdown.
- Direct F32 I/O: full tensors and streamed F32 ranges are read into final
Vec<f32>storage; little-endian hosts avoid both the raw-byte intermediate and per-element decode loop - No destination prefill: raw range reads append into reserved spare capacity, while direct F32 reads initialize reserved F32 storage with exact I/O; short reads remain errors
- Endianness: GGUF little-endian bits are already native on little-endian hosts; big-endian hosts perform an in-place normalization without a second tensor-sized allocation
- Accounting: F32 load charge is exactly its one owned final representation (1× file bytes); F16/BF16 stay at 3× transient and 4 B/element settled; quantized loading remains 1× compact bytes
- Safety boundary: the only new unsafe operations are the audited conversion of uninitialized, aligned F32 spare capacity to an exact byte destination and
set_lenafter successfulread_exact
- Persistent startup ordering: resident persistent weights reserve the full load transient before reading; M7.1 used F32 2×, F16/BF16 3×, and compact quantized tensors 1× (M7.2 reduces only F32 to 1×)
- Atomic settlement:
MemoryBudget::resizesettles a live transient charge to exactTensorData::resident_bytes()without an uncharged gap; read/decode/settlement failures remove the current charge, and model-startup failure rolls back earlier persistent charges - Resident policy: the existing 25% threshold is applied to the representation RAMforge will actually retain (decoded F32 for F32/F16/BF16, raw compact bytes for supported quants), with checked size arithmetic
- Hidden-state lifetime:
forward_single_streamingwrites into a caller-owned output slice;generate()keepstmp:hiddenlive for that buffer's complete lifetime instead of returning a vector pasttmp:forward
- KV cache lifecycle: repeated
generate()calls on one engine work; failed generations release every charge they made (clear_kv_cache()); no stale"kv_cache"allocations - RoPE: half-split
(x[j], x[j+head_dim/2])convention withtheta_j = pos * base^(-2j/head_dim)— the true llama/qwen2 rotation (was interleaved/GPT-J pairs) - F16/BF16 accounting: decoded-F32 residency (4 B/elem) is what the budget books for persistent weights and settled layer charges
- Q/K/V biases (qwen2): loaded + charged, all-or-none validation, bias added after projection matvec (before RoPE), released with the layer; partial sets and shape mismatches are hard errors
- Memory accounting:
MemoryBudget::with_temp(name, bytes, f)is the scoped accounting helper for transient working sets (tmp:hidden,tmp:forward,tmp:embd_row,tmp:streamed_matvec,tmp:logits,tmp:sampling) – released on success and on error. Layer tensors charge before reading (peak = settled prefix + per-tensor transient) and settle to exact resident bytes after construction; a failed layer load releases all its charges. - Cache:
BoundedCache::insert_budgetedcharges each cached entry to the budget (cache:{key}), evicting LRU entries to make budget room; if nothing can be evicted, the entry is simply not cached (streaming keeps working) instead of failing or double-counting. - Matrix layout: one explicit GGML/GGUF convention everywhere:
shape = [in, out], buffer row-major[out][in],y[o] = Σ_i W[o·in+i]·x[i]. No orientation heuristics, no transpose fallbacks, no full-F32 dequantization of 2D weights – arity mismatches are hard errors. - Output projection: single caller-owned logits buffer per
generate()call; streamed (non-resident) output/embedding matrices are projected in budget-bounded row chunks (min(16 MiB, available/4), ≥ 1 row) with per-row block decode. - KV / attention: attention reads the KV history in place (no per-token prefix copies); the KV cache starts at the prompt length and grows in 256-token chunks capped at prompt+max_tokens, budget-checked with rollback on failure. No KV quantization.
- Legacy removal: the pre-M4 fully-resident F32 model loader (
LlamaModel) was deleted – it violated budget integrity, guessed orientation, and duplicated the KV prefix.
scripts/test.sh resolves FORGE_LLAMA_DIR (defaulting to the sibling
native/ tree) and supplies the process-local LD_LIBRARY_PATH needed by the
shared llama.cpp/ggml libraries. When
$FORGE_LLAMA_DIR/models/tiny-llama.gguf exists, it also exports
FORGE_TEST_MODEL so the real-adapter tests run instead of skipping. It does
not modify system configuration.
cargo fmt --all -- --check # known pre-existing diffs; see PROJECT_STATE.md
cargo check --workspace
scripts/test.sh --workspace --locked
cargo clippy --workspace --all-targets --all-features -- -D warningsInspect (shows quantized types):
cargo run -p ramforge-cli -- inspect model.gguf
# Quantization summary: Q4_K: 169 tensors, F16: 121, etc.Plan:
cargo run -p ramforge-cli -- plan model.gguf --ram 8GRun with quantized model:
cargo run -p ramforge-cli -- run model.gguf --ram 8G --prompt "Hello" --max-tokens 32
cargo run -p ramforge-cli -- run model.gguf --ram 1G --prompt "Hello" --max-tokens 16 --verbose
cargo run -p ramforge-cli -- run model.gguf --ram 4G --prompt "Hi" --max-tokens 1 --profile --memory-report
cargo run -p ramforge-cli -- support
# Verbose shows:
# Total model weight bytes: 1500160 (1.43 MiB) quantized
# F32 equiv: 10507264 (10 MiB)
# Peak resident layer bytes: 370688 (0.35 MiB)
# Peak managed bytes: 429056 / budget 819200
# Fits check: total > budget ? trueAccepted --ram syntax: 8G, 8GiB, 8192M, 512MiB, 1.5G, KB/KiB/MB/MiB/GB/GiB.
MODEL may be a single GGUF file, any shard of a standard split GGUF set, or a directory containing exactly one model. Standard *-00001-of-0000N.gguf shards are grouped and validated; multiple independent models in one directory are rejected instead of guessing. inspect reports shard count, per-shard sizes/tensor counts, total file bytes, and merged tensor types.
Chat templates declared by the GGUF are opt-in:
cargo run -p ramforge-cli -- run models/qwen2.5-1.5b-instruct-q4_0 \
--ram 1G --prompt "What is 2+2? Answer briefly." --chat --max-tokens 16
cargo run -p ramforge-cli -- run models/qwen2.5-1.5b-instruct-q4_0 \
--ram 1G --prompt "Explain streaming." --chat --system "Answer in one sentence."Without --chat, the prompt is encoded as raw text, preserving the previous behavior.
Diagnostics/profile data go to stderr; Assistant: text streams to stdout as tokens become decodable.
Out-of-core quantized example:
# Synthetic Q4_K model: 4 layers, n_embd 256, ffn 512
# Total quantized 1500160 bytes (1.43 MiB), F32 equiv 10507264 (10 MiB), budget 800K
cargo run -p ramforge-cli -- run synthetic_q4k.gguf --ram 800K --prompt "hello" --max-tokens 3 --verbose
# Proves:
# total quantized > budget
# quantized resident < F32 equiv
# peak layer < total
# peak managed <= budget
# inference succeedscrates/
ramforge-core/
gguf.rs, model.rs, types.rs
memory.rs – MemoryBudget, parse_memory_size
cache.rs – BoundedCache LRU
datasource.rs – GgufDataSource, exact range reads, split-shard virtual offsets, direct little-endian F32 reads into final storage
model_set.rs – split-GGUF discovery, validation, merged model view
chat_template.rs – conservative GGUF chat-template parser/renderer
tokenizer.rs – Tokenizer from GGUF, control-token-aware encoding
quant.rs – Q4_0, Q8_0, Q2_K/Q3_K/Q4_K/Q5_K/Q6_K/Q8_K block layouts, dequant, quantized matvec (scalar, block-wise)
tensor.rs – TensorData (F32/F16/BF16/Q4_0/Q8_0/Q2_K/Q3_K/Q4_K/Q5_K/Q6_K/Q8_K), direct F32 ownership, QuantizedTensor, resident_bytes, matvec, get_embedding
ramforge-runtime/
accounting.rs – shared tensor load-transient formulas used by execution and planning
backend.rs – ComputeBackend, CpuBackend (rayon threading, optional AVX2 for F32)
ops.rs – RoPE, attention
kv_cache.rs – KV cache explicit, budget-accounted
layer.rs – LayerDescriptor grouping
layer_cache.rs – bounded, budget-charged LRU cache for decoded streamed layers
layer_read.rs – descriptor-only bounded coalesced read planning
residency.rs – ResidencyStats
profile.rs – optional generation timing/counter collector
memory_report.rs – managed memory, Linux RSS, and system-memory visibility
support.rs – architecture/tokenizer/quantization capability registry
persistent.rs – PersistentWeight: actual resident representation ≤25% of budget, else streamed on demand
simd.rs – AVX2/FMA F32 dot/matvec kernels, runtime detection + scalar fallback (M5.6.1)
model.rs – LlamaConfig, validate_required_tensors
streaming_model.rs – StreamingLlamaModel (charge-before-read + direct F32 loads), load/release layer, caller-owned final hidden + scoped tmp:forward
inference.rs – InferenceEngine (file-backed + budget + chunk-growing KV + single logits buffer), generate()
plan.rs – file-size planning plus executable-architecture layer-memory preflight
sampling.rs – greedy, temperature, top-k/p
ramforge-cli/ – inspect, plan, support, run --verbose/--profile/--memory-report
Legacy executor architectures: llama, qwen2 (dense). Mistral works only when represented by a validated llama-compatible GGUF architecture/tensor layout. qwen3, qwen35, gemma, and phi are inspect/plan-only registry entries.
Validated forgeCORE/libllama architectures: llama, qwen2, and gptneox (GPT-NeoX/Pythia). gptneox is intentionally not implemented by the legacy executor; use --backend forgecore. Other architectures are delegated to llama.cpp only without a RAMforge validation claim.
Legacy tensor types: F32, F16, BF16, Q4_0, Q5_0, Q8_0, Q2_K, Q3_K, Q4_K, Q5_K, Q6_K, Q8_K – all usable by the legacy executor
forgeCORE/libllama tensor types: delegated to the pinned native runtime; RAMforge has end-to-end real-model evidence for Q4_0, Q4_K_M, and Q8_0 but does not claim a complete native type enumeration.
Unsupported (clear error):
- Legacy execution of
gptneox→ actionable error naming--backend forgecore; no implicit fallback - Unsupported execution architectures → capability-aware error listing detected architecture, inspect/plan status for both backends, and legacy-supported architectures
- Other quantized types (Q4_1, Q5_1, Q8_1, IQ*, …) for the legacy executor → "unsupported tensor type for inference"
- Missing tensors → "missing tensor 'blk.0.attn_q.weight'"
- Budget too small → "RAM budget too small for layer..."
CPU-only: No GPU. The pinned native build is CPU-only; the forgeCORE adapter binds CPU.
- Quantization: block size, byte size, scale handling, signed/unsigned, dequant values, truncated rejection, invalid size rejection (in
quant.rs) - Matvec: tiny known matrix + known vector vs expected F32 and vs reference dequant + F32 matvec, tolerance 1e-3 (in
quant.rs) - Layer grouping, loading, release, memory accounting, peak residency (in
layer.rs,streaming_model.rs) - Quantized layer loading: Q4_0 model, matvec zeros (in
streaming_model.rs) - Out-of-core F32: total > budget while inference succeeds (in
streaming_model.rs,inference.rs) - Out-of-core quantized: synthetic Q4_K model 1.5MB > 800KB budget, per-layer 370KB fits, peak managed ≤ budget, inference succeeds, quantized resident < F32 equiv (manual run + unit tests)
- Deterministic generation: tiny F32 model greedy 5 tokens deterministic (in
inference.rs) - Existing F32/F16/BF16 inference still works
- Direct F32 loading: ordinary-value parity, exact edge bit patterns (signed zero, finite, subnormal, infinities, NaN payloads), non-square GGML layout, exact 1× accounting, and short-read rollback
- Existing Milestone 4 streaming tests still pass
- M6 integrity proofs: scoped temp release on success/error (
memory.rs), budgeted cache inserts/evictions (cache.rs), explicit ggml layout incl. non-square Q4_0/Q4_K/F32/F16 anchors and arity-error rejections (tensor.rs,backend.rs), chunked streamed output projection + too-small-budget failure (persistent.rs), no-copy attention vs naive reference (ops.rs), chunk-growing KV preserving data with exact bytes (kv_cache.rs), end-to-end out-of-core inference with model > budget (inference.rs)
The current workspace run (cargo test --workspace --locked) passes 482 tests, 0 failed, 16 ignored. The ignored tests include the real-model Qwen2.5 diagnostic, the quantized microbenchmark, the opt-in GPT-NeoX integration test, cgroup enforcement, CLI enforcement, graph-trace, bounded prefetch, and the opt-in observe/active/JSON/reuse-aware residency checks. The cgroup and CLI enforcement tests require explicit --ignored execution, cgroup v2 delegation, real models, and libggml; fixture-gated tests report SKIP when their environment variables are unset. Run scripts/test.sh --workspace --locked to reproduce the standard invocation.
scripts/smoke-real-models.sh accepts a single GGUF file, a split GGUF shard, or a model directory containing one model's shard set. Model files remain read-only. Each case runs in a separate process, waits for exit, checks that MemAvailable is above a configurable threshold (4 GiB by default), and records the exact command, exit status, /usr/bin/time -v facts, per-file model sizes/sha256 hashes, system memory and swap samples, GPU state, and captured output. Results are written as TSV and JSON outside the repository.
cd RAMforge
scripts/smoke-real-models.sh # four 4 GiB smoke cases
scripts/smoke-real-models.sh --no-build # reuse target/debug/ramforge
scripts/smoke-real-models.sh --matrix --include-7b --timeout 900
RAMFORGE_GPTNEOX_MODEL=/path/to/pythia.gguf \
scripts/smoke-real-models.sh --include-gptneox --matrixThe latest opt-in matrix uses exact byte budgets 8G = 8,589,934,592 B, 4G = 4,294,967,296 B, 1G = 1,073,741,824 B, and 100M = 104,857,600 B. It keeps the prompt (Hello), generation length (8 tokens), greedy sampling, and backend fixed within each comparison. A controlled budget rejection is recorded without failing the suite; timeouts and signals still fail. Evidence: /tmp/ramforge-real-model-smoke/20261025-002055.
| Model / case | Backend | Budget | Exit | Wall | Max RSS | Managed peak | Evictions |
|---|---|---|---|---|---|---|---|
| SmolLM2-135M Q4_K_M | legacy | 8G | 0 | 1.22 s | 132.6 MiB | 99.19 MiB | 0 |
| SmolLM2-135M Q4_K_M | legacy | 4G | 0 | 1.16 s | 132.4 MiB | 99.19 MiB | 0 |
| SmolLM2-135M Q4_K_M | legacy | 1G | 0 | 1.26 s | 132.5 MiB | 99.19 MiB | 0 |
| SmolLM2-135M Q4_K_M | legacy | 100M | 0 | 1.19 s | 111.3 MiB | 70.50 MiB | 0 |
| Qwen2.5-1.5B Q4_0 | legacy | 8G | 0 | 3.24 s | 1121.8 MiB | 1012.05 MiB | 0 |
| Qwen2.5-1.5B Q4_0 | legacy | 4G | 0 | 3.26 s | 1121.8 MiB | 1012.05 MiB | 0 |
| Qwen2.5-1.5B Q4_0 | legacy | 1G | 0 | 3.82 s | 1099.0 MiB | 1012.49 MiB | 197 |
| Qwen2.5-1.5B Q4_0 | legacy | 100M | 0 | 6.23 s | 162.5 MiB | 76.70 MiB | 222 |
| SmolLM2-135M Q4_K_M | forgecore | 4G | 0 | 0.20 s | 361.7 MiB | not exposed | n/a |
| Qwen2.5-1.5B Q4_0 | forgecore | 4G | 0 | 1.31 s | 2773.2 MiB | not exposed | n/a |
| Pythia-70M-deduped Q8_0 (GPT-NeoX) | forgecore | 8G | 0 | 0.14 s | 163.7 MiB | not exposed | n/a |
| Pythia-70M-deduped Q8_0 (GPT-NeoX) | forgecore | 4G | 0 | 0.13 s | 163.6 MiB | not exposed | n/a |
| Pythia-70M-deduped Q8_0 (GPT-NeoX) | forgecore | 1G | 0 | 0.13 s | 163.7 MiB | not exposed | n/a |
| Pythia-70M-deduped Q8_0 (GPT-NeoX) | forgecore | 100M | 0 | 0.13 s | 163.6 MiB | not exposed | n/a |
| Pythia-70M-deduped Q8_0 (GPT-NeoX) | legacy | 1G | 1 | 0.01 s | 23.1 MiB | not exposed | n/a |
- Every legacy comparison generated byte-identical stdout at all four budgets: SmolLM2
da6b6d7d…, Qwen2.5209447e5…. The 100M Qwen run shows the budget pressure mechanism: 222 layer-cache evictions and a 76.70 MiB managed peak under 100.00 MiB. The 1G run used 197 evictions; the 4G/8G runs used none. - The earlier GPT-NeoX forgecore runs generated byte-identical stdout (
c69c103d…) at 8G, 4G, 1G, and 100M without--enforce-ram. That was budget-invariance, not out-of-core execution. The new enforced matrix above uses--enforce-ramand shows real reclaim at 1G/512M/256M. - The GPT-NeoX legacy case is a controlled architecture rejection, classified as
unsupported-architecturewith exit 1 in 0.01 s. No weights were loaded, no OOM occurred, and no fallback happened. - The 20B
gptneoxGGUF (13,135,032,320 B file, 13,133,230,080 tensor bytes) was executed at 8G under--enforce-ram --no-repack: exit 0, 7.65 GiB resident of a 12.23 GiB mapping, 54,721 limit events, no OOM kill, one tokenParisin 16.94 s. The 4G/1G/100M 20B cases were not run. - Previous evidence (not re-run in this matrix): Qwen2.5-7B Q4_K_M as a two-shard set through the legacy backend at 4G, exit 0, 5:18.67 wall, 4179.6 MiB max RSS, 4086.50 MiB managed peak, 24 cached layers, 88 evictions. That result exercises split discovery and cross-shard reads, not GPT-NeoX or forgeCORE streaming.
- ForgeCore
Swapswas 0 in every case. Its RSS includes the loader, page cache, Rust heap, and native allocations; it is not model residency. The native build is CPU-only, so none of these runs is GPU validation. - Model inputs are unchanged; per-file sizes and SHA-256 values are recorded in
model-inventory.tsvand each case'smodel-files.tsv.
ramforge run --chat reads tokenizer.chat_template from the GGUF and renders a single user message with an optional --system message. The interpreter is deliberately conservative: it supports the if/elif/else, for, set, member/index access, concatenation, comparisons, and/or/not, is defined, tojson, and whitespace-trimming constructs needed by the real templates tested here. Unsupported constructs fail explicitly; there is no silent model-specific fallback.
ramforge run models/qwen2.5-1.5b-instruct-q4_0 --ram 1G \
--prompt "What is 2+2? Answer briefly." --chat --max-tokens 16 \
--temperature 0 --verbose --memory-reportResult: exit 0, rendered 177-byte Qwen ChatML prompt, generated 2 tokens then EOS, output 4 (correct), 1:07.21 wall, max RSS 1,129,396 KiB, peak managed 1,064,027,136 B under 1 GiB, 1008 layer loads, 981 evictions. The same model with the raw non-chat prompt previously produced non-answer text; chat formatting is therefore quality evidence for this prompt. SmolLM2 also rendered its own ChatML template successfully, but its small-model output was not coherent.
A Qwen2.5-7B --chat run at 4G/900 s was stopped by the harness before emitting a token; prompt processing of the 177-byte ChatML prompt is too slow in the current scalar quantized path. That timeout is a performance limitation, not quality evidence, and it is not counted as a successful 7B chat validation.
A separate Qwen2.5 legacy run used a budget below the model's managed footprint:
ramforge run models/qwen2.5-1.5b-instruct-q4_0.gguf \
--ram 768M --prompt Hello --backend legacy --max-tokens 8 \
--temperature 0 --memory-report --profile --verboseResult: exit 0, 8 tokens, 18.36 s, max RSS 869,312 KiB, peak managed
798,260,928 B (761.28 MiB) under a 805,306,368 B (768 MiB) budget, peak
resident layer 26,341,376 B (25.12 MiB), 224 layer loads, 207 layer-cache
evictions, and Swaps: 0. This demonstrates budget-constrained eviction, not
control of total process RSS: RSS exceeded the budget because it includes
unmanaged loader, allocator, page-cache, and workspace memory.
- Current Q4_K_M CPU inference is functional but extremely slow in the measured NAS-backed Mistral run; profiling identifies both GGUF reads and scalar quantized matvec/dequantization as major costs. Performance optimization remains measurement-driven.
- The layer cache is generation-local and strict LRU. If its complete-layer capacity is smaller than a sequential full-model working set, scan-pattern thrashing can produce few or no hits; planner capacity never guarantees hit rate.
- Read coalescing is opportunistic and bounded. Large tensors/gaps, layouts exceeding the 64 MiB span cap, or insufficient grouped-buffer headroom retain one physical read per tensor; fewer reads do not guarantee lower NAS wall time until benchmarked.
- Real-model execution and streaming are validated. Qwen2.5-1.5B produced a correct chat-templated answer (
4) for a simple arithmetic prompt; broad numerical-quality validation across prompts and models is still incomplete. - GPT-NeoX/Pythia is validated through forgeCORE on one 70M Q8_0 fixture (
mradermacher/pythia-70m-deduped), including native/tokenizer parity and an independent NumPy logits check. It is not a broad GPT-NeoX quality claim. The 20B GPT-NeoX GGUF executed once at 8G under the enforced cgroup cap (one greedy token); that is a fit/mechanism result, not broad quality evidence. - The 20B GPT-NeoX result still relies on OS demand paging, not RAMforge layer staging. The pinned loader prefaults the whole file, so load time and refault cost remain high, and the 20B model was not tested at 4G, 1G, or 100M.
--ramalone is a diagnostic/planning value.--enforce-ramadds a cgroup v2memory.maxhard cap; without it, identical GPT-NeoX output at 100M and 8G is evidence of non-enforcement, not out-of-core execution. The enforced matrix above shows the difference.- Chat templates are interpreted by a conservative Jinja subset, not a full Jinja2 engine. Unsupported filters/tests/constructs fail explicitly.
--chatis opt-in and applies to one user message per invocation. - Qwen2.5-7B processed real split-shard inference at 4G, but its prompt processing is very slow; a chat-templated 7B run exceeded a 900 s harness timeout before emitting a token.
- Legacy quantized inference is limited to Q4_0, Q5_0, Q8_0, Q2_K, Q3_K, Q4_K, Q5_K, Q6_K, Q8_K; Q4_1/Q5_1/Q8_1 and IQ* quants are not supported by the legacy executor. The forgeCORE backend delegates tensor decoding to llama.cpp.
- CPU-only: quantized matvec is scalar (block-wise dequant); F32 dot/matvec have runtime-detected AVX2 kernels and rayon row-parallelism; no GPU
- Tokenizer: SentencePiece unigram (score-based Viterbi) and BPE (gpt2/qwen2 merges); other pre-tokenizers/model families untested. Without explicit metadata, BPE defaults to no BOS and SentencePiece to BOS, matching llama.cpp.
- Persistent weights (
token_embd,output_norm,output): resident when their actual retained representation is at most 25% of budget, otherwise streamed on demand with budget-charged bounded temps (persistent.rs) - KV cache F32, no quantization, no eviction; grows chunk-wise up to prompt+max_tokens
- Minimum practical: one streamed layer plus its charge-before-read transient (quantized 1× file bytes, direct F32 1×, F16/BF16 3×) and the forward working set must fit the budget; one direct F32 output row, or one raw converted/quantized row plus its F32 decode buffer, must fit as well
- Not budget-tracked by design (documented as out of scope): tokenizer vocabulary table, thread stacks, allocator fragmentation, residency bookkeeping, and optional profiling metadata (including O(tensors) read counters)
- GPU, prefetch, double buffering, async I/O
- HTTP server, MoE, speculative decoding, model downloading
- Additional quantization formats beyond Q4_0/Q5_0/Q8_0/Q2_K/Q3_K/Q4_K/Q5_K/Q6_K/Q8_K; no SIMD kernels for quantized matvec (F32 AVX2 only)
MIT OR Apache-2.0