palimpsests 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- palimpsests-0.2.0/.github/workflows/ci.yml +61 -0
- palimpsests-0.2.0/.github/workflows/release.yml +65 -0
- palimpsests-0.2.0/.gitignore +26 -0
- palimpsests-0.2.0/ARCHITECTURE.md +443 -0
- palimpsests-0.2.0/CHANGELOG.md +77 -0
- palimpsests-0.2.0/CONTRIBUTING.md +49 -0
- palimpsests-0.2.0/LICENSE +201 -0
- palimpsests-0.2.0/PKG-INFO +238 -0
- palimpsests-0.2.0/README.md +202 -0
- palimpsests-0.2.0/docs/USAGE.md +258 -0
- palimpsests-0.2.0/pyproject.toml +117 -0
- palimpsests-0.2.0/src/palimpsests/__init__.py +4 -0
- palimpsests-0.2.0/src/palimpsests/audit/__init__.py +33 -0
- palimpsests-0.2.0/src/palimpsests/audit/key_manager.py +77 -0
- palimpsests-0.2.0/src/palimpsests/audit/log.py +280 -0
- palimpsests-0.2.0/src/palimpsests/cli.py +142 -0
- palimpsests-0.2.0/src/palimpsests/context/__init__.py +40 -0
- palimpsests-0.2.0/src/palimpsests/context/block_memory.py +186 -0
- palimpsests-0.2.0/src/palimpsests/context/embeddings.py +48 -0
- palimpsests-0.2.0/src/palimpsests/context/tokens.py +47 -0
- palimpsests-0.2.0/src/palimpsests/context/window_manager.py +174 -0
- palimpsests-0.2.0/src/palimpsests/core.py +327 -0
- palimpsests-0.2.0/src/palimpsests/engine/__init__.py +40 -0
- palimpsests-0.2.0/src/palimpsests/engine/base.py +159 -0
- palimpsests-0.2.0/src/palimpsests/engine/capabilities.py +78 -0
- palimpsests-0.2.0/src/palimpsests/engine/messages.py +74 -0
- palimpsests-0.2.0/src/palimpsests/engine/session.py +55 -0
- palimpsests-0.2.0/src/palimpsests/providers/__init__.py +31 -0
- palimpsests-0.2.0/src/palimpsests/providers/errors.py +60 -0
- palimpsests-0.2.0/src/palimpsests/providers/llamacpp.py +297 -0
- palimpsests-0.2.0/src/palimpsests/providers/native.py +107 -0
- palimpsests-0.2.0/src/palimpsests/providers/ollama.py +261 -0
- palimpsests-0.2.0/src/palimpsests/providers/process.py +168 -0
- palimpsests-0.2.0/src/palimpsests/py.typed +3 -0
- palimpsests-0.2.0/src/palimpsests/registry.py +177 -0
- palimpsests-0.2.0/tests/conftest.py +65 -0
- palimpsests-0.2.0/tests/test_audit.py +186 -0
- palimpsests-0.2.0/tests/test_block_memory.py +196 -0
- palimpsests-0.2.0/tests/test_cli.py +104 -0
- palimpsests-0.2.0/tests/test_context_window.py +224 -0
- palimpsests-0.2.0/tests/test_core.py +281 -0
- palimpsests-0.2.0/tests/test_engine_contract.py +175 -0
- palimpsests-0.2.0/tests/test_llama_process.py +180 -0
- palimpsests-0.2.0/tests/test_llamacpp.py +251 -0
- palimpsests-0.2.0/tests/test_native.py +89 -0
- palimpsests-0.2.0/tests/test_ollama.py +284 -0
- palimpsests-0.2.0/tests/test_registry.py +131 -0
- palimpsests-0.2.0/tests/test_smoke.py +13 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
lint:
|
|
11
|
+
name: lint
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Set up Python
|
|
17
|
+
uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
|
|
21
|
+
# Lint WITHOUT an editable install of the package. This is the fix
|
|
22
|
+
# for a subtle isort divergence: when `palimpsests` is pip-installed
|
|
23
|
+
# (via -e .), ruff's isort sees it as an installed third-party
|
|
24
|
+
# package and regroups first-party imports differently than it does
|
|
25
|
+
# from the src-layout heuristic alone — which passed locally but
|
|
26
|
+
# failed in CI. Linting a plain checkout keeps `palimpsests`
|
|
27
|
+
# first-party by the src heuristic, deterministically.
|
|
28
|
+
- name: Install ruff only
|
|
29
|
+
run: |
|
|
30
|
+
python -m pip install --upgrade pip
|
|
31
|
+
pip install ruff==0.15.20
|
|
32
|
+
|
|
33
|
+
- name: Lint (ruff)
|
|
34
|
+
run: |
|
|
35
|
+
ruff --version
|
|
36
|
+
ruff check --output-format=full .
|
|
37
|
+
|
|
38
|
+
test:
|
|
39
|
+
name: test (${{ matrix.os }}, py${{ matrix.python-version }})
|
|
40
|
+
runs-on: ${{ matrix.os }}
|
|
41
|
+
strategy:
|
|
42
|
+
fail-fast: false
|
|
43
|
+
matrix:
|
|
44
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
45
|
+
python-version: ["3.11", "3.12"]
|
|
46
|
+
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/checkout@v4
|
|
49
|
+
|
|
50
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
51
|
+
uses: actions/setup-python@v5
|
|
52
|
+
with:
|
|
53
|
+
python-version: ${{ matrix.python-version }}
|
|
54
|
+
|
|
55
|
+
- name: Install (base + dev)
|
|
56
|
+
run: |
|
|
57
|
+
python -m pip install --upgrade pip
|
|
58
|
+
pip install -e ".[dev]"
|
|
59
|
+
|
|
60
|
+
- name: Test (pytest)
|
|
61
|
+
run: pytest -q
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
# Publish to PyPI on a version tag. Build and publish are split so the
|
|
4
|
+
# OIDC-scoped publish job has the narrowest possible permissions, and so
|
|
5
|
+
# the built artifacts are visible even if publishing is skipped.
|
|
6
|
+
#
|
|
7
|
+
# Publishing uses PyPI Trusted Publishing (OIDC) — no API token is stored
|
|
8
|
+
# in the repo. The trusted publisher must be configured once on PyPI for
|
|
9
|
+
# project "palimpsests", workflow "release.yml", environment "pypi"
|
|
10
|
+
# (a pending publisher works before the first release, so the first tag
|
|
11
|
+
# creates the project). Until that is configured, the publish step fails
|
|
12
|
+
# harmlessly; the build job still runs.
|
|
13
|
+
|
|
14
|
+
on:
|
|
15
|
+
push:
|
|
16
|
+
tags:
|
|
17
|
+
- "v*"
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
build:
|
|
21
|
+
name: build distributions
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
|
|
26
|
+
- uses: actions/setup-python@v5
|
|
27
|
+
with:
|
|
28
|
+
python-version: "3.12"
|
|
29
|
+
|
|
30
|
+
- name: Build sdist and wheel
|
|
31
|
+
run: |
|
|
32
|
+
python -m pip install --upgrade build
|
|
33
|
+
python -m build
|
|
34
|
+
|
|
35
|
+
- name: Check metadata
|
|
36
|
+
run: |
|
|
37
|
+
python -m pip install --upgrade twine
|
|
38
|
+
python -m twine check dist/*
|
|
39
|
+
|
|
40
|
+
- uses: actions/upload-artifact@v4
|
|
41
|
+
with:
|
|
42
|
+
name: dist
|
|
43
|
+
path: dist/
|
|
44
|
+
|
|
45
|
+
publish:
|
|
46
|
+
name: publish to PyPI
|
|
47
|
+
needs: build
|
|
48
|
+
runs-on: ubuntu-latest
|
|
49
|
+
# The environment gates the release and scopes the OIDC token. Create
|
|
50
|
+
# an environment named "pypi" in the repo settings; optionally add a
|
|
51
|
+
# required reviewer there for a manual approval gate on every publish.
|
|
52
|
+
environment:
|
|
53
|
+
name: pypi
|
|
54
|
+
url: https://pypi.org/p/palimpsests
|
|
55
|
+
permissions:
|
|
56
|
+
# id-token: write is what lets PyPI verify this workflow via OIDC.
|
|
57
|
+
id-token: write
|
|
58
|
+
steps:
|
|
59
|
+
- uses: actions/download-artifact@v4
|
|
60
|
+
with:
|
|
61
|
+
name: dist
|
|
62
|
+
path: dist/
|
|
63
|
+
|
|
64
|
+
- name: Publish
|
|
65
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
|
|
11
|
+
# Test / tooling
|
|
12
|
+
.pytest_cache/
|
|
13
|
+
.ruff_cache/
|
|
14
|
+
.coverage
|
|
15
|
+
htmlcov/
|
|
16
|
+
|
|
17
|
+
# Palimpsests runtime state (never commit user workspaces or models)
|
|
18
|
+
.palimpsests/
|
|
19
|
+
*.gguf
|
|
20
|
+
workspace/
|
|
21
|
+
.context-memory/
|
|
22
|
+
|
|
23
|
+
# Editors / OS
|
|
24
|
+
.vscode/
|
|
25
|
+
.idea/
|
|
26
|
+
.DS_Store
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
# Palimpsests — Architecture
|
|
2
|
+
|
|
3
|
+
**Status:** design document
|
|
4
|
+
**Scope:** the inference engine and the layers built above it
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 0. Thesis
|
|
9
|
+
|
|
10
|
+
Ollama and llama.cpp are optimized for one question: *how fast can I answer a
|
|
11
|
+
single request?* But a growing class of workloads has a different shape —
|
|
12
|
+
**agentic workloads**: a process that makes hundreds of calls in a loop, shares
|
|
13
|
+
one system prompt across calls, retries, branches, and invokes tools. That is a
|
|
14
|
+
different profile than single-request throughput.
|
|
15
|
+
|
|
16
|
+
Palimpsests builds **three levels of control** over local inference behind a
|
|
17
|
+
single abstraction, plus a **context-memory layer** that works the same on all
|
|
18
|
+
three levels. Levels 1–2 are "how do I cheaply run someone else's model." Level 3
|
|
19
|
+
is an own inference service designed for agentic, multi-session workloads on
|
|
20
|
+
local/edge hardware, where datacenter solutions (vLLM/SGLang) do not fit because
|
|
21
|
+
of their VRAM requirements.
|
|
22
|
+
|
|
23
|
+
The name reflects the central mechanism: a *palimpsest* is a parchment scraped
|
|
24
|
+
clean and rewritten, where the old text still shows through. That is exactly what
|
|
25
|
+
the context-memory layer does — it evicts the middle of the context (scrapes),
|
|
26
|
+
writes new content into the window, but the evicted text bleeds back through
|
|
27
|
+
retrieval; at level 3 the same image applies to KV state.
|
|
28
|
+
|
|
29
|
+
The governing principle of this whole document: **we do not modify the attention
|
|
30
|
+
kernel.** Everything we do lives either as engine launch parameters (levels 1–2),
|
|
31
|
+
orchestration above the engine (context-memory), or an own service with KV-state
|
|
32
|
+
management (level 3). Research gives us *principles*; we implement them where we
|
|
33
|
+
have control.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 1. Three levels of control
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
Level 1: Ollama / LM Studio (external daemon)
|
|
41
|
+
-> we don't control inference, only an HTTP client to someone else's server
|
|
42
|
+
-> maximum compatibility, zero control over loading/quant/batching
|
|
43
|
+
|
|
44
|
+
Level 2: Embedded engine (llama.cpp via subprocess)
|
|
45
|
+
-> we control loading, quant, sampling, KV cache, offload
|
|
46
|
+
-> self-contained, no external daemon
|
|
47
|
+
-> we depend on the upstream API
|
|
48
|
+
|
|
49
|
+
Level 3: Own wire protocol (own inference service)
|
|
50
|
+
-> full control: continuous batching, shared prefix KV,
|
|
51
|
+
server-side tool loop, KV persistence
|
|
52
|
+
-> the most work, but this is a platform, not a wrapper
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
All three hide behind a single `InferenceEngine` abstraction. Callers never know
|
|
56
|
+
which level is active — they ask `engine.capabilities`, never `isinstance`.
|
|
57
|
+
|
|
58
|
+
| Level | engine_id | What it adds over the previous |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| 1 | `ollama` | Basic local inference, zero configuration |
|
|
61
|
+
| 2 | `llamacpp` | Fine-grained control of memory mechanisms (§3) |
|
|
62
|
+
| 3 | `pal-native` | Agentic-first: batching, shared prefix, tool-loop, KV-memory |
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 2. The abstraction and the stateless / stateful split
|
|
67
|
+
|
|
68
|
+
Two worlds, deliberately separated so the abstraction does not break when we
|
|
69
|
+
reach level 3:
|
|
70
|
+
|
|
71
|
+
- **stateless** (`chat()` / `chat_stream()`) — "fire and forget." Levels 1–2 live
|
|
72
|
+
here entirely.
|
|
73
|
+
- **stateful** (`InferenceSession`) — a live session with persistent KV state.
|
|
74
|
+
Exists only at level 3.
|
|
75
|
+
|
|
76
|
+
### 2.1 Data shared across all levels
|
|
77
|
+
|
|
78
|
+
- `ChatChunk` — a stream increment (`delta`, `done`, metadata in the final chunk).
|
|
79
|
+
Has a `tool_call` slot for the server-side tool loop (level 3; always `None` on
|
|
80
|
+
1–2).
|
|
81
|
+
- `ChatResponse` — the accumulated stream (non-streaming is derived for free).
|
|
82
|
+
- `ModelInfo` — `name`, `size_bytes`, `engine_id`, `quant`, `loaded`.
|
|
83
|
+
- `EngineMemoryConfig` — see §3.
|
|
84
|
+
- `EngineCapabilities` — the adapter's capability declaration (drives orchestration).
|
|
85
|
+
|
|
86
|
+
### 2.2 `EngineCapabilities` — behavior via data, not types
|
|
87
|
+
|
|
88
|
+
The orchestrator asks `engine.capabilities.server_side_tools`, **not**
|
|
89
|
+
`if isinstance(engine, ...)`. Adding level 3 means raising flags in one place;
|
|
90
|
+
call sites do not change.
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
control_level: int # 1 | 2 | 3
|
|
94
|
+
streaming: bool
|
|
95
|
+
stateful_sessions: bool # holds KV between calls (level 3)
|
|
96
|
+
shared_prefix: bool # shared prefix KV across sessions (level 3)
|
|
97
|
+
server_side_tools: bool # tool-loop without re-prefill (level 3)
|
|
98
|
+
continuous_batching: bool # N sessions in one forward (level 3)
|
|
99
|
+
kv_persistence: bool # KV to/from disk (level 3)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 2.3 The contract
|
|
103
|
+
|
|
104
|
+
- `InferenceEngine.chat_stream(...)` — levels 1–2 implement only this.
|
|
105
|
+
- `InferenceEngine.open_session(...)` — level 3; on 1–2 the base class raises
|
|
106
|
+
`CapabilityUnsupported` (**loud refusal, not a silent fallback**).
|
|
107
|
+
- `InferenceSession` — a live session: `send()`, `append_tool_result()`,
|
|
108
|
+
`save_state() -> bytes`, `load_state(bytes)`, `close()`.
|
|
109
|
+
- `BaseInferenceEngine` — shared logic: `chat()` = accumulation of `chat_stream()`;
|
|
110
|
+
the default `open_session()` raises `CapabilityUnsupported`.
|
|
111
|
+
|
|
112
|
+
**Consequence:** level 3 does not rewrite the architecture — it **fills already
|
|
113
|
+
existing slots**. `NativeEngine` will be the first to return a real
|
|
114
|
+
`InferenceSession` instead of an error; no call site above it changes.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 3. `EngineMemoryConfig` — memory-reduction mechanisms (levels 1–2)
|
|
119
|
+
|
|
120
|
+
Every real memory mechanism is an **engine launch parameter**, not our logic. We
|
|
121
|
+
do not implement them — we **expose** them. Each adapter declares what it can do;
|
|
122
|
+
the UI shows only what is supported.
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
kv_cache_quant: str | None # None | "q8_0" | "q4_0" | "turbo3"
|
|
126
|
+
flash_attention: bool # PREREQUISITE for kv_cache_quant
|
|
127
|
+
gpu_layers: int | None # -ngl; None = CPU-only
|
|
128
|
+
use_mmap: bool # a trade-off, not a clear win
|
|
129
|
+
context_size: int | None # -c; the main driver of KV size
|
|
130
|
+
draft_model: str | None # speculative decoding
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 3.1 Production-ready in engines (we expose, not implement)
|
|
134
|
+
|
|
135
|
+
- **KV cache quantization** (`--cache-type-k q8_0/q4_0`) — the biggest practical
|
|
136
|
+
win. F16 KV at 128K can OOM; q8/q4 KV lets it fit. **Requires Flash Attention**,
|
|
137
|
+
otherwise KV is dequantized every step and becomes slower than no quantization
|
|
138
|
+
at all.
|
|
139
|
+
- **Flash Attention** (`--flash-attn`) — constant attention memory regardless of
|
|
140
|
+
length (tiled). Prerequisite for KV-quant.
|
|
141
|
+
- **Partial GPU offload** (`-ngl N`) — N layers on GPU, the rest on CPU. Runs
|
|
142
|
+
models larger than VRAM. RAM does not drop automatically (mmap).
|
|
143
|
+
- **mmap** (default on) — weights are mapped from disk on demand; `--no-mmap`
|
|
144
|
+
reduces RAM when the model is smaller than RAM but blocks loading a model larger
|
|
145
|
+
than RAM.
|
|
146
|
+
- **Speculative decoding** — a draft model proposes, the large one verifies in
|
|
147
|
+
batches. Speed, not memory.
|
|
148
|
+
|
|
149
|
+
### 3.2 Fresh, working, edge (know it, don't base on it)
|
|
150
|
+
|
|
151
|
+
- **TurboQuant for GGML** (`--cache-type-k turbo3`) — ~4.57x KV compression at
|
|
152
|
+
3.5 bpw. For 13B+ on very long context, near-lossless; degrades on very small
|
|
153
|
+
models. An option, not a default.
|
|
154
|
+
|
|
155
|
+
### 3.3 Research — do NOT base on this (papers, not engines)
|
|
156
|
+
|
|
157
|
+
KIVI, KVQuant, PyramidKV, XQuant rematerialization, low-rank KV,
|
|
158
|
+
retrieval-augmented attention (InfLLM, RetrievalAttention) at the kernel level —
|
|
159
|
+
mostly not in stable engine releases. A source of ideas, not a base.
|
|
160
|
+
|
|
161
|
+
### 3.4 Validation in code
|
|
162
|
+
|
|
163
|
+
`kv_cache_quant != None` requires `flash_attention == True`. This is an adapter
|
|
164
|
+
check with a **clear error**, not a silent performance regression.
|
|
165
|
+
|
|
166
|
+
### 3.5 Sensible defaults (consumer hardware: 16–32GB RAM, maybe a consumer GPU)
|
|
167
|
+
|
|
168
|
+
- **KV quant `q8_0` + Flash Attention** — near-lossless, meaningful savings on
|
|
169
|
+
context.
|
|
170
|
+
- **`turbo3` KV** — for 13B+ at 128K+, with a small-model warning.
|
|
171
|
+
- **`context_size`** — the main lever (a direct multiplier of KV size).
|
|
172
|
+
- **mmap on** by default; `--no-mmap` when the model fits in RAM.
|
|
173
|
+
- **GPU offload** — auto-detect how many layers fit, with a manual override.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## 4. Context-memory — an orchestration layer above all levels
|
|
178
|
+
|
|
179
|
+
The most important layer. It works **the same** on levels 1/2/3 because it lives
|
|
180
|
+
at the text/message level entering the model, not in the attention kernel. This
|
|
181
|
+
is what makes local inference with long memory practical on consumer hardware.
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
context/
|
|
185
|
+
window_manager.py # Ideas 1+3: sink + window + evict-middle, prefix stability
|
|
186
|
+
block_memory.py # Idea 2: evicted -> embed -> retrieval (on the fs layer)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### 4.1 Idea 1 — the StreamingLLM principle for long context
|
|
190
|
+
|
|
191
|
+
**Research:** the first ~4 tokens ("attention sinks") absorb 45–55% of attention
|
|
192
|
+
mass; evicting them under a sliding window blows up perplexity ~100x. Fix: always
|
|
193
|
+
keep the first K tokens + a sliding window of the last W. This is already a
|
|
194
|
+
standard eviction technique, not our invention — we apply it at the
|
|
195
|
+
message-history level.
|
|
196
|
+
|
|
197
|
+
**Our implementation** (not KV, but the message-history level):
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
[SINK: system prompt + first N messages] <- NEVER cut
|
|
201
|
+
[EVICTED MIDDLE: old messages] <- eviction candidates
|
|
202
|
+
[WINDOW: last W messages] <- always kept
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
The difference from naive truncation: we keep the "sink" — the system prompt and
|
|
206
|
+
first exchanges that set the task structure. Implementation is a
|
|
207
|
+
`ContextWindowManager` above the `InferenceEngine`: a tokenizer counts fit
|
|
208
|
+
(`context_size`); on overflow it keeps sink+window and evicts the middle (not
|
|
209
|
+
lost — see §4.2).
|
|
210
|
+
|
|
211
|
+
### 4.2 Idea 2 — the InfLLM principle as block retrieval over files
|
|
212
|
+
|
|
213
|
+
**Research:** InfLLM splits context into blocks, keeps most in CPU memory, pulls
|
|
214
|
+
the top-k relevant ones for the current query.
|
|
215
|
+
|
|
216
|
+
**Our implementation:** retrieval not of KV blocks (kernel level) but of **text
|
|
217
|
+
blocks of evicted context** — the same principle one level up:
|
|
218
|
+
|
|
219
|
+
- The evicted "middle" is chunked into blocks.
|
|
220
|
+
- Each block is embedded by a local embedder (`nomic-embed-text` via the active
|
|
221
|
+
engine).
|
|
222
|
+
- Blocks live in `workspace/.context-memory/` (on the fs layer); the vector index
|
|
223
|
+
is a SQLite database alongside.
|
|
224
|
+
- Before a call: embed the query, retrieve the top-k relevant blocks, insert them
|
|
225
|
+
back between sink and window.
|
|
226
|
+
|
|
227
|
+
This is effectively a RAG pattern over evicted context — a known approach applied
|
|
228
|
+
to our backing store. The result: memory bounded by disk, not RAM.
|
|
229
|
+
|
|
230
|
+
### 4.3 Idea 3 — prefix-cache-aware structuring (a free win)
|
|
231
|
+
|
|
232
|
+
**Research:** the prompt/prefix caching win only materializes if the prefix is
|
|
233
|
+
byte-stable.
|
|
234
|
+
|
|
235
|
+
**Our approach:** the sink part (system + first messages) is stable by definition
|
|
236
|
+
-> the prefix is always identical -> the engine reuses the prefix KV
|
|
237
|
+
**automatically**. So Idea 1 turns this win on for free, as long as we
|
|
238
|
+
deliberately keep the sink stable (no timestamps/dynamics at the start of the
|
|
239
|
+
prompt).
|
|
240
|
+
|
|
241
|
+
### 4.4 Layer summary
|
|
242
|
+
|
|
243
|
+
A 7B model with a real 8K context + block-memory retrieval behaves as if it had a
|
|
244
|
+
much longer context, without OOM, because the evicted content is on disk. The
|
|
245
|
+
backing store (`workspace/.context-memory/`) is deliberately shared with the
|
|
246
|
+
future level-3 KV persistence (§5.5) — it just stores something different
|
|
247
|
+
(text vs KV).
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## 5. Level 3 — own inference service (agentic-first)
|
|
252
|
+
|
|
253
|
+
Not "run someone else's model faster" (that is 1–2), but an inference service for
|
|
254
|
+
agentic, multi-session workloads. All ideas below share one architectural base: a
|
|
255
|
+
**persistent model server with KV-state management that understands requests are
|
|
256
|
+
related** (a shared system prompt, related sessions). Implemented sequentially.
|
|
257
|
+
|
|
258
|
+
> **Important (see §6 Prior art):** none of the level-3 ideas are our invention.
|
|
259
|
+
> Continuous batching and shared prefix are standard in datacenter serving engines
|
|
260
|
+
> (vLLM, SGLang). SSD KV persistence already ships in a product (oMLX) and appears
|
|
261
|
+
> in research (Persistent Q4 KV Cache, arXiv 2603.04428). Our bet is not the
|
|
262
|
+
> novelty of a mechanism, but bringing these mechanisms to the local desktop under
|
|
263
|
+
> a single abstraction together with levels 1–2 and context-memory.
|
|
264
|
+
|
|
265
|
+
### 5.1 Continuous batching for multi-session
|
|
266
|
+
|
|
267
|
+
**Problem:** several parallel agentic sessions. On 1/2 each is a separate request
|
|
268
|
+
(queue, or a separate copy of the model = multiplied RAM). Ollama/LM Studio have
|
|
269
|
+
no continuous batching (serial dispatch, FCFS).
|
|
270
|
+
|
|
271
|
+
**Level 3:** one loaded model; the decode step processes tokens from N sessions in
|
|
272
|
+
one forward pass. Not full vLLM — the right scheduler for **2–8 local sessions**,
|
|
273
|
+
not 1000 users. Follows directly from the multi-session profile.
|
|
274
|
+
|
|
275
|
+
**Slot:** the scheduler multiplexes several `InferenceSession`s onto one model.
|
|
276
|
+
|
|
277
|
+
### 5.2 Shared prefix KV across sessions
|
|
278
|
+
|
|
279
|
+
**Problem:** sessions share a large system prompt (rules, tool definitions). On
|
|
280
|
+
1/2 each call recomputes the prefix KV from scratch.
|
|
281
|
+
|
|
282
|
+
**Level 3:** compute the system prefix KV **once**, keep it in memory; each session
|
|
283
|
+
starts from the ready prefix KV (radix-tree prefix sharing like SGLang, but at the
|
|
284
|
+
scale of "a few sessions share one system prompt").
|
|
285
|
+
|
|
286
|
+
**Slot:** `open_session(system_prompt=X)` — sessions with the same X share the
|
|
287
|
+
prefix KV.
|
|
288
|
+
|
|
289
|
+
### 5.3 Server-side tool-calling loop
|
|
290
|
+
|
|
291
|
+
**Problem:** the agentic loop is host -> engine (up to tool_call) -> parse ->
|
|
292
|
+
execute tool -> **new call** with the result -> engine **re-reads the whole
|
|
293
|
+
context**. Each tool-use iteration is a full re-prefill.
|
|
294
|
+
|
|
295
|
+
**Level 3:** the loop lives **inside** the service. The model generates a tool_call
|
|
296
|
+
-> decode pauses with **KV preserved** -> tool_call goes out -> the result comes
|
|
297
|
+
back -> decode **resumes from the same KV**, appending only the result. The context
|
|
298
|
+
is not re-read. For a local model where prefill is slow, this is the difference
|
|
299
|
+
between "usable" and "painful" on a 10-step tool loop.
|
|
300
|
+
|
|
301
|
+
**Slot:** `send()` yields a chunk with `tool_call`; `append_tool_result()`
|
|
302
|
+
continues from the same KV.
|
|
303
|
+
|
|
304
|
+
### 5.4 Speculative decoding with a role-specific draft model
|
|
305
|
+
|
|
306
|
+
**Research:** a draft model proposes in batches, the large one verifies; ~90 ->
|
|
307
|
+
180+ tokens/sec on high-context long-output.
|
|
308
|
+
|
|
309
|
+
**Level 3:** we integrate draft+target into our loop. The draft model can be chosen
|
|
310
|
+
per session type — a fast classifier gets an aggressive draft, long reasoning gets
|
|
311
|
+
a more accurate one.
|
|
312
|
+
|
|
313
|
+
**Slot:** `config.draft_model` at the `open_session` level.
|
|
314
|
+
|
|
315
|
+
### 5.5 KV states as persistent memory (catches up with oMLX, cross-platform)
|
|
316
|
+
|
|
317
|
+
At level 3 we have KV access -> instead of retrieving text and **recomputing** its
|
|
318
|
+
KV (as block-memory §4.2 does), we store the **KV itself** for evicted blocks to
|
|
319
|
+
disk and load it back ready.
|
|
320
|
+
|
|
321
|
+
**State of the world (see §6):** this already exists — oMLX does paged SSD KV
|
|
322
|
+
caching (safetensors, two-tier RAM/SSD), cutting TTFT from 30–90s to <5s; arXiv
|
|
323
|
+
2603.04428 does the same with Q4 KV on disk. **Our difference** is not discovery
|
|
324
|
+
but: (a) cross-platform (oMLX is Apple/MLX only), (b) integration into a single
|
|
325
|
+
abstraction together with levels 1–2 and context-memory, (c) a backing store
|
|
326
|
+
shared with the text memory of §4.2.
|
|
327
|
+
|
|
328
|
+
**Slot:** `save_state() -> bytes` / `load_state(bytes)`. The bytes go into the same
|
|
329
|
+
`workspace/.context-memory/` as the text blocks of §4.2 — text-memory ->
|
|
330
|
+
KV-memory becomes a natural transition, not a separate project.
|
|
331
|
+
|
|
332
|
+
An honest complexity assessment: this is a late-horizon feature, not a "next PR."
|
|
333
|
+
|
|
334
|
+
### 5.6 Level-3 implementation ranking
|
|
335
|
+
|
|
336
|
+
| Idea | Value | Complexity | Order |
|
|
337
|
+
|---|---|---|---|
|
|
338
|
+
| Continuous batching | Very high | Medium | 1 |
|
|
339
|
+
| Shared prefix KV | Very high | Medium | 1 (one architecture with batching) |
|
|
340
|
+
| Server-side tool loop | High | Medium-high | 2 |
|
|
341
|
+
| Speculative per-role | Medium | Medium | 3 (optional) |
|
|
342
|
+
| KV-as-memory | High | Very high | Horizon |
|
|
343
|
+
|
|
344
|
+
Ideas 1–3 share one base (persistent server + KV-state management), so once it
|
|
345
|
+
exists, tool-loop and KV-memory are additions on top of the same base, not
|
|
346
|
+
separate projects.
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## 6. Prior art & positioning (honest analysis of analogs)
|
|
351
|
+
|
|
352
|
+
A landscape survey (as of mid-2026) showed: **no project assembles this whole
|
|
353
|
+
stack, but every individual piece already exists somewhere.** We invent no new
|
|
354
|
+
mechanism. This must stay in view so strategy is not built on a false sense of
|
|
355
|
+
novelty.
|
|
356
|
+
|
|
357
|
+
### 6.1 What already exists
|
|
358
|
+
|
|
359
|
+
| Stack component | State of the world | Examples |
|
|
360
|
+
|---|---|---|
|
|
361
|
+
| Provider abstraction (L1–2) | commodity | LM Studio, Jan, ServiceStack AI Server (OSS gateway) |
|
|
362
|
+
| Sink/window context (§4.1) | known technique | StreamingLLM; practical guides |
|
|
363
|
+
| Block retrieval of evicted context (§4.2) | RAG pattern | numerous memory projects |
|
|
364
|
+
| Continuous batching on edge (§5.1) | actively worked on | Clairvoyant (SJF sidecar-proxy for Ollama/llama.cpp); vLLM/SGLang for datacenter |
|
|
365
|
+
| Shared prefix KV (§5.2) | serving standard | vLLM, SGLang |
|
|
366
|
+
| KV persistence as memory (§5.5) | **already shipped** | **oMLX** (macOS, paged SSD KV, safetensors); arXiv 2603.04428 (Q4 KV on disk) |
|
|
367
|
+
|
|
368
|
+
### 6.2 Key findings
|
|
369
|
+
|
|
370
|
+
- **oMLX** — a macOS-native inference server on mlx-lm with paged SSD KV caching:
|
|
371
|
+
cache blocks persist to disk (safetensors, two-tier RAM/SSD), survive context
|
|
372
|
+
shifts and restarts without recomputation; TTFT on long context drops from
|
|
373
|
+
30–90s to <5s. This is our §5.5, already in production — but Apple/MLX only, KV
|
|
374
|
+
persistence alone, without the three-level abstraction or context-memory.
|
|
375
|
+
- **Clairvoyant** — a drop-in sidecar proxy for serial OpenAI-compatible backends,
|
|
376
|
+
solving head-of-line blocking via SJF scheduling; directly attacks our §5.1
|
|
377
|
+
problem (batching/queueing on consumer hardware without datacenter VRAM).
|
|
378
|
+
- **ServiceStack AI Server** — an OSS self-hosted gateway with multi-provider
|
|
379
|
+
orchestration; our provider-abstraction level, but server-side, without
|
|
380
|
+
context-memory and without levels 2–3.
|
|
381
|
+
|
|
382
|
+
### 6.3 Where the value actually is
|
|
383
|
+
|
|
384
|
+
Not technology, but **integration + positioning**:
|
|
385
|
+
|
|
386
|
+
1. **The full stack as one product.** No one has assembled "three levels of
|
|
387
|
+
control under one abstraction + context-memory on all three + local-first."
|
|
388
|
+
oMLX is Apple + KV-persistence only. Clairvoyant is a batching proxy only. AI
|
|
389
|
+
Server is a gateway only. Palimpsests is the only one offering the full stack.
|
|
390
|
+
2. **Cross-platform.** The main concrete advantage over oMLX (closest by
|
|
391
|
+
substance): not tied to Apple Silicon.
|
|
392
|
+
3. **One abstraction from wrapper to native service.** The user moves through
|
|
393
|
+
levels 1->2->3 without changing code above the engine.
|
|
394
|
+
|
|
395
|
+
### 6.4 Strategic conclusion
|
|
396
|
+
|
|
397
|
+
The problem is **maturing and consolidating now**: continuous batching on edge is
|
|
398
|
+
being attacked, KV persistence already ships in a product, provider abstraction is
|
|
399
|
+
a commodity. The biggest risk is not "someone did the same thing" but "someone
|
|
400
|
+
assembles the integration faster." So the priority is to **assemble a working full
|
|
401
|
+
stack end-to-end faster than the market consolidates**, rather than perfecting each
|
|
402
|
+
level in isolation.
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
## 7. PR sequence
|
|
407
|
+
|
|
408
|
+
The order is deliberate: **context-memory (I3, I5) before llama.cpp (I6)** — pure
|
|
409
|
+
Python delivers value even at level 1, without the native-wheel CI pain, and
|
|
410
|
+
differentiates faster.
|
|
411
|
+
|
|
412
|
+
| PR | Name | Substance | Depends on |
|
|
413
|
+
|---|---|---|---|
|
|
414
|
+
| I1 | Inference Protocol + contracts | `InferenceEngine`, `InferenceSession`, `ChatChunk/Response`, `ModelInfo`, `EngineMemoryConfig`, `EngineCapabilities`, `BaseInferenceEngine`; registry `local_inference` + `active_inference_engine`; audit `engine_id` (nullable). Inert. | — |
|
|
415
|
+
| I2 | Ollama -> level-1 adapter | Port the HTTP client -> `OllamaEngine`; `chat_stream` via Ollama `stream:true`; thin re-export for compatibility. Zero behavioral diff. | I1 |
|
|
416
|
+
| I3 | `ContextWindowManager` | Ideas 1+3: sink/window/evict + prefix stability. Pure Python. | I1 |
|
|
417
|
+
| I4 | `local_chat` API / CLI | `chat`/`models`/`engine` commands; routes through `ContextWindowManager`. | I2, I3 |
|
|
418
|
+
| I5 | `BlockMemory` | Idea 2: evicted -> embed -> retrieval; backing `workspace/.context-memory/` + SQLite index. | I3, fs layer |
|
|
419
|
+
| I6 | llama.cpp level-2 adapter | `LlamaCppEngine` spawns and owns a `llama-server` subprocess (OpenAI-compatible HTTP, not the `llama-cpp-python` binding); full `EngineMemoryConfig` applied as launch flags; FA<->KV-quant validation inherited; spawn + attach modes; opt-in via `PALIMPSESTS_LLAMACPP_MODEL`; `[llamacpp]` extra is an empty marker (binary out-of-band); wire-level CI (no real llama.cpp). | I1 |
|
|
420
|
+
| I7 | Native wire slot | `NativeEngine` subclasses `BaseInferenceEngine`; `control_level=3` with every feature flag `False`; every operation raises `CapabilityUnsupported`; `is_available()` `False`. Honest placeholder — `ProcessManager` extracted later, on the first real native lifecycle. | I1 |
|
|
421
|
+
|
|
422
|
+
After that: sequential implementation of the level-3 features (§5) on top of the
|
|
423
|
+
I7 slot, in the order of §5.6.
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
## 8. Invariant principles (guardrails)
|
|
428
|
+
|
|
429
|
+
- **We do not touch the attention kernel.** Everything is engine parameters,
|
|
430
|
+
orchestration, or an own service with KV management.
|
|
431
|
+
- **Behavior via `capabilities`, not `isinstance`.** Adding a level = flags in one
|
|
432
|
+
place.
|
|
433
|
+
- **Loud refusal, not a silent fallback** (`CapabilityUnsupported`).
|
|
434
|
+
- **One active engine globally** (radio). Per-call routing comes later.
|
|
435
|
+
- **Subprocess-ready from day one.** Even in-process llama.cpp does not assume
|
|
436
|
+
in-process in the contract.
|
|
437
|
+
- **A shared backing store** for text-memory (§4.2) and KV-memory (§5.5) —
|
|
438
|
+
`workspace/.context-memory/`.
|
|
439
|
+
- **We do not claim mechanism novelty** (§6). Our bet is integration,
|
|
440
|
+
cross-platform reach, and the speed of assembling the full stack.
|
|
441
|
+
- **Research gives principles; we implement them where we have control.**
|
|
442
|
+
Kernel-level papers are a source of ideas for the application/service level, not
|
|
443
|
+
a base.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to Palimpsests are recorded here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project
|
|
5
|
+
aims to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
|
|
6
|
+
once it reaches v1.0. Before v1.0, minor versions may include breaking
|
|
7
|
+
API changes.
|
|
8
|
+
|
|
9
|
+
## [0.2.0] — 2026-07-06
|
|
10
|
+
|
|
11
|
+
The three-level architecture is now structurally complete: all three
|
|
12
|
+
control levels exist behind one `InferenceEngine` contract.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **Level 2 — llama.cpp adapter (`LlamaCppEngine`).** The first *control*
|
|
17
|
+
level: Palimpsests spawns and owns a `llama-server` subprocess, so the
|
|
18
|
+
full `EngineMemoryConfig` (context size, GPU offload, flash attention,
|
|
19
|
+
KV-cache quantization, mmap, draft model) is applied as real launch
|
|
20
|
+
flags rather than ignored. Two modes: spawn (own the server from a
|
|
21
|
+
model path) and attach (talk to a user-run server by URL). Opt-in via
|
|
22
|
+
`PALIMPSESTS_LLAMACPP_MODEL`.
|
|
23
|
+
- **Managed subprocess lifecycle (`LlamaServerProcess`).** Free-port
|
|
24
|
+
allocation, spawn, readiness by health poll, early-death detection, and
|
|
25
|
+
idempotent shutdown — scoped to `llama-server` for now.
|
|
26
|
+
- **Level 3 slot — `NativeEngine`.** A registered, honest placeholder:
|
|
27
|
+
`control_level=3` with every feature flag `False`, every operation
|
|
28
|
+
refusing with `CapabilityUnsupported`, and `is_available()` `False`.
|
|
29
|
+
The serving service (continuous batching, shared-prefix KV, server-side
|
|
30
|
+
tool loop, KV persistence) is not implemented yet.
|
|
31
|
+
- **Block-memory retrieval (`BlockMemory`).** Evicted context is embedded
|
|
32
|
+
and stored in SQLite; the most relevant blocks are retrieved back on
|
|
33
|
+
demand (numpy cosine, no vector DB). Injectable embedder, defaulting
|
|
34
|
+
through the active engine's `/api/embeddings`. Backing store shared with
|
|
35
|
+
future KV persistence under `<workspace>/.context-memory/`.
|
|
36
|
+
- **Block memory wired into the chat flow.** `chat` now stores evicted
|
|
37
|
+
messages and, lazily (only when eviction happened), retrieves relevant
|
|
38
|
+
blocks back as a single prepended system message. Graceful: without an
|
|
39
|
+
embed-capable engine or numpy, chat behaves exactly as before.
|
|
40
|
+
- **Ollama embeddings.** `OllamaEngine.embed()` exposes `/api/embeddings`,
|
|
41
|
+
the default source for block-memory vectors.
|
|
42
|
+
- **`docs/USAGE.md`** — a run + settings guide for the current state.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- `AppContext.engines` widened from `OllamaEngine` to the `InferenceEngine`
|
|
47
|
+
protocol now that multiple adapters coexist. Callers read capabilities,
|
|
48
|
+
never the concrete type, so nothing downstream changed.
|
|
49
|
+
- The `[llamacpp]` extra is now empty and documented: the server-subprocess
|
|
50
|
+
approach needs the `llama-server` binary out-of-band, not a Python
|
|
51
|
+
package. `numpy` moved to its own `[embeddings]` extra (and `[dev]`).
|
|
52
|
+
- Development status classifier is Beta; README, roadmap, and install
|
|
53
|
+
instructions updated to reflect levels 1–2 shipped and the level-3 slot.
|
|
54
|
+
|
|
55
|
+
## [0.1.0] — 2026-07-06
|
|
56
|
+
|
|
57
|
+
Initial release.
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
|
|
61
|
+
- **Level 1 — Ollama adapter (`OllamaEngine`).** Thin HTTP client to an
|
|
62
|
+
external Ollama daemon: streaming chat, model listing, availability
|
|
63
|
+
probe, and the subset of `EngineMemoryConfig` Ollama honors.
|
|
64
|
+
- **The engine contract.** `InferenceEngine` protocol, `BaseInferenceEngine`
|
|
65
|
+
(derives `chat` from `chat_stream`, refuses sessions by default),
|
|
66
|
+
`EngineCapabilities`, `EngineMemoryConfig` (with the flash-attention
|
|
67
|
+
prerequisite for KV-quant enforced), and the level-3 `InferenceSession`
|
|
68
|
+
protocol.
|
|
69
|
+
- **Context-window manager.** Sink/window/evict fitting to a token budget,
|
|
70
|
+
reporting what it evicted.
|
|
71
|
+
- **Registry** — one active engine globally (radio, not checkbox).
|
|
72
|
+
- **Audit log** — append-only, encrypted at rest (SQLCipher) with a key
|
|
73
|
+
from the OS keychain, falling back to an ephemeral key headless.
|
|
74
|
+
- **CLI** — `chat`, `models`, `engine list` / `engine use`.
|
|
75
|
+
|
|
76
|
+
[0.2.0]: https://github.com/Assault-Consulting/Palimpsests/releases/tag/v0.2.0
|
|
77
|
+
[0.1.0]: https://github.com/Assault-Consulting/Palimpsests/releases/tag/v0.1.0
|