Weight Quantization and Memory Residency

Overview

Since the engine-adoption arc (issues #338โ€“#346, engine 0.50/0.51 line), this repository owns no quantization machinery of its own. Every family’s weight loader is a thin wrapper over the SKaiNET engine’s StreamingGgufParametersLoader, and what a weight looks like in memory is a declared, resolved form โ€” not the emergent result of per-family converter code. The historical multi-stage pipeline this page used to describe (MemSegWeightConverter, QuantPolicy, pre-transposed FP32 copies, hand-managed `Arena`s) is gone.

StreamingGgufParametersLoader
WeightForm(OUT_IN, MAPPED)

yes (Q4_K, Q6_K, Q8_0, โ€ฆ)

no (re-encode asked,
platform can't map)

DequantizeTo(FP32)

GGUF file
(Q4_K_M: 1.0 GB)

Servable from
the mapping?

BufferPackedTensorData
file-backed pages, zero-copy

Q*BlockTensorData
packed bytes on the heap

Dense FP32
(export / legacy lane)

KernelDispatch
BLOCKED_ROW_MAJOR keys

FfmRowMajorKernelPack (JVM)
JniMappedKernelPack (Android)

Dense FP32 matmul

The form is the contract: WeightForm

A loader states what it wants per tensor; the engine resolves and delivers it โ€” or visibly refuses. WeightForm has four axes:

Axis Values What it decides

encoding

KeepAsStored (default), DequantizeTo(FP32), RequantizeTo(…​)

What the bytes encode once loaded. Keep-as-stored costs nothing; a dequantization multiplies the tensor (~8ร— for Q4_K โ†’ FP32) and is priced in the memory plan before it is paid.

order

AS_STORED, KERNEL_FEED

Which order the packed blocks run in. Canonical GGUF is row-major blocks; KERNEL_FEED relayouts once at load for kernels that read input-block-major.

shape

AS_STORED, OUT_IN

Which way round the dimensions are labelled. Every loader here asks OUT_IN โ€” the truthful GGUF orientation. There is no pre-transposing and no PreTransposedWeight marker anymore.

residency

HEAP, MAPPED

Where the bytes live. MAPPED is the default across this repository: the weight is served zero-copy from file-backed pages and never charged against the managed heap.

The default every decoder-family loader requests:

WeightForm(shape = WeightShapeOrientation.OUT_IN, residency = WeightResidency.MAPPED)

A request the platform or encoding cannot honour is not an error โ€” the engine heap-stages that tensor and the memory plan says so. The precedence is always: caller override > resolver > file.

MAPPED residency: what it buys

A mapped weight is mmap-backed page cache: resident in RSS while hot, evictable under pressure, and outside the JVM/ART heap cap. Measured consequences:

  • qwen2.5-1.5B (1.0 GB Q4_K_M) runs under -Xmx512m on the JVM โ€” the plan charges ~176 MB of heap (KV cache + forward slab + headroom); the weights page against device RAM.

  • On Android (engine 0.50 measurements, Pixel 8a): the same model loads in ~445 ms with ~566 KB of heap allocated for weights, and decodes with zero steady-state page faults.

The zero-copy part is the kernel story, not just the storage story: the FfmRowMajorKernelPack (JVM) and JniMappedKernelPack (Android) register matmul kernels under BLOCKED_ROW_MAJOR dispatch keys that read canonical packed blocks where they lie โ€” mapped buffer or un-prepacked heap array โ€” so nothing is copied or dequantized on the hot path. Installing them is two lines at startup (the unified CLI does exactly this):

KernelPacks.install()             // reference + best provider tiers
FfmRowMajorKernelPack.install()   // zero-copy packed serving (JVM); JniMappedKernelPack on Android

Without the pack, dispatch falls back to the decoding reference kernel โ€” correct for every format, visibly slower, and (since engine 0.50) emitted as a trace event rather than silently absorbed.

The token embedding: row-dequant, not force-dequant

Embedding gathers rows; matmul kernels never see it. A packed embedding table must not be read through per-element get() โ€” for packed tensor data that returns the raw quantization code, not the value (an engine source-compat quirk, and the root of the historical #993 class of bugs). Instead the decoder loader rewraps a packed delivery as PackedRowDequantTensorData (transformer-core): a RowDequantSource that dequantizes exactly the rows a step looks up via PackedBlockStorage.dequantizeBlock.

This keeps a 152k ร— 1536 vocabulary at its ~131 MB packed footprint instead of a ~933 MB dense FP32 heap array โ€” and because the wrapper forwards PackedBlockStorage and the underlying view, a tied output.weight that aliases the embedding still routes matmulWeightTransposed through the packed kernel chain. (Flipping SmolLM2’s tied lm_head from dense-transpose to the packed kernel took the CLI smoke from ~3.6 to ~50 tok/s.)

The matmul seam: one expression

Every transformer DSL module projects through linearProject (transformer-core), whose entire body is:

ops.matmulWeightTransposed(input, weight)

The engine primitive serves x ยท Wแต€ for a [out, in] weight without materializing a transpose: mapped packed โ†’ zero-copy row-major kernel; heap packed โ†’ relayout-once cache + packed kernels; dense โ†’ the standard transpose path. Dispatch selects by the weight’s storage format, never by model family.

The memory plan: priced before loaded

MemoryPlans.plan prices weights (split mapped vs heap-charged), KV cache, forward slab, and headroom from the GGUF header โ€” before a byte of payload is read. The unified CLI prints it on every run and warns when the plan exceeds the heap cap; --explain-load additionally prints one line per weight saying where it lands and why:

Smollm2 135M ยท llama ยท 30 layers ยท ctx 2048
  weights   mapped, as stored              98 MB   mapped (page cache, evictable โ€” not heap)
  kv cache  bf16 @ ctx 2048                45 MB   resident   (13 MB with TurboQuant 4-bit)
  forward   prefill chunk 256              24 MB
  heap      headroom                       64 MB
  total heap                              133 MB   of 512 MB  โœ” fits

Per-step activations: the forward scope

Weights are the big number but not the only one. OptimizedLLMRuntime runs DIRECT decode inside an engine ForwardScope (#343): step activations bump-allocate from one pre-sized slab and are recycled at the step boundary, so steady-state decode allocates zero new heap bytes per token. The KV caches copy their kept history out to ambient storage (detachFromStep); a caller holding a logits tensor across steps fails loudly (StorageClosedException), never silently. forwardScopeMetrics exposes peakFloats / overflowBytes for plan-vs-actual checks โ€” ForwardScopeSteadyStateTest pins bit-identity with the unscoped path and a flat, overflow-free slab.

Format support

All seven GGML block formats the engine loader delivers โ€” Q4_0, Q5_0, Q5_1, Q8_0, Q4_K, Q5_K, Q6_K โ€” flow through the packed dispatch chain and are pinned against the engine’s canonical dequant by LinearProjectionPackedParityMatrixTest (W4A8 tolerance: packed kernels may quantize activations to int8 per block, so parity is an abs-floor/relative band, not bitwise equality). Ternary BitNet I2_S loads as packed BITNET_B1_58 (0.25 B/weight) with a BITNET_PLANES lm_head โ€” see the BitNet module. Formats outside the loader’s fail-fast set (Q2_K, Q3_K, IQ-series) are rejected at load rather than silently mishandled.