Skip to content

Memory architecture M1 — Flat decode (Storage/Scope/TensorView, matmul via KernelRegistry, TraceSink) #1002

Closed
17 of 17 issues completed

Description

@michalharakal
Parent tracking issue for **M1 — Flat decode** of the SKEEP-003 memory architecture (umbrella: #932). Design docs: `docs/design/memory/memory-architecture-proposal.md` (proposal rev. 2, §-numbers below refer to it) and `docs/design/memory/memory-architecture-milestones-prd.md` (PRD, requirement IDs `Mx-Fy`/`Mx-Ay`) — both land with the SKEEP-003 docs PR; until merged they are attached to #932.

## Goal
Run a real 1B-class model with weights mapped outside the heap and a recycled `Forward` scope so memory over 1 000 decode steps is a flat line, and single-token decode on packed weights goes through the kernel registry with no special cases (#993/#991 class closed by construction).

## Sample app / acceptance vehicle
`skainet-decode` CLI + minimal Android activity (home decided in its own sub-issue: SKaiNET core vs SKaiNET-transformers `llm-apps`): prefill + N decode tokens, before/after table (peak RSS, RSS@1000, allocations/step, adapters/step, TTFT, tok/s, effective bandwidth), Perfetto trace.

## Consumes (proposal §9 phases)
P1 complete · P2 (`Storage` Heap/OffHeap/Mapped, `Owner`, `Scope` Model/Forward/Ambient, `TensorView`, `TensorData` façades, `TraceSink` + allocation events) · P3 for **matmul only** · the P2 prototype benchmark (decision #6) passed first.

## Acceptance criteria (PRD)
- M1-A1 flat RSS: RSS@1000 within 1 % of RSS@50 (JVM `-Xmx256m` and reference Android device)

The checklist is maintained by GitHub's sub-issue tracking on this issue.

Working rules (every slice)

  • One slice = one branch feature/<this-issue#>-<slug> from develop = one PR (Closes #<this issue>), merged independently.
  • develop stays usable after merge: additive API or behind façade / opt-in; no default behaviour switch without golden-parity + benchmark evidence in the PR; deprecate with ReplaceWith, never delete before a major (zero-consumer dead code may be removed with evidence in the PR). No new abstract members on TensorData/KernelProvider (defaults only); no data-class primary-constructor changes (secondary ctors only); BCV: apiDump both api/jvm and api/android, apiCheck clean.
  • Test gate before the PR (JDK 25, scripts/pr-gate.sh once it exists): ./gradlew jvmTest linuxX64Test assemble, ./gradlew verifyNpmPins jsTest wasmJsTest wasmWasiTest, ./gradlew apiCheck, ./gradlew :skainet-test:skainet-test-java:test; storage/backend slices add :skainet-lang:skainet-lang-core:jvmBenchmark and :skainet-backends:benchmarks:jvm-cpu-jmh:jmh before/after when a hot path changes.
  • Commits: conventional commit subject, Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> trailer. Do not edit CHANGELOG.md in slice PRs (parallel branches conflict on it; release notes are written at release time from the merged PRs).

Activity

  1. michalharakal commented on Aug 23, 2026

    @michalharakal
    ContributorAuthor

    M1 — Flat decode: wrap-up

    All 17 slices are merged (#1016–#1032, plus the follow-up #1077 that #1032 uncovered). What M1 set out to build exists in develop and is exercised on every commit:

    Storage / Scope / TensorView (P2) — Storage sealed with Heap everywhere, JVM/Android OffHeap (FFM MemorySegment, direct ByteBuffer) and Mapped (FileChannel.map), K/Native malloc/mmap, WASM→Heap (#1018–#1020). Scope.Model / Forward (pre-sized slab, bump-allocated, reset() per step, retain() the only escape) / Ambient (#1021). Layout + the new sk.ainet.lang.memory.TensorView, the only thing kernels receive, with a decoding get() and materialize() as the single copy point (#1022). Every dense, narrow-float and packed TensorData is now a zero-copy façade over a view (#1023, #1024).

    Dispatch (P3, matmul) — KernelKey(op, operands[(Format, LayoutClass)], placement, capabilities), rank normalised once, adapters visible rather than implicit; DefaultCpuOps.matmul and the JVM arms route through the registry via a defaulted KernelProvider.kernelFor(key), with the is-ladder kept as fallback (#1027–#1029).

    Observability — TraceSink + phase/kernel/adapter/allocation events (#1017), Perfetto/JFR/android.os.Trace exporters (#1025), SKAINET_MEMORY_DEBUG=1 (#1026), plan-vs-actual with a >10 % CI assertion (#1030). KV preallocated in Scope.Model to ctx (#1031), and the cache now declares its Format so the planner reads the KV width instead of guessing it (#1077).

    Acceptance criteria

    ID Status Evidence
    M1-A1 flat memory across steps ✅ in-core DecodeAcceptanceTest.m1a1… — forward scope is 0 bytes between steps, per-step forward use is a single distinct value after warm-up, every reset ends at 0 live bytes
    M1-A2 peak RSS ≤ file + KV + slab + 100 MB ⏳ deferred needs a real 1B GGUF; measured by the cross-repo sample
    M1-A3 zero Forward allocations/step ✅ m1a3… — 0 FORWARD allocations between steps 4→12; 0 MODEL allocations after step 1
    M1-A4 #993 / #991 through the registry ✅ both repros run as registry dispatch tests in #1027, no special-casing
    M1-A5 tok/s and matmul benchmarks ◐ partial no hot path was modified (argued per PR: packed fast paths and the FP32 2D×2D path run byte-for-byte as before); a decode tok/s number needs the real-model sample
    M1-A6 packed matmul bit-identical ✅ golden parity gate (scripts/pr-gate.sh --golden) over all seven GGML encodings + ternary + TurboQuant, the merge condition for every encoding-touching slice
    M1-A7 Perfetto trace shape ✅ m1a7… — one track per scope, kernel spans labelled by TensorId, live-bytes counter returning to "forward":0 at each reset
    M1-A8 plan vs actual within 10 % ✅ m1a8… — PlanVsActual.withinTolerance, adapter bytes 0
    M1-A9 develop green, API source-compatible ✅ every slice passed the full gate before merge; all new API is additive and @ExperimentalMemoryApi, BCV dumps (api/jvm + api/android) additive-only

    What M1 found, and fixed

    The synthetic decode harness (#1032) was worth building for the two real defects it surfaced, both fixed before this tracker closed: adopted/mapped weights were invisible to plan-vs-actual (ModelScope.adopt now emits an Allocation with site "adopted", balanced by a Free on close), and the planner assumed bf16 KV against an FP32 ring — understating a dense cache by 2× (KvCacheMode.FP32 as the stopgap, then #1077 making the store declare its own Format).

    Carried forward, deliberately

    1. The real-model sample. [S1.11] M1 sample: decide the home of skainet-decode (SKaiNET core vs SKaiNET-transformers), then build the CLI + Android activity #1032 chose option (c): the harness lives in core (it runs on every target, every commit), the real skainet-decode CLI + Android activity live in SKaiNET-transformers, where Llama/Qwen/Gemma and the decode loop actually are. That sample closes M1-A2, the tok/s half of A5, and produces the Perfetto screenshot. Tracked cross-repo, not here.
    2. Packed SPI kernels are not yet bridged into the view-keyed registry ([S1.7c] P3: DefaultCpuOpsJvm matmul arms → registered kernels; provider packs (reference always present, capabilities in the key) #1029): they need block-major bytes while a packed TensorView describes row-major file order. That contract is Packed-quant byte-order (block layout) is an unwritten, contradictory contract across the engine and downstream converters #973's subject and is a prerequisite, cross-referenced there.

    Closing. M2 (#1003) starts with the ternary encodings (#1033).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    architecturalcompute-backendCompute backends and graphstensorsTensor operations and data structurestrackingParent/tracking issue with sub-issues

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions