Compare commits

...
135 Commits
Author SHA1 Message Date
Niko1221 6e1a7be257 Engine 0.1.21: one model across two or three GPUs (layer split, experimental); setup requires it 2026-09-29 06:14:43 +02:00
Niko1221 6f1a24defb Layer split: measured results (bench/results/2026-09-29-layer-split) and the recommendations in docs/MULTI_GPU.md 2026-09-29 01:17:30 +02:00
Niko1221 641c62a0e9 Layer split auto: a cost model of the decode window (per-layer time from SMs x clock, missed routed mass)
Fitted on the 5080 + 3090 K sweep (18-32): it picks K=26, the measured best (prompt +18-20%
over the 5080 alone), and gives a slow third card (2080 Ti) a single layer.
2026-09-29 01:00:24 +02:00
Niko1221 52bccfe914 fused_gr: cap the shared-memory opt-in at the card's limit (Turing: 64 KB) 2026-09-29 00:34:46 +02:00
Niko1221 8d09be6051 Layer split: STRATA_SPLIT_TIMING (per-stage host time per window); docs/MULTI_GPU.md 2026-09-29 00:30:16 +02:00
Niko1221 c8b10915aa QSA scorer: an FP32 fallback for Turing (the experimental sm_75 build, a layer-split stage on a 2080 Ti) 2026-09-29 00:14:23 +02:00
Niko1221 500eb977a9 Multi-GPU in the config: "gpu": [0, 2] (setup --gpus 0,2) runs the layer split across the cards
The server sets CUDA_VISIBLE_DEVICES to the list and adds --layer-split ("layer_split" in the
config, auto by default); the Monitor reads the first (main) card.
2026-09-29 00:12:05 +02:00
Niko1221 3d5d96ddce Layer split: keep the 8 GiB pin limit across GPUs (WDDM), report a failed free-memory read 2026-09-29 00:08:55 +02:00
Niko1221 a2d13aa5b3 Layer split: N stages, the batched prompt path, checkpoints, swaps and PCIe per stage, auto placement
--layer-split K1[,K2..]|auto with --split-device D1[,D2..]: each later stage runs its layer
range on its own GPU (weights, session, cache, verify window, prompt path). The prompt path
hands each chunk's rows to the next stage, which reads it on a thread while the previous
stage reads the next chunk. Checkpoints carry every stage's session state; adaptive swaps
copy into the layer's own cache; each GPU probes its own PCIe share. "auto" scores every
placement (up to 3 GPUs) by the profile-weighted pairs each cache would hold.

STRATA_EXPERIMENTAL_SM75: lets CMake build for a Turing card (the 2080 Ti A/B).
2026-09-28 23:56:30 +02:00
Niko1221 4572b8f221 Layer split v1 (step 4): the second stage on its own GPU
--split-device D (default: the second visible GPU) runs layers [K, n) on device D with
its own copy of the dense weights, session, head, drafter and an expert cache filled
from the profile's pairs of its layers. Verifier and MtpDrafter switch to the device
they were made on (OnDevice), the pool wrapper swaps the stage's cache pointers, the
penalty history lives with the head, and the s2 LUT is per device.

v1 switches off what does not cross devices yet: checkpoints, adaptive swaps, the PCIe
share and the batched prompt path (prompts read through the verify windows).
2026-09-28 23:37:29 +02:00
Niko1221 45266e06fc Layer split v1 (step 1-2): a verify window can run a range of layers and hand its residual to the next stage
--layer-split K runs layers [0, K) and [K, n_layers) as two verify stages on the same device (sharing weights,
session and expert cache). The hand-off carries R, the last layer's pending write and inject, so the next stage
folds it in exactly as the unsplit window does. The GPU shared-memory opt-ins are now per device.
2026-09-28 23:11:45 +02:00
Niko1221 962beb84e3 multi-GPU: the helper spins instead of sleeping, and reads/writes its staging zero-copy
Each layer waits for the helper GPU on the CPU pool's critical path. Measured on RIBPC (Windows): ~0.3 ms
per round trip, more than the fast CPU needs for the same experts. The helper's context is created with
cudaDeviceScheduleSpin (STRATA_REMOTE_SPIN=0: the default), and its input/compact-output staging is mapped:
the kernels read and write it directly, two copies fewer per layer (STRATA_REMOTE_ZEROCOPY=0: copies).
2026-09-28 22:52:21 +02:00
Niko1221 4cd2461849 multi-GPU: time the helper GPU's staging/launch and wait per request 2026-09-28 22:47:06 +02:00
Niko1221 7dbe2bf85b PCIe probe: below 4 GB/s (x1 links) the PCIe share is 0 - every miss goes to the CPU pool 2026-09-28 22:42:40 +02:00
Niko1221 6cd274f03a CMake: STRATA_BUILD_TESTS=ON works without the unpublished tests/ tree 2026-09-28 22:42:25 +02:00
Daerdaal 109a209e53 Add optional multi-GPU expert caches and compact transfers 2026-09-28 22:05:16 +02:00
Niko1221andcode-martin bcd62942f1 Engine 0.1.20: a checkpoint at the end of the system prompt (PR #65 on top of #62); setup requires it
A prompt read from token 0 is also checkpointed at its first turn boundary when that is 2,048 tokens or
more (--prompt-cache-root N, 0 = off). It becomes the chain's root, which #62's retention policy pins,
so a new chat of the same client reads only what follows the system prompt and tools. Without it, #62's
root existed only for system prompts past --prompt-cache-every (16K).

Co-Authored-By: code-martin <[email protected]>
2026-09-28 19:17:28 +02:00
Niko1221 93c6ba5d78 Merge PR #62 (j-luwierski): the conversation cache keeps its shared prefix; the rest rotates LRU 2026-09-28 19:16:34 +02:00
Niko1221 1838ead7e2 PR #67 follow-up: name type 42 (Q2_0); its tests run from the repo root and on Windows (no symlinks, open memmaps) 2026-09-28 19:15:31 +02:00
Niko1221 f827ddd1c4 Merge PR #67 (acrogenesis): OrcaRouter IQ3_XXS with explicit BF16 compatibility packing 2026-09-28 19:15:09 +02:00
Niko1221 85118e318d PR #44 follow-up: keep the default from 20 GB/s up, probe native packs only, the share is read by SMs 2026-09-28 19:14:46 +02:00
Niko1221 fdf8a6249f Merge PR #44 (pipeob0): generate: probe the real host->device bandwidth for the PCIe share 2026-09-28 19:14:23 +02:00
Niko1221andMIDHAT NASHAR 3e1145d2f3 serve: report the engine's own version (INFO engine=) over BUILD.json's (from PR #41, midhatn)
Co-Authored-By: MIDHAT NASHAR <[email protected]>
2026-09-28 19:14:23 +02:00
Niko1221 f086119902 PR #63 follow-up: move the unpacked llama.cpp to third_party/llama.cpp itself, retrying a locked move 2026-09-28 19:14:09 +02:00
Niko1221 be4be48935 Merge PR #63 (alfirus): Windows PermissionError in get_llama_cpp() 2026-09-28 19:13:45 +02:00
Niko1221 763d57ddf8 PR #69 follow-up: '(hits / lookups)' in the log; what the hit rate counts (not the PCIe share) 2026-09-28 19:13:40 +02:00
Niko1221 cb708580a5 Merge PR #69 (code-martin): telemetry: per-request decode expert cache hit rate in DONE and web mo 2026-09-28 19:13:21 +02:00
Niko1221 5a60f99cc0 Merge PR #64 (pipeob0): parity tool: count a new-kernel mismatch as a failure, not just print 2026-09-28 19:13:21 +02:00
Niko1221 0ba22c7145 docs: --calibrate in the README; penalized requests are a little slower in 0.1.19 2026-09-28 19:01:31 +02:00
pipeob0 7a375041d1 parity tool: count a new-kernel mismatch as a failure, not just print it
The gate/up row and IQ4_NL down comparisons printed their distance from ggml's vec_dot without affecting the exit code, so a kernel that silently diverged still reported 0 failures. Gate both at 1e-5 (measured 3e-8 and 1e-7, so 100x headroom). Verified: 0 failures at 1e-5, and with the threshold dropped to 1e-9 the gate fires on every layer.

Also corrects the comment above the gate/up dispatch: STRATA_NO_IQ512 drops an AVX-512 CPU to the AVX-2 kernel, not to ggml; ggml's vec_dot needs both switches set.
2026-09-28 18:34:44 +02:00
Niko1221 67492fb32b Merge branch 'mcp': tools from MCP servers in the web app's chat 2026-09-28 18:25:37 +02:00
code-martin 51fce4c83e telemetry: per-request decode expert cache hit rate in DONE and web monitor
- Reset cache hits/lookups baseline at start of autoregressive decode to exclude prefill distortion
- Append req_hits and req_look to engine DONE line and log decode hit rate to stderr
- Parse decode hit metrics in server.py and record hit_rate per request in history
- Add Hit rate column to Recent requests table in web monitor
2026-09-28 18:59:58 +03:00
Niko1221 4c623f1d78 Tools from MCP servers in the web app's chat (serve/mcp.py)
A small MCP client (JSON-RPC 2.0, stdio and Streamable HTTP, standard library
only): servers from the run config ("mcp_servers", or Claude Desktop's
"mcpServers" block) or --mcp-config start with the server; their tools are
offered as <server>__<tool>. A request that sends "strata_mcp": true (the web
app does) gets them: the server runs the calls, feeds the results back and lets
the model continue, up to mcp.max_rounds; the stream carries strata_mcp events.
Other clients are unchanged. Errors, crashes and timeouts become "error: ..."
results; long results are cut at mcp.max_result_chars; Stop cancels a running
tool. GET /mcp lists servers and tools; the web app shows each call as a
collapsible block, a switch in the Sampling drawer and a Monitor card.
The mock engine takes --script more than once (answers in turn).
2026-09-28 17:28:01 +02:00
Niko1221 c1afbdac8a Tune for this PC: setup --calibrate measures the PCIe share, draft floor and CPU workers
tools/calibrate.py starts the engine through serve's StrataEngine and measures decode speed with the
three settings that depend on the machine: --pcie-frac and --spec-min-p through new per-request GEN
keys (pcie_frac=, spec_min_p=; the server forwards a request's "strata_tune"), then 2-3 worker counts
(a restart each). A setting is kept only when it beats the product default by more than 3% in an
interleaved re-measurement. Setup offers it after an interactive install and runs it with --calibrate;
the result is saved per PC + model + context in the settings file and re-applied when a config is
written, so updates keep it. The engine's INFO line now says pool_workers, pcie_frac and spec_min_p.
tools/test_calibrate.py tests it with a stand-in engine (no GPU).
2026-09-28 17:16:53 +02:00
Niko1221 069b92b38d Expert cache: retry smaller when the allocation fails; setup warns about a tiny page file (#60)
On Windows every allocation on the graphics card is also charged to the system commit (RAM + page file).
With the page file off or tiny, the expert cache's one big cudaMalloc failed while the VRAM was free and
the engine stopped. An auto-sized cache now retries at three quarters of the size (up to 8 times) and
says how much commit Windows has left; a failure that remains names the page-file setting. Setup warns
when the page file is under 4 GB; DETAILS has a troubleshooting row. STRATA_TEST_CACHE_FAIL=N fakes N
failed opens to test the retry.
2026-09-28 17:11:57 +02:00
Niko1221 de71982be9 Engine 0.1.19: penalties on every drafted token; the CLI samples; sampler hardening
- serve: a verify window samples T rows, but only row 0's penalty history was staged; rows 1..T-1 read
  unwritten slots (past the allocation at penalty_last_n=4096). Every row now gets its own history
  (strata::kernels::penalty_rows: the consumed tail, the fed-back token, the drafts before that row).
- prompt windows (their picks are discarded) skip the head sampling: Verifier::set_head_sampling.
- sampler: history ids outside [0, n_vocab) are ignored (they set bits past the shared bitmap); sampled
  top_k 0 or > 64 means 64 and every row is written (0 used to leave the row unwritten).
- server: a client's top_k 0 or > 64 is sent as 64 (it fell back to the default 20).
- CLI: the speculative loop gets the CLI's sampling (it was always greedy), the per-token path draws
  Philox(seed, position) like a verify window, and the effective sampling is printed.
- sampler_parity: fixtures 12-15 (per-row histories T=1..8 x H=1..4096, out-of-vocab ids, the top_k
  contract, penalty_rows); outputs are pre-filled so an unwritten row fails.
- setup requires engine 0.1.19.

Found by the engineering performance review (C1, C2).
2026-09-28 16:53:36 +02:00
acrogenesis 6c812b2c51 Merge upstream Strata 0.1.18 while preserving Orca compatibility
Bring in 45 upstream commits through f32577e. Update the split-GGUF test fixture with router geometry required by upstream's dynamic expert counts. Preserve the Orca BF16 conversion and native PLE compatibility paths.

Validation: Release CUDA build for sm_120; 8 packing tests; 23 server tests; sampler, gated residual, and KV streaming GPU parity checks.
2026-09-28 08:52:08 -06:00
Alfir b45c40be6a Fix Windows PermissionError in get_llama_cpp() by using shutil.move instead of pathlib.replace 2026-09-28 22:40:36 +08:00
Jakub Luwierski 5f8ad8284e serve: the conversation cache keeps its shared prefix; the rest rotates LRU
The retained checkpoints are always a prefix chain - the request loop drops
any checkpoint that is not a prefix of the current prompt, because its
positional cells are gone (the KV cache is one arena, one branch of history
at a time).  A radix cache's tree collapsed onto the one branch the session
can hold, and the chain's root is the node a radix cache keeps while its
leaves rotate: in practice the end of the system prompt, which every new
chat of the same client mounts through.

First-in-first-out kept that node only by accident of being first: after
prompt_cache newer checkpoints it was gone, and the next chat re-read the
whole prefix at prefill speed (~30 s per 30K tokens).  Now the root is
pinned while the budget allows root and leaf apart (cap >= 2; below that
the old behaviour stands), the rest rotates by least recent use, and a
mount re-dates the checkpoint it mounted through.

The policy is one pure function (conv_cache.hpp) with its scenarios in
conv_cache_test.cpp; checkpoint_save/restore and the state format are
untouched, so STRATA_CKPT_REREAD's token-for-token guarantee holds.
2026-09-28 16:27:51 +02:00
Niko1221 f32577e3c2 Engine 0.1.18; setup explains an engine that is not published yet (#58); SETUP.bat 2026-09-28 15:50:24 +02:00
Niko1221 55e05f585d sampler: PR #59's test is fixture 11; the second kernel's comment says non-empty window 2026-09-28 15:48:15 +02:00
Jakub Luwierski 81601a5124 sampler: an empty penalty window must not touch the unsized shared bitmap
`use_bits` required only a non-null history, but the launch sizes the bitmap
for `penalty_last_n > 0` alone - a caller handing over a stale history buffer
with the penalties disabled made the kernel zero `bits_words` words of a
ZERO-BYTE dynamic shared allocation.  Unreachable from the serve path
(set_history passes nullptr when the window is empty) and invisible to
memcheck (the write lands in the unallocated SMEM window), but undefined per
the contract of the public `sample_tokens`.  The gate now also requires a
non-empty window (`hlen > 0`, both kernels), and fixture 10 pins a stale
history with last_n = 0 to the no-history result in both kernels.
2026-09-28 15:46:00 +02:00
Niko1221 ec4cebfe52 Engine 0.1.17; setup requires it (the sampler fix, #53) 2026-09-28 15:25:13 +02:00
Niko1221 ae8e6e6ab5 Choosing the GPU (#51): --gpu N, setup picks the one with the most VRAM
- A config's "gpu" (or the server's --gpu N) runs the engine and the image
  encoder on that card: CUDA_VISIBLE_DEVICES=N with CUDA_DEVICE_ORDER=PCI_BUS_ID,
  so N means what nvidia-smi and the Monitor's NVML mean (CUDA's own default,
  fastest first, can number the cards differently). The Monitor reads that card.
- setup lists every NVIDIA GPU, picks the one with the most VRAM (or --gpu N),
  says so when there is more than one, and saves the choice; START-HERE.bat
  --gpu N also overrides it when starting. A number that does not exist stops
  with the list of cards. Configs without "gpu" run as before.
- Tests: the child environment; the choice on a simulated 4060 + 5070 PC; end to
  end on the real engine with --gpu 0.
2026-09-28 15:01:16 +02:00
Niko1221 7d7b5d7cf7 sampler: one penalties stage and top_p before min_p, as llama.cpp (#53)
The sampled chain applied the repetition/frequency/presence penalties a second
time after the temperature, and ran min_p before top_p: both came from the
order of the case labels in llama.cpp's common/sampling.cpp, not the order its
chain runs (params.samplers: PENALTIES ... TOP_K ... TOP_P, MIN_P ...
TEMPERATURE). With presence_penalty 1.5 at T 0.7 a repeated token was drawn at
2.6% instead of 10.5% (@hendrikp's example). Greedy decoding is unchanged.

sampler_parity: the host reference follows the same order; two new fixtures
check against an independently computed distribution (#53's example over 8,000
draws: 0.1040 vs 0.1050) and that top_p-before-min_p is observable. Both fail on
the old kernel (642 and 91 draws differ).
2026-09-28 14:57:54 +02:00
Niko1221 9a11b3eb70 tools/needle_bench.py: needle-in-a-haystack recall through a running server (#33)
Builds its long texts from this repository's own files at run time, hides a
code word at the chosen depths and asks through the API. Checked on the Coder:
1K, 32K (10/50/90%) and 128K found.
2026-09-28 14:53:30 +02:00
Niko1221 e4ee33f921 docs: 32 GB of RAM runs the Coder 2026-09-28 14:48:21 +02:00
Niko1221 c99cede026 serve: Claude Code works - /v1/messages?beta=true (#55), mid-conversation system messages (#56)
- POST routes ignore the query string (Claude Code calls /v1/messages?beta=true
  and got a 404, which it reports as a model problem); /settings too.
- A system (or late developer) message after the first one becomes a user
  message in place: the chat template only takes a system message at the start
  and raised "System message must be at the beginning" (the client saw a dropped
  connection). Kept in place, not merged into the first, so the conversation
  cache still reuses the prompt.
- Tests with the issues' own requests (they fail on the old code); DETAILS:
  how to point Claude Code at Strata.
2026-09-28 14:48:12 +02:00
Niko1221 a63d5ce78d README: How does it work? in plain words, with illustrations in the app's style (colors, icons, font; light and dark) 2026-09-28 14:38:40 +02:00
Niko1221 b500a81e4e License: MIT (the ggml code, the font and the projection vector keep their own licenses) 2026-09-28 14:29:00 +02:00
Niko1221 26cdd4cde7 Engine 0.1.16; setup requires it (the Coder needs the model's own expert count, PR #54) 2026-09-28 14:27:45 +02:00
Niko1221 47a83696e9 The GSQ-RCO Coder (256 of 512 experts): setup, profile, speeds (PR #54)
- setup: a third family, "coder" (IQ1_M), with its own expert profile; its
  shard 2 and vision encoder are the original's files and are reused; 32 GB of
  RAM is now enough to install (the Coder's experts are 23 GB).
- data/expert-profile-coder.bin: the shipped ranking mapped onto the kept
  experts through the release's rco-allocation.txt (72% GPU hits, 12 GB card);
  tools/make_profile.py takes --n-expert.
- README/DETAILS: the Coder's speeds (prompts 599-1,350 tok/s, output
  43-53 tok/s, 1K-262K) and what it is; bench/results/2026-09-28-coder.
- The experimental speed projection can be turned on for the Coder too.
2026-09-28 14:20:58 +02:00
Niko1221 ef206bb671 Merge PR #54 (pjgmobile): pruned expert variants (GSQ-RCO Coder: 256 of 512 experts) 2026-09-28 13:45:47 +02:00
Niko1221 71c60dafce setup: model files in a data folder next to Strata (no re-download after an update); freeze warnings
- The model files (models/, packs/, mtp/: 70-120 GB) live in Strata-data next to
  the Strata folder, remembered per user (%APPDATA%\Strata\settings.json,
  ~/.config/strata/settings.json). Updating by unzipping a new copy used to give
  an empty folder and a full download again. Model files of this folder and of
  earlier Strata folders on the same drive are moved there (a rename) and their
  configs repointed; files on another drive are used where they are. A new copy
  with no config sets itself up like the last install, reusing the files.
  --data-dir chooses another place.
- The start says, before and while it happens, that the PC can be slow or stop
  responding for 1-3 minutes while the experts are loaded and locked: setup's
  start box, and the server narrates the engine's start from its log (weights,
  experts + the warning, expert cache, ready, a "still starting" line every 20 s).
2026-09-28 13:45:39 +02:00
pete 97ae3b2418 Merge remote-tracking branch 'origin/main' into pruned-geometry-support
# Conflicts:
#	src/prefill/prefill.cpp
2026-09-28 12:34:36 +01:00
pete 8bef4161cf Support pruned expert variants (GSQ-RCO Coder: 256 of 512 experts)
- iq_pack.py: derive n_expert from the router tensor shape (was hardcoded 512)
- expert_layout.cpp: parse the pack header's recorded n_expert instead of the
  caller's compile-time canonical geometry; use it for the contiguity stride
- gguf_reader.hpp: Qwen4ExpGuard expert-count checks become presence-only
- generate.cpp: read qwen4exp.expert_count/expert_used_count from the model
  file into the runtime geometry; MTP drafter keeps canonical geometry (the
  draft is the original 512-expert head); serve gate allows profile-less
  caches; K from metadata
- layer.cpp: native fused router stays 512x10-only, generic top-k fallback
- prefill: expert count taken from the geometry everywhere; route_kernel
  templatized (512->16, 256->8 registers/lane, else generic router_top10)
2026-09-28 12:29:32 +01:00
Niko1221 256485621e docs: troubleshooting row for the IQ3_XXS long-prompt stop (fixed in 0.1.15) 2026-09-28 13:18:18 +02:00
Niko1221 9ac549fd32 Engine 0.1.15; setup requires it (kernels loaded at start: IQ3_XXS at 64K+ no longer runs out of VRAM mid-prompt) 2026-09-28 13:17:58 +02:00
Niko1221 06f41f49a8 docs: speed tables measured with engine 0.1.14 (prompts 1.2-2.4x faster than the old tables) 2026-09-28 13:07:35 +02:00
Niko1221 bef32ed6c3 engine: load every CUDA kernel at start (CUDA_MODULE_LOADING=EAGER)
With lazy loading, a kernel first used mid-prompt found no VRAM left for its code
once the expert cache and the prompt path had taken it: IQ3_XXS at 64K and 128K on
a 12 GB card ended with 'out of memory: cudaFuncSetAttribute' in the MMQ launch.
Loaded at start the kernels cost ~30 MB, accounted before the cache is sized
(~23 fewer slots). A user's own CUDA_MODULE_LOADING is kept.
2026-09-28 12:49:25 +02:00
Niko1221 dcb0c24422 docs: Linux updates, other distributions and WSL; the 0.1.14 stall fix; the full expert profile 2026-09-28 11:46:37 +02:00
Niko1221 235cf4bd09 serve: the prompt progress line says how far the engine is (N of M tokens), not only the prompt's size (#29) 2026-09-28 11:22:55 +02:00
Niko1221 b38d11761a Engine 0.1.14; setup requires it (the PCIe share without host CUDA calls in a verify window, #31) 2026-09-28 11:21:01 +02:00
Niko1221 fbbfb9ce60 Merge PR #43 (pipeob0): AVX2 multi-token i-quant row kernels 2026-09-28 11:04:17 +02:00
Niko1221 5c52c0fd00 Merge PR #42 (pipeob0): MEM_LARGE_PAGES on Windows - enable the privilege, round the size 2026-09-28 11:02:44 +02:00
Niko1221 9253c92ad7 The PCIe share by the copy kernel for every pack (#31, #29); setup and profile fixes (#46, #49, #45)
- --pcie-mode auto is the copy kernel for native packs too. Issue #31's thread
  dumps show the host stuck for good in the DMA path's cudaMemcpyAsync (a driver
  lock) inside a verify window while the GPU spins on the flag those copies
  raise. The copy kernel needs no host CUDA call there; IQ3_S decode 45.3 -> 44.8
  tok/s. --pcie-mode dma keeps the old path.
- setup: a locally compiled engine records its version; one kept after a failed
  compile is recognised by the version in its binary, and a config it cannot
  read (--prefill auto on 0.1.12) goes back to 2048 (#49).
- setup: nvcc in /opt/cuda (Arch), a clearer message off Ubuntu/Debian (#46);
  a failed build is tried once more (#45: ptxas's intermittent PTX parse error).
- data/expert-profile.bin ranks all 24,576 experts (the first 8,000 unchanged):
  the cache stopped at 8,000 slots on 24-32 GB cards (#46). tools/make_profile.py
  builds or extends a profile from --dump-routing traces; --expert-cache 0 with a
  profile now means auto.
2026-09-28 11:02:44 +02:00
pipeob0 f4820f507d generate: probe the real host->device bandwidth for the PCIe share
pcie_frac's defaults (0.55 native / 0.2 canonical) were measured on a PCIe 4.0 x16 link.
A x8 card in a x8 slot carries about half of that, and the copy engine's share then waits
for DMA that arrived late.  Probe the H2D bandwidth once at startup the way the engine uses
it (DMA reads from pinned host memory) and scale the default by it: an RTX 5060 Ti at x8
probes 14.1 GB/s -> pcie_frac 0.29 instead of 0.55.  --pcie-frac still wins when given, and
a machine that cannot run the probe keeps the current default.
2026-09-28 09:53:34 +02:00
pipeob0 d7a66bd33e generate: say which expert kernels an AVX2-only CPU gets
The notice printed 'the expert kernels run on AVX2' whether or not the AVX2 i-quant row
kernels were used, and with STRATA_NO_IQ256 set they are not: the run is on ggml's vec_dot.
Name the kernel, and name the switch that changed it.
2026-09-28 09:51:47 +02:00
pipeob0 d1b30db332 pinned: say how many bytes were refused, and stop blaming the privilege
The refusal note blamed SeLockMemoryPrivilege whatever VirtualAlloc answered, and did not
say what was asked for: 1450 (ERROR_NO_SYSTEM_RESOURCES, the large-page pool has that many
2 MB pages free right now), 87 (a size that is not a multiple of the minimum) and 1314 (the
privilege is genuinely off) all read as the same one line.  Observed for real on a 5700X3D
box: a run minutes after an engine was killed printed 1450 while the next one succeeded -
the killed process had not released its pinned large pages yet, which is only visible if the
byte count and the error are both in the log.
2026-09-28 09:51:47 +02:00
pipeob0 0015035642 parity tool: guard the AVX-512 calls, and check the AVX2 kernels too
The tool called the AVX-512 row kernels and the AVX-512 quantizer unconditionally, so on
an AVX2-only CPU it died with SIGILL before printing anything - the one tool you want on
that machine.  Both calls are now behind cpu_avx512_ok().

Adds a block that runs the new AVX2 kernels directly against ggml's vec_dot on Q8_0
activations (the IQ4_NL down rows' real input) and times them, so a kernel change can be
validated and measured without the engine.
2026-09-28 09:51:47 +02:00
pipeob0 89c62e5361 kernels: AVX2 multi-token i-quant kernels for an AVX2-only CPU
The native pack's i-quant gate/up rows fell back to ggml's single-token vec_dot on a
CPU without AVX-512 (Ryzen 5000 series, Ice Lake, ...): the weights were decoded once
per token, and the 5700X3D's pool was decode bound.

iq_avx2.cpp adds multi-token row kernels for IQ2_XXS/IQ2_XS/IQ3_XXS/IQ3_S/IQ2_S and
IQ4_NL down rows, and for IQ2_XS the vectorized sign decode of ggml's AVX2 vec_dot
(the 8th sign bit by parity through the bit_helper pshufb, the sign vectors out of
shuffles of one 32-byte load, the 8 scale bytes through the unpack trick), with an
even_signs table equivalent to keven_signs_q2xs.  A row is decoded once and every
token in the verify window only pays a load, a sign, a maddubs, a madd and an add.

Measured on a 5700X3D + RTX 5060 Ti, Swift native pack, greedy, 300 tokens, medians of
interleaved A/B pairs: gate/up 15.16 -> 13.84 ms/round, rows 22.7 -> 23.8 GB/s,
48.5 ms/round vs 50.9 (-4.7%).  Single thread, one expert's gate+up: IQ2_XS 3 tokens
536 -> 416 us, IQ2_XXS 390 -> 337 us.  IQ4_NL down rows 248 -> 174 us for 3 tokens.
native_expert_parity: rel 3.1e-08 vs ggml's vec_dot, 0 failures.

STRATA_NO_IQ256 and STRATA_NO_IQ4NL fall back to ggml per format.
2026-09-28 09:51:47 +02:00
pipeob0 97cf327c1e pinned: enable SeLockMemoryPrivilege and round the arena to GetLargePageMinimum
reserve() asked for MEM_LARGE_PAGES without ever enabling SeLockMemoryPrivilege in the
process token, and passed the arena size unrounded.  VirtualAlloc requires an exact
multiple of GetLargePageMinimum, so the call failed with 87 (ERROR_INVALID_PARAMETER)
and the log blamed the privilege.  Both fixed; STRATA_NO_LARGEPAGES skips the attempt
and the note now says what actually happened.
2026-09-28 09:51:46 +02:00
Niko1221 6c06571710 Linux/WSL: engine errors end the engine and reach the client
- The --serve stdin reader reads the descriptor instead of std::cin: glibc's
  exit() waits for stdin's lock, which getline held while waiting for input,
  so an engine ending on any error hung in exit() and the server waited forever.
- session_init fails when a QSA layer could not pin its KV streaming copy; it
  used to continue half-built and hit an illegal memory access at the first
  request (seen under WSL, where the driver pins only about 1 GiB). The message
  says how much was pinned and what to change.
- setup: no KV streaming under WSL (new installs and existing configs).
- server: an engine ERR after an SSE stream started is sent as an error event
  (it was a 400 response written into the open stream: an empty answer), and
  the request is recorded as "error".
2026-09-28 08:51:22 +02:00
Niko1221 fb9b924e5c verify.cpp: include <cmath> (engine 0.1.13 did not compile on Linux: std::isfinite)
MSVC brought <cmath> in through another header; GCC does not. Found by building in WSL (Ubuntu 24.04, CUDA 13.0)
through setup.sh the way a Linux user does.
2026-09-28 08:03:22 +02:00
Niko1221 ffcdb437b8 Linux setup fixes, one pool worker per physical core on Linux, --port for installs, Monitor totals
- setup.sh is executable in the repository (a fresh clone could not run `./setup.sh`: Permission denied).
- setup.sh: Debian/Ubuntu ship Python's venv without ensurepip (the separate python3-venv package); the check
  now needs both, so it installs python3-venv instead of failing to create .venv - and a .venv left without pip
  by a failed run is made again.
- Linux: physical_cores() keeps one allowed CPU per (package, core) from sysfs, like the Windows branch; SMT
  siblings no longer get their own pool worker (issue #40: 23 -> 15 workers on a 13700K, +16% decode).
- START-HERE / setup.sh `--port N` now applies to an existing install; without it the install's own port is
  kept (issue #32).
- Monitor: the server keeps the last 500 requests and totals since it started; the Monitor shows the totals and
  a "Show all" toggle (`/metrics?requests=all`), 12 rows otherwise (issue #35).
2026-09-28 07:55:39 +02:00
Niko1221 2838cbae46 prompt path: never write into the borrowable cache slots while laying out (a resident expert was corrupted)
When serving, the prompt path lays its buffers out at start in the expert-cache slots it may borrow, but those
slots hold resident experts until a request lends them. Writing the MMQ row table at layout time overwrote part
of a resident expert: decode later computed it on the GPU, and the first window that routed to it turned every
logit into NaN ("!!!!" from mid-answer, e.g. at position 138 of an 86-token prompt, even with the prompt read
through the windows). The table is written at the start of every prompt read, while the slots are lent.
STRATA_DBG_NAN=1 also reports the first verify window with non-finite logits and the state a prompt leaves.
2026-09-28 04:56:25 +02:00
Niko1221 9ee557f781 prompt path: zero the tail after the last gathered expert before MMQ (garbage answers after the first request)
MMQ reads up to one 256-value tile past a matrix's last row when the row is not a multiple of it (the down
product: 640 values). Those weights meet zero activations, harmless only if they decode finite; llama.cpp
zero-pads after every tensor. The group buffers held leftovers - after the first request's refill, other
experts' bytes read as IQ4_NL blocks - and a non-finite scale times zero gave NaN logits ("!!!!" answers from
the second request on). The group buffers get a 4 KB tail, zeroed after the last expert before each launch.
The MMQ row table (in the lent slots the refill overwrites) is written at every prompt.
STRATA_DBG_NAN=1 reports the first layer of a chunk whose MoE produced non-finite values.
2026-09-28 04:19:42 +02:00
Niko1221 7cf709e5b1 engine 0.1.13: about 2x faster long prompts; stall reports for issue #31
Prompt path (bench/results/2026-09-28-prefill-speed):
- `--prefill auto`: chunks up to 8192 tokens, sized to the cache slots the prompt path may borrow; a
  request borrows only what its prompt needs (Prefill::relayout). setup writes it and switches old
  `--prefill 2048` configs once the engine supports it.
- The attention and MoE halves share one scratch region (~260 KB less per token).
- Experts through llama.cpp's MMQ kernels (ggml-cuda mmq.cuh, compiled from the same checkout as ggml
  into strata_mmq; Strata Q2_0 blobs converted to GGUF blocks on the gather; IQ1_M layers keep FP16).
- Chunks >= 2048: every non-resident expert streams through a ring in a fixed order across layers, so
  the next layer's arrive during its attention half (384 slots when the arena is pinned, else 96).
- The PLE block for the whole chunk at once; the next chunk's PLE rows read on a thread; unpinned
  experts copied by stager threads (epoch-tagged claims).
- STRATA_PREFILL_TIMING=1 prints a per-phase GPU timeline.
Q2_0 32K prompt 572 -> 1290 tok/s, IQ3_S 383 -> 1208 (RTX 5070 12 GB). Needles 5/5 1K-262K.

Issue #31 / #29: the serve watchdog (now 60 s) prints a stall report - each expert-pool thread's state,
the verify window's flags, layers served during the stall, memory and page faults - and on Windows
writes a minidump with every thread's stack; Release builds keep a PDB. The engine prints its version.
2026-09-28 02:03:00 +02:00
acrogenesis b9da8e7bde Support Orca IQ3_XXS with explicit BF16 compatibility packing 2026-09-27 14:10:52 -06:00
Niko1221 0f3ff4dece setup: recompile a locally built engine when its source changed (git pull on Linux, issue #31)
A compiled engine was stamped only as source=local, so later runs never rebuilt it and a
git pull kept the old binary (on Linux, where there is no ready-made engine, that also meant
no v0.1.12 pool fix). BUILD.json now keeps a hash of the engine's and the image encoder's
source files; on start a changed hash recompiles (incrementally, in build/) before the model
starts, and a failed compile falls back to the installed engine.
2026-09-27 21:40:19 +02:00
Niko1221 6255c8fd14 web app: attach text files - drop them on the chat or use the attach button (issue #30)
Text files (code, notes, logs, data; up to 512 KB) become chips next to
the message and are sent as "File: name" plus a fenced block (a longer
fence when the file contains one). Binary files are refused with a toast;
pictures work as before (when images are on). The attach button is always
there now. Kept small on purpose: the web app is for quick tests and
settings.
2026-09-27 20:53:38 +02:00
Niko1221 9ffd6c9461 engine: fix a race in the CPU expert pool, and never hang silently again (issue #29, engine 0.1.12)
On a card with most experts in VRAM (RTX 4090) generation could stop for
good after thousands of tokens: the GPU "100%" at low power, one CPU core
busy, no tokens, no error.

- The pool's workers sleep after 20 ms without work (issue #4). With most
  experts resident many layers have no CPU work, so they also slept
  mid-request, and a worker that woke late was still counted as parked: it
  could claim from the next batch while the host was writing it, run a job
  twice, or add to `done` after the reset - and `done != n` then never
  ended while the GPU waited on the pool. Every claim now carries its
  batch: `head` is one 64-bit word (epoch | njobs | index), a claim is a
  CAS that only succeeds for the epoch the worker woke for, and the host
  waits with `<` instead of `!=`. Same speed (pool_test 47.2 GB/s).
- Every pool wait is bounded (60 s: a message and the engine stops), and
  --serve runs a watchdog: a request with no progress (tokens, prompt
  chunks, verify windows) for STRATA_WATCHDOG_S (120) seconds ends the
  engine with where it was stuck (strata/core/progress.hpp). The server
  (issue #27) answers with an error, names the cause from the log, and
  starts the engine again on the next request.
- pool_stress: tiny batches with sleeping workers; STRATA_POOL_SPIN_US
  shortens the spin to make them sleep constantly.

Tested: pool_test, pool_stress; 25,000-token generation at 262K with KV
streaming and constantly sleeping workers (73.9 tok/s, unchanged); the
watchdog deliberately tripped (2 s): error, log line, restart, next answer.
2026-09-27 20:53:38 +02:00
pete 898d6a4c3e Merge remote-tracking branch 'origin/main'
# Conflicts:
#	serve/server.py
2026-09-27 17:10:41 +01:00
Niko1221 1e3bded2f7 web app: "Use for other apps too" - the Chat settings as every client's defaults
A switch in the Sampling drawer. With it on, Apply stores the drawer's
settings on the server (POST /settings): thinking level, temperature,
top-p, top-k, seed, max tokens and the experimental speed projection. omp
and other API clients then get them for anything they don't send
themselves, from their next request (no restart); what a client sends still
wins. Off again, clients use their own settings.

- Kept between starts in <config>.shared-settings.json next to the run
  config; the server window says what other apps use.
- Only Strata's own page can change them: JSON only (a cross-site form or
  simple request can't send it without a CORS preflight, which this server
  never grants), no foreign Origin, and the API key when one is set.
- Values are checked (bad ones are a 400 naming the setting).
- Tests: defaults reach the engine, a client's own values win, off clears
  them and the file, 415/403/400/401.
2026-09-27 17:50:38 +02:00
Niko1221 ae3dc218ea web app: a long switch label no longer squeezes the switch
The Sampling drawer's "Experimental speed projection" row has a longer label
than "Show thinking", and the flex row shrank its switch, clipping the knob
at the drawer's edge. Switches in toggle rows keep their size.
2026-09-27 17:45:18 +02:00
Niko 3cba59ea31 Update README.md 2026-09-27 17:48:41 +02:00
Niko ecba7dfc10 Update README.md 2026-09-27 17:46:12 +02:00
Niko1221 1314ebfc7f README: a video up top, speed and model choice first, simpler troubleshooting (engine 0.1.10)
- An animated preview of the pagoda garden demo under the title (the full
  video is a release download: GitHub does not play videos from a
  repository), and a screenshot of the app in "Using it".
- "Is my PC enough?" folded into Install; "How fast is it?" and "Which model
  should I pick?" come first. The model table shows shard 1 (what gets
  loaded) instead of the download, with a plain fit rule: RAM >= shard 1 +
  ~10 GB, since the experts live in RAM.
- "Something went wrong?" as short questions and answers, including the
  first-start freeze and "the engine stopped unexpectedly".
- DETAILS troubleshooting: the same two rows, and the VRAM stall row now
  points to engine 0.1.9.
- setup requires engine 0.1.10 (INFO arena_mib, the start-up RAM warning).
2026-09-27 17:30:09 +02:00
Niko1221 7664149081 serve: an engine that dies mid-answer is an error, and the next request restarts it (issue #27)
On Linux with too little RAM for the model's experts (IQ3_XXS on 47 GB), the
out-of-memory killer ended the engine mid-answer. The server then recorded
the reply as "length" ("Max tokens" in the Monitor, no tok/s), the stream
was cut with a traceback, and every later request failed on the dead pipe.

- The engine's death is EngineDied: the reply ends with a clear error event
  (OpenAI and Anthropic streams; 503 before the answer), the Monitor says
  Error, and the server window names the likely cause (RAM; dmesg).
- The next request starts the engine again (same command) instead of
  failing forever; the engine counts as gone as soon as its output closes,
  and a failed write to it is not mistaken for the client leaving.
- The engine's INFO line reports its expert arena (arena_mib); at start the
  server warns when less than ~6 GB of RAM is left beside it.
- Test: a fake engine dies mid-stream -> error event + [DONE], Monitor
  "error", the next request restarts it and answers. End to end with Q2_0:
  strata.exe killed mid-answer -> error (exit code 1); next request restarts
  it (31 s) and answers.
2026-09-27 17:23:55 +02:00
Niko1221 6abee7f397 Experimental speed projection: an optional control vector (EXPERIMENTAL, off by default)
The engine takes llama.cpp's control-vector flags (--control-vector-scaled
FILE:SCALE, --control-vector-layer-range A B, --cvec-mode add|project,
--cvec-dir per-layer|single:L) and applies the vector after each steered
layer's FFN write on every hyper-connection stream: project h -= s (h.v) v,
or add h += d - where the experimental-speed-projection package's patched
llama.cpp applies it (build_cvec after the layer's second combine).

- strata/kernels/cvec: one kernel per (stream, token); with a pending fused
  write it applies it first with the fused read's own arithmetic, so a
  loaded-but-off vector is bitwise the stock engine. cvec_parity tests it.
- Per request: cvec=0|1 on the GEN/GENI line, the API field
  "experimental_speed_projection", a switch in the web app's Sampling
  drawer; a switch drops the conversation cache once. About and the
  Monitor say which way each request ran.
- setup asks (default off; --experimental-speed-projection on|off|GGUF),
  original Qwen3.8-Flash-Next only. The vector ships in
  data/experimental-speed-projection with a README that says what it is:
  its package describes it as a refusal-direction projection.

Measured (bench/results/2026-09-27-esp): off = stock token for token; on
costs 0.2-0.4% per token; mean KL 0.063 on 2,557 teacher-forced tokens.
2026-09-27 16:47:45 +02:00
Niko1221 868dc724d6 serve: reachable from other devices on the network (issue #26)
--host 0.0.0.0 bound fine, but nothing in the normal flow could set it: the
generated run scripts and setup's start always used the default
127.0.0.1, the config had no host, and the server then printed
http://0.0.0.0:8080, which opens nothing on another device.

- "host" in strata-<model>.json (the run scripts need no change); setup
  takes --host and --api-key and writes them there.
- With a non-local host the server prints this PC's addresses (the default
  route's first), warns when there is no API key, and on Windows says how to
  let it through the firewall. The local lines name 127.0.0.1.
- Documented in DETAILS.
2026-09-27 16:42:27 +02:00
Niko1221 f1b35531c3 engine: size the expert cache around the output head, so VRAM stays free (engine 0.1.9)
The expert cache takes what is free minus --vram-reserve-mib (700), so
everything allocated after it comes out of that reserve. The native output
head (0.44 GB Q2_0, 0.52 GB IQ3_S) was loaded after it and ate most of it:
IQ3_S at 128K ended with 30 MiB free, the driver paged GPU memory, and a
request stalled for good at its first verify window (GPU "100%" at 45 W).
Q2_0 at 32K ended with 75 MiB.

The head now loads before the cache is sized. Same reserve, now actually
free: 480-626 MiB with everything loaded (Q2_0 32K, IQ3_S 128K + vision).
The cache is ~8% smaller (Q2_0 32K: 3,848 slots vs 4,167) and decode is not
slower: 78-79 tok/s median vs 71.7 for 0.1.8 on the same greedy prompts.

Below 256 MiB the startup line now says which --vram-reserve-mib to add.
2026-09-27 16:23:07 +02:00
Niko1221 20351c6030 serve: --fit-max-tokens follow-ups (PR #24)
- A prompt that leaves no room at all is still the clear 400 with the flag
  on (it clamped to 1 token, which the engine then refused as over the
  context).
- "fit_max_tokens": true in the run config switches it on too, since the
  run scripts are generated by setup.
- Tests: an overshoot is clamped to the room on both APIs, a budget that
  fits is unchanged, no room is a 400. Documented in DETAILS.
2026-09-27 16:12:38 +02:00
Niko1221 bbba5bf576 Merge PR #24: optional --fit-max-tokens (clamp the output cap to the context instead of 400) 2026-09-27 16:11:54 +02:00
pete ba8c55e1cc serve: optional --fit-max-tokens clamps the output cap to remaining context instead of rejecting
Agentic clients legitimately request the model's full output cap with a long
prompt (e.g. prompt 131,888 + max_tokens 130,298 on a 262,144 window: rejected
over a 42-token overshoot). Default behaviour is unchanged: without the flag
the server still rejects with 400, like llama.cpp. With --fit-max-tokens the
server clamps max_tokens to the remaining room (max_context - CTX_SLACK -
prompt, floor 1 token) and answers.

Measured on a 262,144-token host: prompt 251,455 + max_tokens 131,072 ->
HTTP 200, completion_tokens 10,681 (= 262,144 - 251,455 - 8), finish length.
Flag off: byte-identical 400. Small requests unaffected in both modes.
2026-09-27 13:24:23 +01:00
pete b78d9da751 Merge remote-tracking branch 'origin/main' 2026-09-27 13:24:23 +01:00
pete 5f563ce3a3 serve: optional --fit-max-tokens clamps the output cap to remaining context instead of rejecting
Agentic clients legitimately request the model's full output cap with a long
prompt (e.g. prompt 131,888 + max_tokens 130,298 on a 262,144 window: rejected
over a 42-token overshoot). Default behaviour is unchanged: without the flag
the server still rejects with 400, like llama.cpp. With --fit-max-tokens the
server clamps max_tokens to the remaining room (max_context - CTX_SLACK -
prompt, floor 1 token) and answers.

Measured on a 262,144-token host: prompt 251,455 + max_tokens 131,072 ->
HTTP 200, completion_tokens 10,681 (= 262,144 - 251,455 - 8), finish length.
Flag off: byte-identical 400. Small requests unaffected in both modes.
2026-09-27 13:11:09 +01:00
Niko1221 6c431c60cc Record PR #21's rebased head as merged (its change is already in via 7f21e2b) 2026-09-27 12:14:51 +02:00
Niko1221 822a5c783f Web app: ship the Outfit font (OFL); the theme follows the system until picked; engine 0.1.8
- serve/web/fonts: Outfit as one variable font (latin + latin-ext, weights
  100-900) with its OFL.txt, in place of the design kit's commercial font.
- The page no longer saves the system's theme on first load (which froze
  it); only a click on the theme button or toggle is remembered, and the
  page follows system changes until then.
- setup: requires engine 0.1.8 (--kv q4_0, the INFO line).
2026-09-27 12:13:45 +02:00
Niko1221 0d721cf87b Web app: Chat, Monitor and About tabs (#22)
The browser page at http://127.0.0.1:8080 is now the Strata app, in the
Strata design language (serve/web, same origin, no CDN):
- Chat: streaming answers, collapsible thinking, Markdown with code copy,
  images when enabled, sampling/thinking settings, export, new chat.
- Monitor: live model state (reading with progress / generating at tok/s),
  GPU load, VRAM, temperature, power, PCIe traffic, CPU, RAM, disk, the
  context fill and the last requests, each with a minute of history.
- About: the model and engine settings, this PC, the API addresses.

Server: GET /metrics (API key when set), /web/* app files, /fonts/* only
when installed locally (not shipped; the page falls back to the system
font). serve/telemetry.py reads NVML via ctypes and psutil (new setup
dependency) with fallbacks. The engine prints an INFO line before READY
(kv, expert slots, speculation); older servers ignore it and the server
works with older engines.

Based on the dashboard idea in PR #22 by code-martin.
2026-09-27 12:00:33 +02:00
Niko1221andcode-martin 40542e8bb3 Merge PR #21: Q4_0 KV with Hadamard rotation (--kv q4_0), integrated with KV streaming; accurate expert-cache note (#23)
PR #21 (code-martin): a 4-bit K/V format (orthonormal FWHT-256 rotation of K, V and Q, then q4_0 blocks). On merge:
- Q4 is a full third format of the KV-streaming code (host copy, block residency, prompt-path staging, MTP ring,
  STATE_HASH): with --kv-resident the PR wrote K/V through the residency map's -1 entries. kv_stream_parity now
  checks Q4 streamed vs resident, bitwise.
- The per-token path ran attention twice (once before the blocks were resident); once now.
- The q4 group maximum breaks |x| ties the same way in every lane (one scale per block).
- Dropped: the duplicate path fixes (in 0.1.7), personal .gitignore entries, the re-encoded CMakeLists.
- Measured (bench/results/2026-09-27-kv-q4): int8 matches fp16; q4_0 costs precision on long documents
  (perplexity +8-12%), needles 5/5, +3.7% decode at 128K. Opt-in: setup asks above 8K context (int8 recommended),
  START-HERE.bat --kv q4_0, or --kv q4_0 in the config.

#23: the start-up warning "the GPU hit path is NOT CORRECT" dated from a long-fixed fault. Measured teacher-forced
on 2,557 tokens (bench/results/2026-09-27-cache-parity): cache on vs off, 95-98% same top-1 and equal perplexity
(-0.005 +- 0.005 nats); the warning is now one accurate line.

Co-Authored-By: code-martin <[email protected]>
2026-09-27 11:36:28 +02:00
code-martin e8b75ff2f6 feat(kv): add Q4_0 KV cache mode with FWHT-256 Walsh-Hadamard rotation
- Implement Q4_0 KV quantization and dequantization kernels with scale/offset
- Implement fast Walsh-Hadamard transform (FWHT-256) rotation for outlier suppression
- Add Q4_0 attention decode support in qsa_decode_attn
- Add kv_q4_parity parity test verifying CPU vs CUDA numerical accuracy
- Support --kv q4_0 in generate CLI and runtime layer allocation
- Update .gitignore for build and repository hygiene
2026-09-27 11:48:16 +03:00
code-martin 7f21e2b401 feat(kv): add Q4_0 KV cache mode with FWHT-256 Walsh-Hadamard rotation
- Implement Q4_0 KV quantization and dequantization kernels with scale/offset
- Implement fast Walsh-Hadamard transform (FWHT-256) rotation for outlier suppression
- Add Q4_0 attention decode support in qsa_decode_attn
- Add kv_q4_parity parity test verifying CPU vs CUDA numerical accuracy
- Support --kv q4_0 in generate CLI and runtime layer allocation
- Update .gitignore for build and repository hygiene
2026-09-27 11:29:08 +03:00
Niko1221 bbe8944ed3 Prompt lookup that pays, sampling fixes, shallow-path setup fix (engine 0.1.7)
- Prompt lookup on by default: the suffix drafter's windows are taken only where a learned policy (measured
  acceptance per match length, measured window cost) expects more tokens per ms than the MTP window; lookup windows
  up to --spec + 2. Code edits +6-11%, other text unchanged, verified output identical. --suffix-draft 0 = off.
- Per-request sampling (#19) keeps greedy requests on the in-graph sampler (no extra launch per window), and draws
  row t of a window with Philox(seed, pos0 + t), so a seed replays the same text whatever the drafts were.
- tools: a shallow install (G:\Strata) no longer crashes the MTP packer and the pack tools (IndexError, #20).
- setup: says at step 1 when the PC has less RAM than the smallest model needs; requires engine 0.1.7.
2026-09-27 10:18:14 +02:00
Niko1221 54adac29a4 Merge branch 'pr-19' into integrate-017 2026-09-27 09:58:30 +02:00
Niko1221 e5eb7b2ae7 Merge branch 'pr-18' into integrate-017 2026-09-27 09:58:30 +02:00
Niko1221 8f279d82a0 KV streaming: the draft layer keeps its KV in VRAM when no pinned RAM is left (engine 0.1.6)
The MTP drafter's ring over a host copy is set up after the expert arena has pinned what it could; when that
pinned allocation fails the drafter now falls back to its fully resident KV instead of stopping the engine
("mtp: state init failed", seen with Swift 1.5 at 262K). setup requires engine 0.1.6.
2026-09-27 08:34:09 +02:00
Niko1221 0616dfb74e KV streaming for the QSA layers (engine 0.1.5)
From 64K context the KV cache lives in pinned RAM and a VRAM block cache holds the part the sparse attention reads
(--kv-resident N; setup turns it on with 32768 when RAM allows). Blocks the indexer selects are made resident on
the device inside the captured graphs (clock eviction, zero-copy reads of misses); writers write through, so the
attention reads exactly the same values. The freed VRAM goes to the expert cache: Q2_0 at 262K decodes 62.6 tok/s
instead of 50.9. The MTP drafter keeps its window in a ring. New ctest kv_stream_parity. Also wires the opt-in
suffix drafter (--suffix-draft) into the engine.
2026-09-27 08:29:03 +02:00
Jakub Luwierski b819fe6934 sampler: the penalty count as a shared bitmap
The penalties made the sampled path pay O(k x n_vocab x hlen) integer
compares per token: every top_k argmax round recounted every
candidate's history occurrences (k=20 rounds, hlen=64, ~318M compares
on the 248,320 vocabulary) - measured 45.2 -> 30.8 tok/s on a real
chat workload.  But the history touches at most hlen DISTINCT tokens,
so membership is a bit: each block now builds a shared bitmap from its
history row (atomicOr, one pass) and a candidate pays the O(hlen)
count scan only when its bit is set - at most hlen x k scans per token
instead of k x n_vocab.  Neutral values and the no-history path skip
the bitmap entirely.

The counts - and therefore every penalised value, kept token and
draw - are exactly what the per-candidate scan produced;
sampler_parity passes unchanged, plus a new fixture pinning the
window clamp (an 8-entry history at penalty_last_n=4 must punish only
the tail half).
2026-09-27 08:24:09 +02:00
Jakub Luwierski e3e1493df4 serve: the sampling block supports min_p and the penalties
min_p, presence_penalty, repetition_penalty and frequency_penalty move
from "warned and ignored" to the sampled path, and penalty_last_n sets
the window they count over - the server sends the engine's default of
64 when a penalty is set and the client left the window out.
sampling_keys forwards each field only when it is not neutral, so a
client (or a config) that leaves them out costs nothing.  The
validator range-checks the new fields and still refuses to start the
server on a value it cannot honour.

Verified: the validator accepts and range-checks min_p/presence/
frequency/repetition/penalty_last_n; the GEN line spells them the
engine's way and omits neutral values; the last-64 default appears
only when a penalty is actually set.
2026-09-27 07:55:19 +02:00
Jakub Luwierski 4703da4363 generate: min_p and the penalties ride the GEN line
The sampled chain's last two gaps close: `min_p` and the llama.cpp
penalty set (repeat/freq/present over a history window) are live end
to end - GEN keys, per-request params, and the token history the
penalties count over.

- GEN accepts min_p=F penalty_last_n=N penalty_repeat=F penalty_freq=F
  penalty_present=F between max_new and the ids (the server's
  spelling); the engine clamps defensively (min_p to 0..1, a negative
  last_n to 0) and skips what it does not know, as before.
- min_p cuts the top_k survivors to the prefix with p >= min_p * p_max
  - in logits, sel_logit >= sel_logit[0] + logf(min_p) - before top_p,
  so the two cuts compose; 0 disables, and the head always survives,
  so the kept count never reaches zero.
- the penalties need the token history: the serve loop uploads the
  last penalty_last_n tokens - the consumed tail plus the fed-back
  head, which joins consumed_seq only after the window commits - into
  one device row before every window.  A request without penalties
  hands the sampler a null row and takes the byte-for-byte neutral
  path (no upload, nothing handed over).

Verified: sampler_parity grows a full-chain host reference (the Philox
draw transcribed host-side) and fixtures for the sampled chain with
penalties and for min_p at 0/0.5/0.9 - the kernel matches the
reference on all of them, and each fixture first asserts it can SEE
the feature before asserting the kernel matches.  On the IQ3_XXS
artifact, same seed: a request with neutral penalty keys is
token-identical to one without them; a six-copy prompt that loops
24/24 on one token clean produces 24 distinct tokens with repeat 1.5
+ present 0.5; min_p 0.9 changes the pick.
2026-09-27 07:55:19 +02:00
Jakub Luwierski 9e6e5b7a1a serve: sampling defaults from the run config
The run config's optional `sampling` block (temperature, top_p, top_k,
seed) sets defaults for the fields a request leaves out, so a client
that sends no sampling keys gets configured sampling instead of greedy.
The request's own fields always win: an explicit temperature=0 still
means greedy, and a field set to null falls back to the default.

Keys the sampled path does not implement yet (min_p, presence_penalty,
repetition_penalty, frequency_penalty) are named at startup and ignored
rather than silently dropped; unknown keys are reported the same way.
An invalid value (top_k outside 1..64, a non-positive seed, a negative
temperature) refuses to start the server - a typo'd config should not
quietly change sampling.

Verified with a spy engine through Service.run: defaults apply when the
request omits the fields, the request's own values win, a null field
falls back to the default, and a server without the block forwards the
request unchanged; the config validator rejects out-of-range values and
warns on the not-yet-implemented keys.
2026-09-27 07:50:10 +02:00
Jakub Luwierski 2e9acd3d8b docs: the v1 limits line still said temperature was ignored
It contradicted the paragraph next to it, and since the sampling keys
now ride the GEN line (and the block-parallel sampler makes a sampled
window cost about what a greedy one does), it was simply false.
2026-09-27 07:33:52 +02:00
Jakub Luwierski b7d73e1b32 serve: forward temperature/top_p/top_k/seed to the engine
StrataEngine.generate accepted the request's sampling parameters and
dropped them, so every served request decoded greedy whatever the client
asked - the README's 'temperature / top_p / top_k / seed are honored per
request' was not true over the HTTP API, and the engine-side per-request
sampling was unreachable from it.

The keys ride the GEN line in the engine's own spelling; an absent
temperature keeps the engine's greedy default and temperature=0 is not
forwarded (it means the same thing); top_k outside the sampled path's
1..64 is dropped rather than sending a value the engine would refuse.

Verified end to end over the HTTP server (static residency for the
seeded leg): temperature=0 and absent agree token for token; a
temperature=0.8 request differs from greedy; two temperature=0.8
seed=99 requests return identical text.
2026-09-27 07:33:17 +02:00
Jakub Luwierski 8f3c21dc02 perf: sample the verify window's head with a block per token
The sampled chain ran in ONE thread per token (64-thread launch): top_k
alone was k sequential full-vocabulary scans with an inner sweep over the
taken list, and a verify window measured ~1.6 s in the sampler - sampled
requests decoded at 1.5 tok/s against 54 greedy.

The selection is k argmax rounds, so it parallelises exactly like the
greedy kernel: one block per token, the rounds back to back inside it,
each a two-level (warp, then block) reduction.  Ties resolve to the
lowest index (the serial scan's strict '>'), so the kept sequence - set
and order - is unchanged, and the top_p cut, temperature and Philox draw
read that order as before.  sampler_parity passes; a 248,320-vocab
GPU-vs-CPU draw check matches the serial reference on five parameter
sets including injected duplicate logits.

Measured (RTX 4070 TS, IQ3_XXS, 128-token generations): sampled decode
1.50 -> 42.7 tok/s; greedy decode unchanged (53.3 vs 54.3 tok/s median,
within run noise; greedy launches the untouched kernel).
2026-09-27 07:33:17 +02:00
Jakub Luwierski 90546d7428 docs: temperature sampling + reproducibility note (static residency) 2026-09-27 07:33:17 +02:00
Jakub Luwierski 7546c36d9d verify: a persistent per-request draw counter; sampled requests default top_k=20
The sampled path requires top_k in 1..64 (the selection loop is
O(k*n_vocab) by design); with the engine-default top_k=0 it returned
without writing the token, leaking stale buffer contents as output.
Sampled requests now default to the sampler's own top_k=20.

The Philox draw counter also must persist across a request's verify
windows - it was read from a run-local variable that reset every
window, so every window redrew the same numbers (identical sampled
output on identical requests).
2026-09-27 07:31:54 +02:00
Jakub Luwierski dea4f4f2b5 serve: per-request temperature sampling for the verify window's head
Temperature was ignored because the verify window's head sampling was
pinned to greedy INSIDE the captured CUDA graph (verify.cpp hardcoded
sp.greedy = true), and the GEN protocol carried no sampling keys.

- the sampling moves OUT of the captured graph: the graph computes the
  head logits, and run() applies sample_tokens host-side after the
  replay, with that request's own parameters and a fresh Philox counter
  per window (a captured sampler would replay the same draws forever)
- the GEN protocol accepts optional key=value pairs before the ids:
  temperature=, top_p=, top_k=, seed=; absent keys stay greedy, exactly
  as before
- server.py forwards the request's temperature/top_p/top_k/seed
- emitted tokens are always the TARGET's own samples (the drafts only
  fill the verified window), so the output distribution is the target's
  regardless of what the drafts propose

Building on the conversation cache: greedy requests stay bit-identical,
sampled requests with the same seed replay identically.
2026-09-27 07:29:40 +02:00
btechnet 656a835682 serve: treat unset/-1 max_tokens as remaining context, not 1024
A missing, null, 0 or -1 max_tokens / max_completion_tokens now means the
rest of the context (minus the 8-token margin strata --serve keeps), so
replies no longer stop at 1024 tokens. prepare() resolves the budget after
tokenizing and returns it; an explicit budget is honoured exactly and still
rejected when prompt + budget exceeds the context. Adds serve/test_server.py.
2026-09-26 20:27:30 -05:00
Niko1221 55e2fb8576 setup: require engine v0.1.4 (IQ3_S) 2026-09-26 23:57:44 +02:00
Niko1221 59b0696208 IQ3_S support: the GSQ-RCO IQ3_S model (engine 0.1.4)
- GPU experts: IQ4_XS gate/up (the IQ3_S file keeps layer 47's routed experts in it), transcribed from llama.cpp's
  vec_dot_iq4_xs_q8_1; checked on all 48 layers against ggml's float reference (native_expert_parity: 0 failures).
- MTP draft head: gather_rows takes rows of any size (the IQ3_S head is Q6_K, 2100-byte rows, not a multiple of 16).
- setup: IQ3_S for the original model (Swift 1.5 has none), 64 GB RAM, context capped at 128K like IQ3_XXS.
- README / DETAILS: IQ3_S speed (RTX 5070, Ryzen 5 7600, 64 GB): 54 tok/s at 4K, 42 at 128K, 374-397 prompt.
2026-09-26 23:55:41 +02:00
Niko1221 0223d32663 setup: update an old ready-made engine when an existing install starts
START-HERE.bat on an installed setup went straight to starting the model, so the MIN_ENGINE check (and the
engine download) only ran when setup was run again with its options.  Now every start checks the installed
engine and replaces it when it is older than MIN_ENGINE.  If it cannot (no internet, the model still running,
no ready-made engine for this GPU) the installed engine is kept and starts as before.
2026-09-26 22:24:29 +02:00
Niko1221 d0017a7c58 setup: require engine v0.1.3 (conversation cache, idle CPU fix, Linux pool fix) 2026-09-26 22:18:20 +02:00
Niko1221 0c2e63e930 Merge pull request #10: Conversation cache + short prompt parts through the decode windows (#8, #10) 2026-09-26 21:25:03 +02:00
Niko1221 888751a0e1 Merge pull request #7: serve: fix zero-token replies from the engine-queue race between requests (#7) 2026-09-26 21:25:03 +02:00
Niko1221 af1c91f245 Merge pull request #9: CPU pool: sleep between requests instead of spinning (#9, fixes #4) 2026-09-26 21:25:03 +02:00
Niko1221 14b22c51c1 Merge pull request #14: CPU pool: fix dangling else in Linux physical_cores() (#14, fixes #13 and #11) 2026-09-26 21:25:03 +02:00
pete ad29df2396 Merge branch 'pr-14' 2026-09-26 19:20:59 +01:00
Vistawizard be028948f4 CPU pool: fix dangling else in the Linux physical_cores() branch
Without braces the `else` bound to `if (CPU_ISSET(i, &set))`, so every CPU id
outside the affinity mask appended all hardware_concurrency() ids again:
16143 pool workers on a 16-CPU box, load > 10000 and a hang at start.
Brace both branches so the fallback only runs when sched_getaffinity fails.

Fixes #13, root cause of #11.
2026-09-26 19:23:15 +02:00
pete 8936605cc5 Merge branch 'pr-8' 2026-09-26 17:39:42 +01:00
pete 3503b5ddd8 Merge branch 'pr-7' 2026-09-26 17:39:42 +01:00
Mirtraxxx 470b3d7c21 serve: read short prompt parts through the decode windows
The batched prompt path costs ~300 ms per run however few tokens it
has (it streams every non-resident expert the chunk routes to over
PCIe), plus ~180 ms to refill the expert-cache slots it borrows. So a
one-word message waited ~1.4 s for its first token, and every chat
follow-up paid the same fixed cost.

Each part of the prompt (up to the checkpoint, then the assistant
header) now picks its own path: parts of at most --short-read N tokens
(default 64, 0 = off) go through the verify windows, S tokens at a
time, as decode reads them (~16 ms a token). Longer parts and parts
with picture rows stay batched. Slots are lent just before a batched
run and refilled before a window read, so windows always see the whole
expert cache. STRATA_CKPT_REREAD keeps every read batched.
2026-09-26 01:02:20 -03:00
Mirtraxxx be0e0c94e3 cpu pool: sleep between requests instead of spinning (fixes #4)
A parked expert-pool worker spun on `epoch_` with `_mm_pause` forever, so every
worker's core stayed at 100% while the engine waited for a request: 7 cores
(44% of a 16-thread Ryzen 7 5700X) with nothing to do.

A parked worker now spins as before for up to 20 ms, then blocks on a
condition variable. Every publish goes through `publish()`, which bumps the
epoch and notifies only when a worker is actually asleep (one uncontended
load otherwise). Inside a request the gaps between batches are microseconds,
so the token path never sleeps.
2026-09-25 21:27:53 -03:00
Mirtraxxx 42810ee8bb serve: conversation cache - keep the chat between requests, read only what is new
Every request used to start from session_zero and read its whole prompt again,
so a long chat or an agent loop waited minutes per turn. The engine now keeps
the live session and up to 6 checkpoints of the running state in RAM and reads
only the part of a prompt after the longest one it starts with.

- A checkpoint is the state that cannot be rewound - the 36 GDN recurrences and
  conv histories, the 12 QSA indexer tails, the PLE history (~118 MB). The KV
  cache and the pooled indexer keys are positional (a query reads only cells
  below it, and `dead` for its own block), so they stay in place.
- Taken at the last <|im_start|> of each prompt (the prompt is read in two parts
  there: the history, then the new turn's header) and every 16K fresh tokens.
- Used only when the prompt starts with exactly its tokens and pictures (images
  are compared by a hash of their embeddings and grid); checkpoints the next
  request would overwrite are dropped.
- New lines on stdout: RESUME <n> before reading, PP <pos> <n> <ms> <tok/s> per
  chunk, REUSED <n> after; DONE gains <drafts accepted> <drafts offered>
  <reused>. The server ignores what it does not know; PP lines now count as its
  heartbeat, so keep-alives keep flowing during a long prompt.
- --prompt-cache N (default 6, 0 = off), --prompt-cache-every N, --turn-token ID.
- Checks: STRATA_CKPT_REREAD=1 reads a checkpoint's tokens again instead of
  restoring it; STRATA_STATE_HASH=1 prints a fingerprint of every state part
  after each request.
2026-09-25 21:18:23 -03:00
btechnet 0f8b5d4938 serve: fix zero-token replies from engine-queue race between requests
Symptom: with two requests in flight (opencode fires a title-generation
request alongside the main tool-calling turn), the second request often
returned zero tokens. The server logged `done: 0 tokens in 0 s (length,
cancel=False)` and opencode showed an empty reply.

Cause: StrataEngine multiplexes every request over one resident
`strata --serve` subprocess and a single shared line queue (self.lines),
with no per-request framing. In Service.run(), a stop token (<|im_end|>)
breaks the token loop with the engine generator still suspended and its
internal `done` flag False. That generator's cleanup - write STOP, then
drain self.lines until DONE - therefore ran only when run()'s frame was
torn down later, AFTER the `with self.fifo` lock had been released. By
then the next request held the lock and had issued its own GEN; the prior
request's drain raced it on the shared queue and consumed the NEW
request's DONE. The new request saw an immediate end-of-stream: 0 tokens,
with `finish` left at its default of "length".

Fix: close the engine generator inside the `with self.fifo` block via
try/finally, so a request's STOP+drain completes while it still holds the
lock and cannot leave stray lines on the shared queue for the next
request to misread as its own DONE.

Also in this change:
- Clamp a non-positive max_new to 1024 in both /v1/chat/completions and
  /v1/messages. Some clients send max_tokens: -1 ("unlimited"); that value
  passed straight through and `GEN -1` made the engine yield 0 tokens.
- Distinguish a client disconnect: catch GeneratorExit and set
  finish="disconnect" instead of leaving it as "length".
- Diagnostics gated on STRATA_DEBUG (off by default): a per-request
  `_debug_req` line (api, msg/tool counts, raw max_tokens, resolved
  max_new, thinking flag, prompt tokens) and a `raw:` dump of the decoded
  model output at completion.
- Progress reporting: add tok/s and cancel state to the `done:` line, and
  emit the progress line every 1 s instead of every 15 s for live
  throughput.
2026-09-25 18:11:01 -05:00
161 changed files with 20606 additions and 1046 deletions

No files matched your search

+1
View File
@@ -46,3 +46,4 @@ build-vision.bat
/dist/
*.done
/third_party/*.zip
+74 -7
View File
@@ -8,7 +8,14 @@
# written against sm_120 (RTX 5000 / Blackwell) and silently building for an older arch would produce a
# binary that runs and is wrong about its own performance, which is the worst of both outcomes.
cmake_minimum_required(VERSION 3.24)
project(strata VERSION 0.1.0 LANGUAGES CXX)
project(strata VERSION 0.1.21 LANGUAGES CXX)
add_compile_definitions(STRATA_VERSION="${PROJECT_VERSION}")
# issue #31: release builds keep a PDB beside the exe (not shipped in it), so a minidump from a stalled engine can
# be read back to the exact source line
if(MSVC AND CMAKE_BUILD_TYPE STREQUAL "Release")
add_compile_options($<$<COMPILE_LANGUAGE:CXX>:/Zi>)
add_link_options(/DEBUG /OPT:REF /OPT:ICF)
endif()
# The prebuilt release (setup.py downloads it, so users need no compiler): ggml's CPU backend for any AVX2 CPU
# instead of this machine's (Strata's own AVX-512 kernels are per-file and chosen at run time either way), and
@@ -49,7 +56,7 @@ if(STRATA_ENABLE_CUDA)
if(_base STREQUAL "native" OR _base STREQUAL "all" OR _base STREQUAL "all-major")
continue()
endif()
if(_base LESS 80)
if(_base LESS 80 AND NOT STRATA_EXPERIMENTAL_SM75) # the A/B of a Turing card as a layer-split stage
message(FATAL_ERROR
"Strata needs an NVIDIA GPU of compute capability 8.0 or newer (RTX 30 / 40 / 50 series), but "
"CMAKE_CUDA_ARCHITECTURES is '${CMAKE_CUDA_ARCHITECTURES}'.")
@@ -177,7 +184,10 @@ if(STRATA_ENABLE_CUDA)
src/kernels/cuda/native_qsa_score.cu
src/kernels/cuda/ple.cu
src/kernels/cuda/kv_q8.cu
src/kernels/cuda/kv_stream.cu
src/kernels/cuda/kv_q4.cu
src/kernels/cuda/fused_gr.cu
src/kernels/cuda/cvec.cu
src/kernels/cuda/qsa_decode_attn.cu
src/kernels/cuda/fused_gdn.cu
src/kernels/cuda/dequant_bf16.cu
@@ -235,7 +245,7 @@ if(STRATA_ENABLE_CUDA)
# already links `strata_core` - putting `gdn_layer` in core would make the two mutually dependent. A third
# library is the honest shape of the dependency graph: core knows about devices and weights, kernels know
# about arithmetic, and this one knows the ORDER the arithmetic happens in.
add_library(strata_engine STATIC src/core/layer.cpp src/core/session.cpp src/core/expert_source.cpp
add_library(strata_engine STATIC src/core/layer.cpp src/core/session.cpp src/core/expert_source.cpp src/core/remote_experts.cpp
src/core/expert_cache.cpp src/core/native_head.cpp src/core/native_dense.cpp src/core/verify.cpp src/core/mtp.cpp)
target_include_directories(strata_engine PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_link_libraries(strata_engine PUBLIC strata_core strata_kernels strata_kernels_cpu CUDA::cudart)
@@ -250,7 +260,7 @@ if(STRATA_ENABLE_CUDA)
add_executable(strata src/program/generate.cpp)
target_include_directories(strata PRIVATE ${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES})
target_link_libraries(strata PRIVATE strata_engine strata_prefill CUDA::cudart)
target_link_libraries(strata PRIVATE strata_engine strata_prefill strata_spec CUDA::cudart)
add_executable(dequant_s2_parity src/kernels/dequant_s2_parity.cpp)
# The tests are HOST C++ files that call cudaMalloc/cudaMemcpy themselves, so they need the toolkit headers
@@ -356,6 +366,22 @@ if(STRATA_ENABLE_CUDA)
target_link_libraries(kv_q8_parity PRIVATE strata_kernels CUDA::cudart)
add_test(NAME kv_q8_parity COMMAND kv_q8_parity)
# KV streaming (docs/kv-streaming-design.md): streamed vs resident attention, bitwise (GPU, synthetic, no model).
add_executable(kv_stream_parity src/kernels/kv_stream_parity.cpp)
target_include_directories(kv_stream_parity PRIVATE ${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES})
target_link_libraries(kv_stream_parity PRIVATE strata_kernels CUDA::cudart)
add_test(NAME kv_stream_parity COMMAND kv_stream_parity)
# PR #21: Q4_0 KV with Walsh-Hadamard rotation vs a host reference (GPU, synthetic, no model).
add_executable(kv_q4_parity src/kernels/kv_q4_parity.cpp)
target_include_directories(kv_q4_parity PRIVATE ${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES})
target_link_libraries(kv_q4_parity PRIVATE strata_kernels CUDA::cudart)
add_test(NAME kv_q4_parity COMMAND kv_q4_parity)
add_executable(cvec_parity src/kernels/cvec_parity.cpp)
target_include_directories(cvec_parity PRIVATE ${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES})
target_link_libraries(cvec_parity PRIVATE strata_kernels CUDA::cudart)
add_test(NAME cvec_parity COMMAND cvec_parity)
# Plan v0.3 P3: native MMVQ for 1..8 tokens (bitwise selftest + --bench timing); synthetic data, no model.
if(STRATA_BUILD_TESTS)
add_executable(native_mmvq_multi bench/micro/native_mmvq_multi.cpp)
@@ -417,7 +443,7 @@ endif()
# worse opt-out. `cpu_require_expert_support()` therefore refuses at startup on a CPU without the
# features, and the fallback exists for tests, exactly as P2.S3 specifies.
# Plan v0.3 P6: speculation components that need no GPU.
add_library(strata_spec STATIC src/spec/suffix_drafter.cpp src/spec/controller.cpp)
add_library(strata_spec STATIC src/spec/suffix_drafter.cpp src/spec/controller.cpp src/spec/draft_policy.cpp)
target_include_directories(strata_spec PUBLIC include)
if(STRATA_BUILD_TESTS)
add_executable(suffix_drafter_test src/spec/suffix_drafter_test.cpp)
@@ -426,6 +452,13 @@ if(STRATA_BUILD_TESTS)
add_executable(controller_test src/spec/controller_test.cpp)
target_link_libraries(controller_test PRIVATE strata_spec)
add_test(NAME controller_test COMMAND controller_test)
add_executable(draft_policy_test src/spec/draft_policy_test.cpp)
target_link_libraries(draft_policy_test PRIVATE strata_spec)
add_test(NAME draft_policy_test COMMAND draft_policy_test)
# the serve loop's conversation-cache retention policy (header-only, no library to link)
add_executable(conv_cache_test src/program/conv_cache_test.cpp)
target_include_directories(conv_cache_test PRIVATE include)
add_test(NAME conv_cache_test COMMAND conv_cache_test)
endif()
add_library(strata_kernels_cpu STATIC src/kernels/cpu/expert.cpp src/kernels/cpu/pool.cpp
@@ -485,16 +518,43 @@ if(STRATA_NATIVE_EXPERTS)
endif()
# part of strata_kernels_cpu (the pool calls these and they call its CPU probe: one library, so GNU ld's
# one-pass static linking has no cycle to resolve)
target_sources(strata_kernels_cpu PRIVATE src/kernels/cpu/native_expert.cpp src/kernels/cpu/iq_avx512.cpp)
target_sources(strata_kernels_cpu PRIVATE src/kernels/cpu/native_expert.cpp src/kernels/cpu/iq_avx512.cpp
src/kernels/cpu/iq_avx2.cpp)
target_include_directories(strata_kernels_cpu PRIVATE third_party/ggml)
if(MSVC)
set_source_files_properties(src/kernels/cpu/iq_avx512.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX512")
set_source_files_properties(src/kernels/cpu/iq_avx2.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX2")
else()
set_source_files_properties(src/kernels/cpu/iq_avx512.cpp PROPERTIES COMPILE_OPTIONS
"-mavx512f;-mavx512bw;-mavx512vl;-mavx512dq;-mavx512vnni;-mavx512vbmi;-mf16c;-mfma")
set_source_files_properties(src/kernels/cpu/iq_avx2.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma;-mf16c")
endif()
target_link_libraries(strata_kernels_cpu PUBLIC ggml-cpu ggml-base)
target_compile_definitions(strata_kernels_cpu PUBLIC STRATA_NATIVE_EXPERTS=1)
if(STRATA_ENABLE_CUDA)
# prompt-speed plan step 2b: llama.cpp's MMQ kernels (ggml-cuda, MIT) for the prompt path's experts, compiled
# from the same checkout as ggml above - only mmq.cuh's instances for the packs' types and the quantizer, with
# the few host symbols of ggml-cuda.cu they use in src/prefill/ggml_cuda_host.cu
if(STRATA_GGML_DIR)
set(_strata_gg ${STRATA_GGML_DIR}/ggml)
else()
set(_strata_gg ${strata_llamacpp_SOURCE_DIR}/ggml)
endif()
set(_strata_mmq ${_strata_gg}/src/ggml-cuda)
add_library(strata_mmq STATIC src/prefill/moe_mmq.cu src/prefill/ggml_cuda_host.cu ${_strata_mmq}/quantize.cu)
foreach(_t q2_0 iq2_xxs iq2_xs iq2_s iq3_xxs iq3_s iq4_nl iq4_xs)
target_sources(strata_mmq PRIVATE ${_strata_mmq}/template-instances/mmq-instance-${_t}.cu)
endforeach()
target_include_directories(strata_mmq PRIVATE ${_strata_gg}/include ${_strata_gg}/src ${_strata_mmq}
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_compile_options(strata_mmq PRIVATE $<$<COMPILE_LANGUAGE:CUDA>:--extended-lambda -use_fast_math>)
if(MSVC)
target_compile_options(strata_mmq PRIVATE $<$<COMPILE_LANGUAGE:CUDA>:-Xcompiler=/bigobj>)
endif()
target_link_libraries(strata_mmq PUBLIC ggml-base CUDA::cudart)
target_link_libraries(strata_prefill PUBLIC strata_mmq)
target_compile_definitions(strata_prefill PUBLIC STRATA_PREFILL_MMQ=1)
endif()
if(STRATA_ENABLE_CUDA)
add_executable(native_expert_parity src/kernels/native_expert_parity.cpp)
target_include_directories(native_expert_parity PRIVATE ${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES})
@@ -528,9 +588,16 @@ if(STRATA_BUILD_TESTS)
add_executable(pool_test src/kernels/cpu/pool_test.cpp)
target_link_libraries(pool_test PRIVATE strata_kernels_cpu)
add_test(NAME pool_test COMMAND pool_test --selftest WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
# issue #29: tiny batches with sleeping workers, as on a big-VRAM card; a hang fails it
add_executable(pool_stress src/kernels/cpu/pool_stress.cpp)
target_link_libraries(pool_stress PRIVATE strata_kernels_cpu)
add_test(NAME pool_stress COMMAND pool_stress 20)
endif()
if(STRATA_BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
# the unit tests above build from src/; the tests/ tree (model-level checks) is not part of the published repo
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/CMakeLists.txt)
add_subdirectory(tests)
endif()
endif()
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Niko1221 and the Strata contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+145 -72
View File
@@ -3,131 +3,197 @@
<p align="center"><b>Run a 125-billion-parameter AI model on a normal gaming PC</b><br>
one NVIDIA card (12-24 GB) + 64 GB of RAM · Windows or Linux · one click to install</p>
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="A voxel pagoda garden that Strata's model wrote, running in the browser"></a><br>
<sub>A voxel pagoda garden, 1 shot prompt running on an RTX 5070 with Strata (IQ3_S, 128K context) ·
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">full video (49 s)</a></sub></p>
Strata runs **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** - a large, smart AI model that
normally needs a server - on your own PC. It writes its answers at **60-95 tokens per second** (a token is about ¾
of a word): faster than you can read.
- **Private:** everything runs on your PC. Nothing is sent anywhere.
- **Works with your apps:** chat apps, coding agents and scripts that speak the OpenAI or Anthropic API just work.
- **Sees pictures** too, if you want (screenshots, photos, scanned pages).
- **Free and open source.**
> **Jump to:** [Is my PC enough?](#is-my-pc-enough) · [Install](#install-3-steps) · [How fast?](#how-fast-is-it) ·
> [Which model?](#which-model-should-i-pick) · [Using it](#using-it) · [Problems?](#something-went-wrong) ·
> [How it works](#how-does-it-work) · [All the details](docs/DETAILS.md)
> **Jump to:** [How fast?](#how-fast-is-it) · [Which model?](#which-model-should-i-pick) · [Install](#install) ·
> [Using it](#using-it) · [Problems?](#something-went-wrong) · [How it works](#how-does-it-work) ·
> [All the details](docs/DETAILS.md)
---
## Is my PC enough?
| You need | |
| --- | --- |
| **Graphics card** | NVIDIA RTX 30, 40 or 50 series with **12 GB of VRAM or more** |
| **Memory (RAM)** | **64 GB** |
| **Free disk space** | ~80 GB (an SSD makes the first start much faster) |
| **System** | Windows 10/11, or Linux |
That's it. The only thing you install yourself is a current **NVIDIA driver**
([nvidia.com/drivers](https://www.nvidia.com/drivers) or the NVIDIA App). Everything else - Python, the engine, the
model - is set up for you.
## Install (3 steps)
**Windows**
1. [Download this project](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) and unzip it (or `git clone` it).
2. Double-click **`START-HERE.bat`**.
3. Answer 4 questions - or just press Enter each time for the recommended choice:
- **Which model?** The original, or Swift 1.5 (a version that thinks shorter and answers sooner)
- **Which size?** Q2_0, IQ2_XS or IQ3_XXS - see [which model](#which-model-should-i-pick)
- **How much context?** How much text it can keep in mind at once (it suggests one for your card)
- **Images?** Whether it should also read pictures
Then it downloads everything (the model is ~70 GB, so the first time takes a while - you can stop and it picks up
where it left off) and **starts the model**. Your browser opens a chat page at `http://127.0.0.1:8080`.
**Next time**, just double-click `START-HERE.bat` again: it starts right away, nothing is downloaded twice. Close its
window to stop the model.
**Linux:** run `./setup.sh` - same questions, same result.
## How fast is it?
Measured on an RTX 5070 (12 GB), a Ryzen 5 7600 and 64 GB of RAM:
| Size | Writes answers (short chat) | Writes answers (128K context) | Reads your prompt |
| --- | ---: | ---: | ---: |
| **Q2_0** | 95 tokens/s | 65 tokens/s | 539 tokens/s |
| **IQ2_XS** | 78 tokens/s | 52 tokens/s | 463 tokens/s |
| **IQ3_XXS** | 66 tokens/s | 45 tokens/s | 410 tokens/s |
| **Q2_0** | 90 tokens/s | 67 tokens/s | 1,310 tokens/s |
| **IQ2_XS** | 74 tokens/s | 60 tokens/s | 1,240 tokens/s |
| **IQ3_XXS** | 62 tokens/s | 46 tokens/s | 1,110 tokens/s |
| **IQ3_S** | 52 tokens/s | 41 tokens/s | 1,070 tokens/s |
| **Coder** (IQ1_M) | 51 tokens/s | 44 tokens/s | 1,300 tokens/s |
- **Writes answers** = how fast the reply appears (tokens per second).
- **Reads your prompt** = how fast it takes in what you send (long documents, code, chat history).
- **Reads your prompt** = how fast it takes in what you send (long documents, code, chat history), measured on a
32K-token prompt; a 4K prompt reads at 740-1,000 tokens/s. A 32K prompt takes about 25 seconds with Q2_0.
A card with more VRAM is faster, because more of the model fits on the GPU: an RTX 3090 (24 GB) should do roughly
100-140 tokens per second. All measurements, long-context numbers and estimates for other cards are in the
[details](docs/DETAILS.md#speed-measured).
Every PC is different: `START-HERE.bat --calibrate` measures a few engine settings on yours and keeps the fastest
(about 5-10 minutes; on the PC above it made the Coder 7% faster).
**Two or three NVIDIA cards?** `START-HERE.bat --setup --gpus 0,2` splits the model's layers across them (experimental):
each card keeps the experts of its own layers, and prompts flow through the cards in a pipeline. On an RTX 5080 +
RTX 3090 it read prompts 18-20% faster than the 5080 alone, with decoding on par. See [docs/MULTI_GPU.md](docs/MULTI_GPU.md).
## Which model should I pick?
**The size** (the same model, compressed more or less):
| Size | Download | Speed | Quality | Pick it if... |
| --- | ---: | --- | --- | --- |
| **Q2_0** | 66 GB | fastest | good | you want speed |
| **IQ2_XS** | 68 GB | fast | better | you want a good all-rounder (**recommended**) |
| **IQ3_XXS** | 76 GB | slower | best | you want the best answers (uses 43 GB of your 64 GB RAM) |
| Model | RAM+VRAM Requirements | Speed | Quality |
| --- | ---: | --- | --- |
| **Q2_0** | 37.6 GB | fastest | good |
| **IQ2_XS** | 39.2 GB | fast | better (**recommended**) |
| **IQ3_XXS** | 47.0 GB | slower | great |
| **IQ3_S** | 54.8 GB | slowest | best: matches the full model on the published tests (original model only) |
**Will it fit?** Shard 1 is the part of the model that gets loaded when it starts: its experts go into your **RAM**,
the rest onto your graphics card (the second shard, a 29 GB lookup table, stays on the SSD). So it fits when your
**RAM is at least shard 1 + about 10 GB** for Windows and your other programs. With 64 GB of RAM every size fits
(IQ3_S with little else open); with 48 GB, Q2_0 and IQ2_XS. A bigger graphics card makes it faster, but it doesn't
lower the RAM needed.
**The version:**
- **Qwen3.8-Flash-Next** - the original.
- **[Coder](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-Coder-GGUF)** - ISTA-DASLab's coding
version: half of the experts removed, keeping the ones that code, tool use and images need (91% of the full model's
SWE-bench Verified score, 99% of LiveCodeBench, by its authors). One size (IQ1_M: its experts stored like IQ3_S):
shard 1 is **29.6 GB**, so it fits a PC with **32 GB of RAM**, runs 262K context on 64 GB, and reads long prompts
the fastest of all. Weaker outside coding.
- **[Swift 1.5](https://huggingface.co/ukisai/Swift-1.5-Qwen3.8-Flash-Next-GSQ-RCO-GGUF)** - a fine-tune by UkisAI
that thinks much shorter before answering, so you get the answer sooner, with about the same quality. Same speed per
token. Its own license applies (see its page).
token, and about the same RAM as the same size of the original (no IQ3_S). Its own license applies (see its page).
Not sure? Take **IQ2_XS**. You can add another one later with `START-HERE.bat --setup`.
Not sure? Take **IQ2_XS** - or the **Coder** if you mainly write code, or have 32-48 GB of RAM. You can add another
one later with `SETUP.bat` (the same as `START-HERE.bat --setup`; on Linux `./setup.sh --setup`).
For **OrcaRouter's Flash-Next Uncensored IQ3_XXS**, see the [manual compatibility setup](docs/ORCA.md).
It needs an explicit packing conversion and is not an installer menu option.
## Install
**You need:** an NVIDIA RTX 30, 40 or 50 card with 12 GB of VRAM or more, enough RAM for the size you pick (above),
~80 GB of free disk space (an SSD makes the first start much faster), and Windows 10/11 or Linux. The only thing you
install yourself is a current **NVIDIA driver** ([nvidia.com/drivers](https://www.nvidia.com/drivers) or the NVIDIA
App). Everything else - Python, the engine, the model - is set up for you.
**Windows**
1. [Download this project](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) and unzip it (or `git clone` it).
2. Double-click **`START-HERE.bat`**.
3. Answer a few questions - or just press Enter each time for the recommended choice:
- **Which model and size?** The original or Swift 1.5, and Q2_0, IQ2_XS, IQ3_XXS or IQ3_S - see [above](#which-model-should-i-pick)
- **How much context?** How much text it can keep in mind at once (it suggests one for your card)
- **Images?** Whether it should also read pictures
- **Experimental speed projection?** Off unless you say yes - [read what it does](docs/DETAILS.md#experimental-speed-projection-experimental-off-by-default) first
Then it downloads everything (the model is ~70 GB, so the first time takes a while - you can stop and it picks up
where it left off) and **starts the model**. Your browser opens the Strata app at `http://127.0.0.1:8080`.
> **While the model starts, your PC can be slow or stop responding for 1-3 minutes** (longest the first time): Strata
> loads 35-55 GB into your RAM and locks part of it for the graphics card. That's normal - wait, and don't close the
> window. The window tells you what it is doing.
**Next time**, just double-click `START-HERE.bat` again: it starts right away, nothing is downloaded twice. Close its
window to stop the model.
**Updating:** download the new version and unzip it anywhere (or `git pull`), then run `START-HERE.bat` in it. The
model files are kept in a `Strata-data` folder next to your Strata folder, so a new copy finds them and sets itself up
the same way - nothing big is downloaded again.
**Linux:** run `./setup.sh` - same questions, same result.
## Using it
- **Chat in the browser:** `http://127.0.0.1:8080` - a simple chat page (it opens by itself when the model starts).
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="The Strata app's Monitor tab next to a coding agent"><br>
<sub>The Strata app's <b>Monitor</b> (left) while a coding agent writes the pagoda garden from the video (right)</sub></p>
- **In the browser:** `http://127.0.0.1:8080` - the Strata app (it opens by itself when the model starts): **Chat**, a
live **Monitor** of the model and your GPU/CPU/RAM, and **About** with the settings and addresses.
- **Chat in the terminal:** `.venv\Scripts\python chat.py`
- **Your apps and coding agents:** add it as an "OpenAI-compatible" provider with base URL
**`http://127.0.0.1:8080/v1`**, any API key and any model name. Apps that use Anthropic's API: `http://127.0.0.1:8080/v1/messages`.
- **Thinking:** the model thinks before it answers. Choose **off, low, medium or high** - in the chat page menu, with
`/think low` in `chat.py`, or with your app's "reasoning effort" setting. Off is fastest; high is best for hard questions.
- **Pictures:** in the chat page click **Picture**; in `chat.py` type `/image <path>`; in apps just attach them.
- **From your phone or another PC:** see the [details](docs/DETAILS.md#using-it) (set an API key first).
- **From your phone or another PC:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`, then open the
address the server window prints; see the [details](docs/DETAILS.md#using-it).
- **Experimental speed projection (off by default):** an experimental control vector that setup can turn on; it
changes how the model answers - read [what it does](docs/DETAILS.md#experimental-speed-projection-experimental-off-by-default) first.
**Good to know:** it answers one request at a time, and it re-reads the whole conversation for every answer. So in
very long chats you wait longer before it starts writing: about 1 minute per 30,000 tokens of conversation.
**Good to know:** it answers one request at a time. The first message of a chat is read in full (about 1 minute per
30,000 tokens); after that it keeps the conversation and reads only what is new, so follow-ups start in seconds.
## Something went wrong?
| What you see | What to do |
| --- | --- |
| `the NVIDIA driver is too old` | Update the driver (NVIDIA App or nvidia.com/drivers), restart the PC, run `START-HERE.bat` again. |
| It stopped during download or setup | Run `START-HERE.bat` again - it continues where it stopped. |
| `port 8080 is already in use` | Strata is already running - look for its window. |
| The first start takes minutes | Normal: it loads 35-43 GB into RAM. The next start is faster. |
| Slow, and the disk light is busy | Not enough free RAM: close other programs (browsers use a lot), or pick Q2_0 / IQ2_XS. |
| "prompt exceeds the context" | The conversation is longer than the context you chose: run `START-HERE.bat --setup` and pick more. |
**My PC froze, or got very slow, the first time Strata started.**
That's normal while it starts, most of all the first time. Strata loads 35-55 GB into your RAM, locks part of it for
the graphics card, and works out how much of the model fits on your GPU. The mouse can freeze for a few minutes. **Wait, and don't close the
window.** The next starts are much faster. Still frozen after 10 minutes? Restart the PC, close other programs
(browsers use a lot of RAM) and try again. If it keeps happening, pick a smaller size (Q2_0 or IQ2_XS).
More in the [full troubleshooting table](docs/DETAILS.md#troubleshooting). Still stuck? Open an issue and attach
`strata-<model>.log` from this folder.
**It stopped while downloading or installing.**
Run `START-HERE.bat` again. It continues where it stopped.
**It says the NVIDIA driver is too old.**
Update it (NVIDIA App or [nvidia.com/drivers](https://www.nvidia.com/drivers)), restart the PC, and run
`START-HERE.bat` again.
**It says port 8080 is already in use.**
Strata is already running. Look for its window.
**It's very slow and the disk light keeps blinking.**
Your PC is out of free RAM. Close other programs, or pick a smaller size (Q2_0 or IQ2_XS).
**An answer stopped with "the engine stopped unexpectedly".**
Usually not enough RAM (on Linux the system then stops the engine). Just send your message again: Strata starts the
engine by itself. If it keeps happening, close other programs or pick a smaller size.
**It says the prompt exceeds the context.**
The conversation is longer than the context you chose. Start a new chat, or run `SETUP.bat` and pick more
context.
**Still stuck?** Look in the [full troubleshooting table](docs/DETAILS.md#troubleshooting), or open an issue and
attach `strata-<model>.log` from the Strata folder.
## How does it work?
A model this big doesn't fit on a gaming graphics card. Strata splits the work between the parts of your PC:
Models like this one normally run on servers with hundreds of gigabytes of graphics memory. Your graphics card has
12-24 GB. Strata makes it fit by **sharing the work across your whole PC** - the same idea as a kitchen, where the
things you use all the time stay on the counter and the rest waits in the pantry.
<p align="center"><img src="docs/paper/tiers.svg" width="700" alt="how Strata splits the model between GPU, RAM and SSD"></p>
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="The model's 24,576 experts: the busiest on the graphics card, all of them in RAM, a lookup table on the SSD"></p>
- **The GPU** runs the part of the model that is used for every word, plus the "experts" it needs most often.
- **The RAM** holds all 24,576 experts, and **the CPU** computes the few the GPU doesn't have - at the same time as the GPU.
- **The SSD** holds a big lookup table; the model reads a few rows of it per word.
- **A small helper inside the model guesses the next words**, and Strata checks several guesses at once. That makes
it 1.6-1.8x faster than going word by word - and the answer is exactly the same.
- **The model is a team of 24,576 small specialists ("experts"),** and each word it writes needs only 10 of them.
So it doesn't have to have all of them on the graphics card at once.
- **Your graphics card** does the part of the work needed for every word, and keeps the few thousand experts that
are asked most often. It keeps learning which ones those are while you use it.
- **Your RAM** holds every expert. When a word needs one the card doesn't have, **your processor** works on it -
at the same time as the graphics card, so neither waits for the other.
- **Your SSD** holds a big lookup table; the model only reads a few small rows of it per word.
The full story is in the [paper](docs/paper/Strata-Paper.pdf) and the [details](docs/DETAILS.md).
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="A small helper guesses the next words; the big model checks them all at once and keeps the right ones"></p>
- **Guess, then check.** A small, fast helper built into the model guesses the next few words, and the big model
checks all the guesses in one go. It keeps the ones it agrees with and writes the next word itself - so one step
often produces several words. The helper only guesses - the big model decides every word - so you get the same
quality answer, 1.6-1.8x sooner.
- **Long texts are read in big pieces** (up to 8,192 tokens - pieces of words - at a time), which is why a long
document or code base is read at over 1,000 tokens per second.
Want the full picture? The [details](docs/DETAILS.md#how-it-works) explain every part and its numbers, and the
[paper](docs/paper/Strata-Paper.pdf) tells the whole story, with the measurements behind it.
## Credits
@@ -138,3 +204,10 @@ The full story is in the [paper](docs/paper/Strata-Paper.pdf) and the [details](
- Built with parts of [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp) (MIT). Ideas from
[Splash](https://github.com/incoai/splash), [ninfer](https://github.com/Neroued/ninfer) and
[HyperQwen](https://github.com/syv-ai/HyperQwen). More in the [details](docs/DETAILS.md#credits-and-licenses).
## License
Strata is open source under the [MIT License](LICENSE). A few parts carry their own licenses: `third_party/ggml`
(MIT, llama.cpp / ggml), the web app's font (SIL Open Font License 1.1) and the experimental speed projection's
vector in `data/experimental-speed-projection` (Qwen Community License 1.0, from the model's activations). The
models are not part of this repository; each model's own license applies to its files.
+3
View File
@@ -0,0 +1,3 @@
@echo off
rem Change settings or install another model (the same as START-HERE.bat --setup).
call "%~dp0START-HERE.bat" --setup %*
@@ -0,0 +1,29 @@
# 2026-09-27: expert cache on vs off (issue #23)
Question: the engine used to warn at start that "the GPU hit path is NOT CORRECT ... any OUTPUT from it is not". Does
the expert cache make the output worse?
Method: teacher-forced. `strata --dump-logits` runs a text one token at a time and writes the logits at every
position, so both runs see the same text and each position's next-token prediction can be compared:
- cache off: no `--expert-cache`, every routed expert computed by the CPU pool;
- cache on: `--expert-profile pack/profile-decode-8k.bin --expert-cache auto`, 5,130 of the most-used experts on the GPU.
Q2_0, RTX 5070, the first 512 or 1,024 tokens of three frozen prompts.
| Text | Same top-1 | Median KL | Perplexity off | Perplexity on | On - off (nats per token) |
|---|---:|---:|---:|---:|---:|
| Code agent (512) | 97.7% | 3.0e-04 | 2.672 | 2.630 | -0.016 +- 0.019 |
| Long document (1,024) | 96.2% | 4.5e-03 | 29.844 | 29.843 | -0.000 +- 0.005 |
| Chat (1,024) | 95.0% | 4.4e-03 | 18.380 | 18.301 | -0.004 +- 0.006 |
| **All 2,557 tokens** | | | | | **-0.005 +- 0.005** |
Reading:
- The cache changes which of two nearly equal candidates wins at 2-5% of positions. Most of the flips have a top-2
margin under 0.5 logits (`logits-compare.txt`).
- Neither side is more accurate: the perplexity of the actual text is the same, within 1 standard error.
- A GPU expert and the CPU compute the same quantized expert with a different floating-point order. Both are an
equally good approximation of the model.
The start-up warning dates from round 328, when the GPU hit path had a real fault (divergence from token 0). That fault
was fixed long ago and is covered by `expert_parity`, `native_expert_parity` and the grouped kernels' tests. Since
engine 0.1.8 the warning is replaced by one accurate line.
+36
View File
@@ -0,0 +1,36 @@
# 2026-09-27: the experimental-speed-projection control vector
The package's `Qwen3.8-Flash-Next-experimental-speed-projection.gguf` (47 unit directions of 2,560, one per layer),
applied as `--control-vector-scaled FILE:1.0 --control-vector-layer-range 4 44 --cvec-mode project --cvec-dir
per-layer`: after layers 4-44, every hyper-connection stream `h -= (h . v) v`. The package describes it as a
refusal-direction projection. Q2_0, RTX 5070, fixed experts (`--expert-cache 4000 --pcie-frac 0 --adapt-swaps 0`).
## Kernel (`cvec_parity`)
Project and add against a double-precision reference (max error 5e-7); with the switch off, the kernel's pending
residual write is bitwise the fused hyper-connection read's own, so a loaded-but-off vector is the stock engine.
## Teacher-forced, stock vs projection (`esp_kl.py`, per-token path)
| Text | Tokens | Same top-1 | Mean KL | p99 KL | Perplexity stock | With the vector |
|---|---:|---:|---:|---:|---:|---:|
| Code | 511 | 92.8% | 0.098 | 1.69 | 2.594 | 2.982 |
| Document | 1,023 | 89.8% | 0.058 | 0.84 | 30.299 | 30.984 |
| Chat | 1,023 | 89.6% | 0.050 | 0.33 | 18.409 | 18.485 |
| **All** | 2,557 | | **0.063** (max 4.10) | 1.00 | | |
The package reports a mean KL of 0.186 on its own neutral text with UD-Q2_K_XL in llama.cpp.
Cost on the same tokens: 44.34 -> 44.26, 46.74 -> 46.56, 47.02 -> 46.83 tok/s (0.2-0.4%).
## Serve (`esp_serve.py`)
- Loaded and switched off (`cvec=0`) vs an engine without it: 65 / 65 first tokens after prefixes (verify windows
and the batched prompt path) and three 256-token generations **token-identical**.
- On, off, on again: identical (the switch drops the conversation cache, so nothing computed one way is reused the
other way).
- The prompt paths steer like the per-token path: served-with-vector first tokens agree with the per-token path's
teacher-forced argmax with the vector 62 / 65, and with the stock one 57 / 65 (stock served: 64 / 65 and 57 / 65).
- Decode tok/s of 3 x 256-token generations: stock 66.4, on 76.7 / 81.2. **Not a like-for-like comparison:** the
model writes different text with it on (on the code prompt it copied a long stretch of the source, which prompt
lookup drafts at 91% acceptance, where stock stopped after 78 tokens). On the same tokens it is 0.2-0.4% slower.
+85
View File
@@ -0,0 +1,85 @@
"""Teacher-forced logits, stock vs the experimental-speed-projection vector, on the same texts and fixed experts.
python esp_kl.py run # the dumps (per-token path)
python esp_kl.py compare # same top-1, mean/median KL(stock || projection), perplexities
"""
import json
import os
import subprocess
import sys
import numpy as np
S = os.path.dirname(os.path.abspath(__file__))
E = r"C:\Users\AI-Server\Desktop\Strata\Public\Engine"
EXE = E + r"\build-dev\strata.exe"
VEC = r"C:\Users\AI-Server\Desktop\Strata\experimental-speed-projection\Qwen3.8-Flash-Next-experimental-speed-projection.gguf"
TEXTS = {"code": S + r"\p512.ids", "doc": S + r"\4k-02-long-doc-1k.ids", "chat": S + r"\4k-04-chat-1k.ids"}
OUT = S + r"\esp-kl"
ARMS = {"stock": [],
"proj": ["--control-vector-scaled", VEC + ":1.0", "--control-vector-layer-range", "4", "44",
"--cvec-mode", "project", "--cvec-dir", "per-layer"]}
def base_args():
c = json.load(open(S + r"\cfg-rel.json"))
a = list(c["args"])
i = a.index("--expert-cache")
a[i + 1] = "4000"
j = a.index("--prefill") # the prompt one token at a time: a logits row per position
del a[j:j + 2]
return a + ["--pcie-frac", "0", "--adapt-swaps", "0"], c
def run():
os.makedirs(OUT, exist_ok=True)
args, c = base_args()
env = dict(os.environ)
env["PATH"] = os.pathsep.join(c["lib_dirs"]) + os.pathsep + env["PATH"]
for name, path in TEXTS.items():
for arm, extra in ARMS.items():
out = f"{OUT}\\{name}-{arm}.bin"
if os.path.exists(out):
continue
cmd = [EXE, *args, *extra, "--tokens-file", path, "--max-new", "1", "--dump-logits", out]
with open(f"{OUT}\\{name}-{arm}.log", "w") as log:
r = subprocess.run(cmd, cwd=c["cwd"], stdout=log, stderr=subprocess.STDOUT, env=env)
print(name, arm, "exit", r.returncode, flush=True)
def load(p):
h = np.fromfile(p, dtype=np.int32, count=2)
return np.memmap(p, dtype=np.float32, mode="r", offset=8, shape=(int(h[1]), int(h[0])))
def lsm(x):
x = x.astype(np.float64)
m = x.max(axis=-1, keepdims=True)
return x - (m + np.log(np.exp(x - m).sum(axis=-1, keepdims=True)))
def compare():
print(f"{'text':<6}{'tokens':>7}{'same top-1':>11}{'mean KL':>9}{'median KL':>11}{'p99 KL':>8}{'ppl stock':>10}{'ppl proj':>9}")
allkl = []
for name, path in TEXTS.items():
ids = [int(x) for x in open(path).read().replace(",", " ").split()]
a, b = load(f"{OUT}\\{name}-stock.bin"), load(f"{OUT}\\{name}-proj.bin")
n = min(a.shape[0], b.shape[0], len(ids) - 1)
kl, same, la, lb = [], 0, [], []
for r in range(0, n, 64):
pa, pb = lsm(a[r:min(n, r + 64)]), lsm(b[r:min(n, r + 64)])
kl += list((np.exp(pa) * (pa - pb)).sum(axis=1))
same += int((pa.argmax(1) == pb.argmax(1)).sum())
nxt = np.array(ids[r + 1:min(n, r + 64) + 1])
la += list(pa[np.arange(len(nxt)), nxt])
lb += list(pb[np.arange(len(nxt)), nxt])
kl = np.array(kl)
allkl += list(kl)
print(f"{name:<6}{n:>7}{100 * same / n:>10.1f}%{kl.mean():>9.3f}{np.median(kl):>11.4f}{np.percentile(kl, 99):>8.2f}"
f"{np.exp(-np.mean(la)):>10.3f}{np.exp(-np.mean(lb)):>9.3f}")
allkl = np.array(allkl)
print(f"all {len(allkl)} tokens: mean KL {allkl.mean():.3f}, max {allkl.max():.2f}, p99 {np.percentile(allkl, 99):.2f}")
if __name__ == "__main__":
{"run": run, "compare": compare}[sys.argv[1]]()
+129
View File
@@ -0,0 +1,129 @@
"""Serve-mode checks of the experimental-speed-projection vector (exact mode: fixed experts, no PCIe share).
python esp_serve.py run ARM # ARM = stock | vec; writes esp-serve/<arm>.json
python esp_serve.py compare
Per arm: the first greedy token after many prefixes of three texts (short ones go through the verify windows, long
ones through the batched prompt path; the conversation cache reuses the earlier prefix), and three 256-token greedy
generations. The vec arm runs everything with cvec=1, then cvec=0, then cvec=1 again.
"""
import json
import os
import sys
import threading
S = os.path.dirname(os.path.abspath(__file__))
E = r"C:\Users\AI-Server\Desktop\Strata\Public\Engine"
sys.path.insert(0, E)
from serve.server import StrataEngine # noqa: E402
EXE = E + r"\build-dev\strata.exe"
VEC = r"C:\Users\AI-Server\Desktop\Strata\experimental-speed-projection\Qwen3.8-Flash-Next-experimental-speed-projection.gguf"
TEXTS = {"code": S + r"\p512.ids", "doc": S + r"\4k-02-long-doc-1k.ids", "chat": S + r"\4k-04-chat-1k.ids"}
OUT = S + r"\esp-serve"
VEC_ARGS = ["--control-vector-scaled", VEC + ":1.0", "--control-vector-layer-range", "4", "44",
"--cvec-mode", "project", "--cvec-dir", "per-layer"]
def ids_of(name):
return [int(x) for x in open(TEXTS[name]).read().replace(",", " ").split()]
def engine(extra):
c = json.load(open(S + r"\cfg-rel.json"))
a = list(c["args"])
a[a.index("--expert-cache") + 1] = "4000"
env = dict(os.environ)
env["PATH"] = os.pathsep.join(c["lib_dirs"]) + os.pathsep + env["PATH"]
return StrataEngine(EXE, a + ["--pcie-frac", "0", "--adapt-swaps", "0"] + extra, cwd=c["cwd"],
log=OUT + r"\engine.log", env=env)
def gen(eng, ids, n, sampling):
toks = [t for t in eng.generate(ids, n, sampling, threading.Event()) if t is not None]
return toks, dict(eng.last)
def suite(eng, sampling):
out = {"first": {}, "long": {}}
for name in TEXTS:
ids = ids_of(name)
# short prefixes growing by 4 (each continues the live session through the verify windows), then long ones
# SHRINKING (no cached prefix, so each is read from 0 by the batched prompt path)
lens = list(range(32, 72, 4)) + list(range(min(len(ids) - 1, 700), 95, -48))
out["first"][name] = {L: gen(eng, ids[:L], 1, sampling)[0][0] for L in lens}
for name in TEXTS:
toks, last = gen(eng, ids_of(name)[:300], 256, sampling)
out["long"][name] = {"tokens": toks, "last": last}
return out
def run(arm):
os.makedirs(OUT, exist_ok=True)
eng = engine(VEC_ARGS if arm == "vec" else [])
res = {"info": eng.info}
if arm == "stock":
res["stock"] = suite(eng, {})
else:
res["on"] = suite(eng, {"experimental_speed_projection": True})
res["off"] = suite(eng, {"experimental_speed_projection": False})
res["on2"] = suite(eng, {"experimental_speed_projection": True})
eng.close()
json.dump(res, open(f"{OUT}\\{arm}.json", "w"))
print(arm, "done", eng.info, flush=True)
def compare():
import numpy as np
sys.path.insert(0, S)
from esp_kl import load, OUT as KL
st = json.load(open(OUT + r"\stock.json"))
ve = json.load(open(OUT + r"\vec.json"))
print("engine INFO (vec):", ve["info"])
def eq_first(a, b):
tot = same = 0
for name in a:
for L, t in a[name].items():
tot += 1
same += int(b[name][L] == t)
return same, tot
def eq_long(a, b):
return all(a[n]["tokens"] == b[n]["tokens"] for n in a), \
[(n, next((i for i, (x, y) in enumerate(zip(a[n]["tokens"], b[n]["tokens"])) if x != y), None)) for n in a]
print("1. vector loaded, switched off == no vector")
print(" first tokens: %d / %d identical" % eq_first(st["stock"]["first"], ve["off"]["first"]))
print(" 256-token generations identical:", *eq_long(st["stock"]["long"], ve["off"]["long"]))
print("2. repeatable: on, off, on again")
print(" first tokens: %d / %d identical" % eq_first(ve["on"]["first"], ve["on2"]["first"]))
print(" 256-token generations identical:", *eq_long(ve["on"]["long"], ve["on2"]["long"]))
print("3. the prompt paths against the per-token path's teacher-forced argmax (row L-1 predicts token L)")
for arm, first in (("stock", st["stock"]["first"]), ("on", ve["on"]["first"])):
for ref in ("stock", "proj"):
agree = tot = 0
for name in first:
d = load(f"{KL}\\{name}-{ref}.bin")
for L, t in first[name].items():
L = int(L)
if L - 1 < d.shape[0]:
tot += 1
agree += int(int(np.argmax(d[L - 1])) == t)
print(f" served {arm:<5} vs per-token {ref:<5}: {agree} / {tot}")
print("4. how different the text is: 256-token generations, on vs stock, first difference")
print(" ", eq_long(st["stock"]["long"], ve["on"]["long"])[1])
print("5. decode speed (exact mode: fixed experts, no PCIe share - not the normal setup)")
for label, r in (("stock", st["stock"]), ("vec off", ve["off"]), ("vec on", ve["on"]), ("vec on 2", ve["on2"])):
g = sum(v["last"]["generated"] for v in r["long"].values())
ms = sum(v["last"]["decode_ms"] for v in r["long"].values())
acc = sum(v["last"].get("drafts_accepted", 0) for v in r["long"].values())
off = sum(v["last"].get("drafts_offered", 0) for v in r["long"].values())
print(f" {label:<9} {g / ms * 1000:6.1f} tok/s drafts accepted {acc}/{off}")
if __name__ == "__main__":
if sys.argv[1] == "run":
run(sys.argv[2])
else:
compare()
@@ -0,0 +1,20 @@
1. vector loaded, switched off == no vector
first tokens: 65 / 65 identical
256-token generations identical: True [('code', None), ('doc', None), ('chat', None)]
2. repeatable: on, off, on again
first tokens: 65 / 65 identical
256-token generations identical: True [('code', None), ('doc', None), ('chat', None)]
3. the prompt paths against the per-token path's teacher-forced argmax (row L-1 predicts token L)
served stock vs per-token stock: 64 / 65
served stock vs per-token proj : 57 / 65
served on vs per-token stock: 57 / 65
served on vs per-token proj : 62 / 65
4. how different the text is: 256-token generations, on vs stock, first difference
[('code', 11), ('doc', 6), ('chat', 3)]
5. decode speed (exact mode: fixed experts, no PCIe share - not the normal setup)
stock 66.4 tok/s drafts accepted 256/309
vec off 65.7 tok/s drafts accepted 251/318
vec on 76.7 tok/s drafts accepted 366/425
vec on 2 81.2 tok/s drafts accepted 366/423
[exited with code 0]
@@ -0,0 +1,6 @@
text tokens same top-1 mean KL median KL p99 KL ppl stock ppl proj
code 511 92.8% 0.098 0.0031 1.69 2.594 2.982
doc 1023 89.8% 0.058 0.0271 0.84 30.299 30.984
chat 1023 89.6% 0.050 0.0265 0.33 18.409 18.485
all 2557 tokens: mean KL 0.063, max 4.10, p99 1.00
+60
View File
@@ -0,0 +1,60 @@
# 2026-09-27: Q4_0 KV with Hadamard rotation (PR #21, `--kv q4_0`)
PR #21 (code-martin) adds a 4-bit K/V format: an orthonormal 256-point Walsh-Hadamard rotation of K, V and Q, then
ggml `q4_0` blocks (576 B per cell and layer, vs 1,056 B in int8).
## Integration fixes made on merge
- **Q4 + KV streaming (`--kv-resident`).** The PR had no host copy for Q4 and skipped the staging, so with streaming
on, the prompt path wrote K/V through the residency map's -1 entries (out-of-bounds writes).
- Q4 is now a third format in `kv_stream`: host copy, block residency, prompt-path staging, MTP ring and STATE_HASH.
- `kv_stream_parity` checks it bitwise, streamed vs resident, with ring restore.
- **Duplicate attention.** The per-token decode path ran attention twice, the first time before the selection's
blocks were made resident; it now runs once.
- **Tie-break.** The group maximum's tie-break is now deterministic across lanes: equal |x| with opposite signs
could give one block two scales.
- **Unrelated changes dropped:** duplicate path fixes (already in 0.1.7), personal `.gitignore` entries, and a
re-encoded CMakeLists.
`kv_q4_parity` (the PR's test) and all 70 ctest tests pass.
## Quality (teacher-forced, `tools/kv_precision_compare.py`)
Same text, same fixed expert set (Q2_0, 3,000 slots, `--adapt-swaps 0`); only `--kv` differs. fp16 is the reference.
| Text | KV | Same top-1 as fp16 | Mean KL | Perplexity | vs fp16 (nats/token) |
|---|---|---:|---:|---:|---:|
| code agent (512) | int8 | 98.4% | 0.014 | 2.649 | -0.011 +- 0.006 |
| | q4_0 | 95.7% | 0.030 | 2.598 | -0.031 +- 0.014 |
| document (1,024) | int8 | 95.6% | 0.009 | 30.18 | +0.012 +- 0.005 |
| | q4_0 | 91.3% | 0.039 | 32.26 | **+0.078 +- 0.011** |
| chat (1,024) | int8 | 95.5% | 0.009 | 18.24 | -0.003 +- 0.005 |
| | q4_0 | 90.7% | 0.028 | 18.52 | +0.012 +- 0.008 |
| document, 8K (every 8th position) | int8 | 92.3% | 0.114 | 7.63 | -0.013 +- 0.020 |
| | q4_0 | 88.0% | 0.297 | 8.65 | **+0.112 +- 0.037** |
- **int8 is indistinguishable from fp16** (within about 2 standard errors everywhere).
- **q4_0 costs measurable precision, and more as the context grows:**
- document perplexity +8% at 1K and +12% at 8K;
- the KL from fp16 is 2.6-4.2x int8's;
- code and chat are near-neutral.
- **Needles with `--kv q4_0 --kv-resident 32768`: 5/5** (1K, 32K, 128K at 10% and 90% depth, 262K;
`needles-q4.*`). Retrieval holds.
## Speed and memory (Q2_0, 128K, matrix bench, `--kv-resident 32768`)
| KV | Expert slots | Decode tok/s | Tokens/round | Rounds/s | K/V in RAM |
|---|---:|---:|---:|---:|---:|
| int8 (`kv-stream/matrix-stream`) | 4,044 | 62.4 | 2.52 | 24.8 | 1.52 GiB |
| q4_0 (`matrix-q4`) | 4,190 | 67.4 | 2.62 | 25.7 (+3.7%) | 0.83 GiB |
Without streaming (contexts up to 64K, or too little RAM for it) q4_0 halves the whole KV cache in VRAM instead.
## Decision
`--kv q4_0` is a user option:
- `START-HERE.bat --kv q4_0`, or the question setup asks above 8K context;
- or `--kv q4_0` in a `strata-*.json`.
int8 stays the default: every published quality number was measured with it, and q4_0's precision cost is real,
though it passes the needles.
+44
View File
@@ -0,0 +1,44 @@
# The GSQ-RCO Coder (256 of 512 experts) on Strata
RTX 5070 12 GB, Ryzen 5 7600, 64 GB DDR5-5200, Windows 10. Engine 0.1.15 + PR #54 (the expert count from the model
file), the settings setup writes (`--prefill auto`, 8-bit KV above 4K, KV streaming from 64K, MTP, greedy, 256 tokens),
`data/expert-profile-coder.bin`, the same code-agent prompts as `2026-09-28-speed-0114`.
| | 1K | 4K | 32K | 64K | 128K | 262K |
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
| Prompt, tokens/s | 599 | 1,152 | 1,298 | 1,350 | 1,266 | 1,034 |
| Output, tokens/s | 53.3 | 50.6 | 53.3 | 50.8 | 44.0 | 42.8 |
| Draft acceptance | 0.720 | 0.707 | 0.785 | 0.696 | 0.619 | 0.684 |
| Experts in VRAM | 2,570 | 2,615 | 2,418 | 2,382 | 2,326 | 2,208 |
Every run in [`matrix.json`](matrix.json). The 128K and 262K answers were read: coherent (they list and discuss the
files of the prompt).
## Why it is fast
The kept experts are stored like IQ3_S's (IQ2_S / IQ3_XXS / IQ3_S / IQ4_XS gate and up, IQ4_NL / Q2_0 down; "IQ1_M" is
the release's name for 1.89 bits per *original* parameter), so a token costs about what IQ3_S costs where the expert is
on the CPU. But there are half as many experts: the 23 GB of them pin completely (`cudaHostRegister` of the whole
arena), a 12 GB card holds ~20% of them instead of ~10%, and the prompt path streams half as many.
## The expert profile
`data/expert-profile-coder.bin` (48 x 256, all 12,288 pairs ranked): the shipped 48 x 512 ranking, each pair mapped to
its kept expert's new index through section 2 of the release's `tensor-allocation/*.rco-allocation.txt` (the kept
experts in their original order; gate, up and down agree on which were kept), pruned pairs dropped. 5,492 of the
shipped top 8,000 survive (as PR #54 found). With 2,537 slots on the 12 GB card, 72% of the expert reads of a 4K code
prompt hit the GPU - a wrong index mapping would give about the slot share, 21%.
## Images
The Coder ships the original's vision encoder (the same file). With `--vision` on the GPU it described a Strata
Monitor screenshot correctly, windows, labels and the numbers of the dashboard and the requests table (1,079 prompt
tokens with the image, 2,012 generated at ~50 tokens/s).
## The experimental speed projection
The vector was made from the full model's activations; the Coder has the same residual stream. On the Coder it loads
(41 layers steered), costs no measurable speed (47.7-55.7 tokens/s with it on or off), and both settings write correct
answers to three coding prompts. What it is for - fewer refusals - was not measured. (Two runs of the same prompt with
it off also differ after ~130-200 characters in serve mode, so this check cannot show how far it moves ordinary text;
the teacher-forced method of `2026-09-27-esp` needs the token-by-token prompt path, which native packs do not run.)
@@ -0,0 +1,86 @@
[
{
"model": "CODER",
"tier": "1k",
"prompt_tokens": 1017,
"exit": 0,
"wall_s": 18.0,
"prefill_tok_s": 599.05,
"ttft_s": 2.54,
"decode_tok_s": 53.33,
"decoded": 256,
"spec_accept": 0.72,
"tokens_per_round": 2.86,
"vram_slots": 2570
},
{
"model": "CODER",
"tier": "4k",
"prompt_tokens": 3562,
"exit": 0,
"wall_s": 19.6,
"prefill_tok_s": 1152.17,
"ttft_s": 3.94,
"decode_tok_s": 50.6,
"decoded": 256,
"spec_accept": 0.707,
"tokens_per_round": 2.88,
"vram_slots": 2615
},
{
"model": "CODER",
"tier": "32k",
"prompt_tokens": 31566,
"exit": 0,
"wall_s": 40.7,
"prefill_tok_s": 1297.8,
"ttft_s": 25.17,
"decode_tok_s": 53.34,
"decoded": 256,
"spec_accept": 0.785,
"tokens_per_round": 3.02,
"vram_slots": 2418
},
{
"model": "CODER",
"tier": "64k",
"prompt_tokens": 64162,
"exit": 0,
"wall_s": 75.5,
"prefill_tok_s": 1350.09,
"ttft_s": 49.01,
"decode_tok_s": 50.84,
"decoded": 256,
"spec_accept": 0.696,
"tokens_per_round": 2.61,
"vram_slots": 2382
},
{
"model": "CODER",
"tier": "128k",
"prompt_tokens": 128478,
"exit": 0,
"wall_s": 121.5,
"prefill_tok_s": 1266.31,
"ttft_s": 102.95,
"decode_tok_s": 44.03,
"decoded": 256,
"spec_accept": 0.619,
"tokens_per_round": 2.33,
"vram_slots": 2326
},
{
"model": "CODER",
"tier": "262k",
"prompt_tokens": 259943,
"exit": 0,
"wall_s": 280.7,
"prefill_tok_s": 1033.91,
"ttft_s": 252.92,
"decode_tok_s": 42.8,
"decoded": 256,
"spec_accept": 0.684,
"tokens_per_round": 2.6,
"vram_slots": 2208
}
]
@@ -0,0 +1,47 @@
# Faster prompt reading (engine 0.1.13)
RTX 5070 12 GB, Ryzen 5 7600, 64 GB DDR5. 32K-token prompt (`bench/prompts/long/32k-01-code-agent.ids`), one-shot
engine, `--expert-cache auto`, greedy. Numbers are prompt tokens/s.
## What changed, step by step
| Step | What | Result |
|---|---|---|
| Buffers | The attention half and the MoE half of a layer share one scratch region (~260 KB less per prompt token) | bit-identical; chunk 4096 borrows 1.78 GiB instead of 2.84 |
| PLE rows | The next chunk's PLE rows are read from the model file on a thread while the GPU works | bit-identical; 762 -> 790 at chunk 4096 |
| Chunk size | `--prefill auto`: the largest of 8192 / 6144 / 4096 / ... whose buffers fit in the cache slots it may borrow; a request borrows only what its own prompt needs | Q2_0: 4096 791, 6144 878, 8192 973 |
| Host copies | Experts the arena could not pin are copied to pinned buffers by helper threads instead of the launching thread | bit-identical; IQ3_S at 4096: 551 -> 652 |
| PLE block | Computed for the whole chunk (two GEMMs) instead of token by token | Q2_0 981 -> 1053, IQ3_S 831 -> 940 |
| Experts | llama.cpp's MMQ kernels (weights stay quantized, int8 tensor cores) instead of dequantize-to-FP16 + cuBLAS | Q2_0 1052 -> 1130, IQ3_S (8192) 932 -> 1014 |
| Streaming | At chunks of 2048+ every non-resident expert streams in a fixed order through a ring, so the next layer's arrive while the current layer's attention runs | bit-identical; Q2_0 1130 -> 1290, IQ3_S -> 1208 |
Tried and dropped: cuBLAS grouped GEMMs for the experts (faster in isolation, slower inside the engine: 770 vs 790).
## Before and after
| | engine 0.1.12 (`--prefill 2048`) | engine 0.1.13 (`--prefill auto`) |
|---|---:|---:|
| Q2_0, 32K prompt | 572 | 1,290 |
| IQ3_S, 32K prompt | 383 | 1,208 |
| Server, Q2_0, 128K context, 999-token prompt | 353 | 438 |
| Server, 6,927-token prompt | 529 | 1,077 |
| Server, 28,584-token prompt | 584 | 1,249 |
Output speed is unchanged (the decode path is the same). Short prompts (a few hundred tokens) go through the verify
windows as before.
## Quality
- "Bit-identical" steps were checked with the GDN state hash after the 32K prompt (all 36 recurrent layers).
- The steps that change rounding (the batched PLE block, MMQ's 8-bit activations) were compared teacher-forced: the
32K prompt is read by the prompt path up to its last 256 tokens, those go through the token path with their logits
dumped. At 32K every rounding change moves the predictions about this much - even a different chunk size of the
unchanged engine - so that is the yardstick:
| Against the 8192-chunk FP16 path | Same top-1 | Mean KL | Perplexity (reference 9.21) |
|---|---:|---:|---:|
| Chunk 6144, same code (the yardstick) | 89.8% | 0.33 | 9.18 |
| MMQ experts | 85.9% | 0.38 | 8.63 |
- Needles (the answer hidden at a given depth): 5 of 5 at 1K, 32K, 128K (two depths) and 262K, Q2_0, KV streaming.
- A rerun of the same configuration is bit-identical.
@@ -0,0 +1,32 @@
# Speed with engine 0.1.14
RTX 5070 12 GB (PCIe 5.0 x16), Ryzen 5 7600, 64 GB DDR5-5200, Windows 10. The ready-made 0.1.14 engine, one-shot runs
with what setup writes: `--prefill auto`, the shipped expert profile, `--expert-cache auto`, 8-bit KV above 4K, KV
streaming (`--kv-resident 32768`) from 64K, MTP (`--spec 4 --spec-min-p 0.5`), greedy, 256 generated tokens. One
code-agent prompt per length (`bench/prompts/*/…-code-agent.ids`, the same prompts as `2026-09-24-final`).
The tables: [`matrix.md`](matrix.md) (prompt and output speed, draft acceptance, experts in VRAM); every run in
[`matrix.json`](matrix.json).
- **IQ2_XS** was measured with Swift 1.5's IQ2_XS (the original's file was not on the machine; the two run at the same
speed: 4K 465 vs 467 prompt tokens/s in `2026-09-24`).
- **IQ3_S at 128K** comes from a first pass during which a large download was running (the clean rerun of that row
was stopped when Windows ran short of memory); the other rows are from runs on an otherwise idle machine.
- **IQ3_XXS and IQ3_S at 262K** are not measured: with 43 / 50 GB of experts, a 260K context exceeds 64 GB of RAM.
## Against 0.1.12
Prompts are 1.2x (1K) to 2.4x (32K-128K) faster: the 0.1.13 prompt path (chunks of up to 8,192 tokens, MMQ experts,
streamed expert copies; see `2026-09-28-prefill-speed`).
Output speed moves with the text: speculative decoding is faster when more drafts are accepted, and the new prompt
path's rounding changes the answer, so the same prompt can land a few percent either way (Q2_0 32K: acceptance 0.865
then, 0.709 now). The engine itself is not slower: on the 4K prompt, run back to back on the same machine state,
0.1.14 writes 88.5 tokens/s and 0.1.12 85.7 (3 runs each, same free VRAM).
## Found on the way
IQ3_XXS at 64K and 128K ended with `out of memory: cudaFuncSetAttribute` in the MMQ launch: CUDA loads a kernel's code
when it is first used, and mid-prompt there was no VRAM left for it. Loading every kernel at start
(`CUDA_MODULE_LOADING=EAGER`, which the engine now sets itself) fixes it for ~30 MB of VRAM; the IQ3_XXS 64K/128K rows
were measured with that fix.
@@ -0,0 +1,322 @@
[
{
"model": "Q2_0",
"tier": "1k",
"prompt_tokens": 1017,
"exit": 0,
"wall_s": 41.0,
"prefill_tok_s": 494.43,
"ttft_s": 3.55,
"decode_tok_s": 84.26,
"decoded": 256,
"spec_accept": 0.709,
"tokens_per_round": 2.72,
"vram_slots": 4176
},
{
"model": "Q2_0",
"tier": "4k",
"prompt_tokens": 3562,
"exit": 0,
"wall_s": 41.2,
"prefill_tok_s": 1006.63,
"ttft_s": 5.05,
"decode_tok_s": 90.26,
"decoded": 256,
"spec_accept": 0.846,
"tokens_per_round": 3.23,
"vram_slots": 4123
},
{
"model": "Q2_0",
"tier": "32k",
"prompt_tokens": 31566,
"exit": 0,
"wall_s": 63.1,
"prefill_tok_s": 1307.6,
"ttft_s": 25.63,
"decode_tok_s": 73.61,
"decoded": 256,
"spec_accept": 0.709,
"tokens_per_round": 2.68,
"vram_slots": 3833
},
{
"model": "Q2_0",
"tier": "64k",
"prompt_tokens": 64162,
"exit": 0,
"wall_s": 89.0,
"prefill_tok_s": 1293.5,
"ttft_s": 51.12,
"decode_tok_s": 69.02,
"decoded": 256,
"spec_accept": 0.679,
"tokens_per_round": 2.62,
"vram_slots": 3780
},
{
"model": "Q2_0",
"tier": "128k",
"prompt_tokens": 128478,
"exit": 0,
"wall_s": 147.4,
"prefill_tok_s": 1208.46,
"ttft_s": 107.81,
"decode_tok_s": 67.21,
"decoded": 256,
"spec_accept": 0.691,
"tokens_per_round": 2.64,
"vram_slots": 3697
},
{
"model": "Q2_0",
"tier": "262k",
"prompt_tokens": 259943,
"exit": 0,
"wall_s": 309.8,
"prefill_tok_s": 966.84,
"ttft_s": 270.35,
"decode_tok_s": 60.3,
"decoded": 256,
"spec_accept": 0.623,
"tokens_per_round": 2.43,
"vram_slots": 3525
},
{
"model": "IQ2_XS",
"tier": "1k",
"prompt_tokens": 1017,
"exit": 0,
"wall_s": 40.4,
"prefill_tok_s": 461.28,
"ttft_s": 2.28,
"decode_tok_s": 74.38,
"decoded": 256,
"spec_accept": 0.772,
"tokens_per_round": 2.84,
"vram_slots": 4386,
"note": "first pass"
},
{
"model": "IQ2_XS",
"tier": "4k",
"prompt_tokens": 3562,
"exit": 0,
"wall_s": 41.8,
"prefill_tok_s": 810.71,
"ttft_s": 4.48,
"decode_tok_s": 73.84,
"decoded": 256,
"spec_accept": 0.79,
"tokens_per_round": 3.24,
"vram_slots": 4336,
"note": "first pass"
},
{
"model": "IQ2_XS",
"tier": "32k",
"prompt_tokens": 31566,
"exit": 0,
"wall_s": 65.0,
"prefill_tok_s": 1237.98,
"ttft_s": 25.59,
"decode_tok_s": 71.49,
"decoded": 256,
"spec_accept": 0.856,
"tokens_per_round": 3.28,
"vram_slots": 4057,
"note": "first pass"
},
{
"model": "IQ2_XS",
"tier": "64k",
"prompt_tokens": 64162,
"exit": 0,
"wall_s": 97.1,
"prefill_tok_s": 1135.63,
"ttft_s": 56.59,
"decode_tok_s": 64.29,
"decoded": 256,
"spec_accept": 0.707,
"tokens_per_round": 2.76,
"vram_slots": 4005,
"note": "first pass"
},
{
"model": "IQ2_XS",
"tier": "128k",
"prompt_tokens": 128478,
"exit": 0,
"wall_s": 158.0,
"prefill_tok_s": 1070.61,
"ttft_s": 120.1,
"decode_tok_s": 59.76,
"decoded": 256,
"spec_accept": 0.726,
"tokens_per_round": 2.69,
"vram_slots": 3923,
"note": "first pass"
},
{
"model": "IQ2_XS",
"tier": "262k",
"prompt_tokens": 259943,
"exit": 0,
"wall_s": 332.4,
"prefill_tok_s": 886.2,
"ttft_s": 293.43,
"decode_tok_s": 52.83,
"decoded": 256,
"spec_accept": 0.628,
"tokens_per_round": 2.53,
"vram_slots": 3565,
"note": "first pass"
},
{
"model": "IQ3_XXS",
"tier": "1k",
"prompt_tokens": 1017,
"exit": 0,
"wall_s": 44.1,
"prefill_tok_s": 414.55,
"ttft_s": 2.54,
"decode_tok_s": 60.26,
"decoded": 256,
"spec_accept": 0.864,
"tokens_per_round": 3.52,
"vram_slots": 3304,
"note": "first pass"
},
{
"model": "IQ3_XXS",
"tier": "4k",
"prompt_tokens": 3562,
"exit": 0,
"wall_s": 45.8,
"prefill_tok_s": 769.57,
"ttft_s": 4.73,
"decode_tok_s": 62.07,
"decoded": 256,
"spec_accept": 0.855,
"tokens_per_round": 3.24,
"vram_slots": 3263,
"note": "first pass"
},
{
"model": "IQ3_XXS",
"tier": "32k",
"prompt_tokens": 31566,
"exit": 0,
"wall_s": 69.7,
"prefill_tok_s": 1107.75,
"ttft_s": 28.61,
"decode_tok_s": 51.4,
"decoded": 256,
"spec_accept": 0.678,
"tokens_per_round": 2.6,
"vram_slots": 3031,
"note": "first pass"
},
{
"model": "IQ3_XXS",
"tier": "64k",
"prompt_tokens": 64162,
"exit": 0,
"wall_s": 104.9,
"prefill_tok_s": 1065.31,
"ttft_s": 61.77,
"decode_tok_s": 49.95,
"decoded": 256,
"spec_accept": 0.741,
"tokens_per_round": 2.94,
"vram_slots": 2965,
"note": "first pass"
},
{
"model": "IQ3_XXS",
"tier": "128k",
"prompt_tokens": 128478,
"exit": 0,
"wall_s": 172.1,
"prefill_tok_s": 1015.11,
"ttft_s": 128.12,
"decode_tok_s": 45.78,
"decoded": 256,
"spec_accept": 0.665,
"tokens_per_round": 2.35,
"vram_slots": 2901,
"note": "first pass"
},
{
"model": "IQ3_S",
"tier": "1k",
"prompt_tokens": 1017,
"exit": 0,
"wall_s": 56.2,
"prefill_tok_s": 396.13,
"ttft_s": 4.14,
"decode_tok_s": 51.52,
"decoded": 256,
"spec_accept": 0.826,
"tokens_per_round": 3.12,
"vram_slots": 2617
},
{
"model": "IQ3_S",
"tier": "4k",
"prompt_tokens": 3562,
"exit": 0,
"wall_s": 57.5,
"prefill_tok_s": 736.9,
"ttft_s": 6.35,
"decode_tok_s": 51.6,
"decoded": 256,
"spec_accept": 0.747,
"tokens_per_round": 2.89,
"vram_slots": 2580
},
{
"model": "IQ3_S",
"tier": "32k",
"prompt_tokens": 31566,
"exit": 0,
"wall_s": 82.5,
"prefill_tok_s": 1069.83,
"ttft_s": 31.33,
"decode_tok_s": 48.19,
"decoded": 256,
"spec_accept": 0.772,
"tokens_per_round": 2.94,
"vram_slots": 2383
},
{
"model": "IQ3_S",
"tier": "64k",
"prompt_tokens": 64162,
"exit": 0,
"wall_s": 115.0,
"prefill_tok_s": 1069.8,
"ttft_s": 61.47,
"decode_tok_s": 48.75,
"decoded": 256,
"spec_accept": 0.786,
"tokens_per_round": 2.9,
"vram_slots": 2347
},
{
"model": "IQ3_S",
"tier": "128k",
"prompt_tokens": 128478,
"exit": 0,
"wall_s": 191.8,
"prefill_tok_s": 931.09,
"ttft_s": 138.11,
"decode_tok_s": 40.48,
"decoded": 256,
"spec_accept": 0.661,
"tokens_per_round": 2.71,
"vram_slots": 2310,
"note": "first pass (a download was running)"
}
]
@@ -0,0 +1,35 @@
### Prompt processing (tok/s)
| Model | 1K | 4K | 32K | 64K | 128K | 262K |
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
| Q2_0 | 494 | 1,007 | 1,308 | 1,294 | 1,208 | 967 |
| IQ2_XS | 461 | 811 | 1,238 | 1,136 | 1,071 | 886 |
| IQ3_XXS | 415 | 770 | 1,108 | 1,065 | 1,015 | - |
| IQ3_S | 396 | 737 | 1,070 | 1,070 | 931 | - |
### Output (tok/s)
| Model | 1K | 4K | 32K | 64K | 128K | 262K |
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
| Q2_0 | 84.3 | 90.3 | 73.6 | 69.0 | 67.2 | 60.3 |
| IQ2_XS | 74.4 | 73.8 | 71.5 | 64.3 | 59.8 | 52.8 |
| IQ3_XXS | 60.3 | 62.1 | 51.4 | 50.0 | 45.8 | - |
| IQ3_S | 51.5 | 51.6 | 48.2 | 48.8 | 40.5 | - |
### Draft acceptance
| Model | 1K | 4K | 32K | 64K | 128K | 262K |
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
| Q2_0 | 0.709 | 0.846 | 0.709 | 0.679 | 0.691 | 0.623 |
| IQ2_XS | 0.772 | 0.790 | 0.856 | 0.707 | 0.726 | 0.628 |
| IQ3_XXS | 0.864 | 0.855 | 0.678 | 0.741 | 0.665 | - |
| IQ3_S | 0.826 | 0.747 | 0.772 | 0.786 | 0.661 | - |
### Experts in VRAM
| Model | 1K | 4K | 32K | 64K | 128K | 262K |
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
| Q2_0 | 4,176 | 4,123 | 3,833 | 3,780 | 3,697 | 3,525 |
| IQ2_XS | 4,386 | 4,336 | 4,057 | 4,005 | 3,923 | 3,565 |
| IQ3_XXS | 3,304 | 3,263 | 3,031 | 2,965 | 2,901 | - |
| IQ3_S | 2,617 | 2,580 | 2,383 | 2,347 | 2,310 | - |
@@ -0,0 +1,84 @@
# Layer split across GPUs (2026-09-29)
**Rig (a community member's PC, "RIBPC"):**
- Ryzen 9 9950X3D, 62 GB DDR5, Windows 11, CUDA 13.1.
- RTX 5080 16 GB (x16), RTX 3090 24 GB (x4 gen4, ~6 GB/s), RTX 2080 Ti 22 GB (x4 gen3, Turing sm_75; the
experimental `STRATA_EXPERIMENTAL_SM75` build).
**Model:** the Coder (GSQ-RCO IQ1_M, 48 layers x 256 experts = 12,288 expert pairs, ~23 GB of experts), 32K
context, int8 KV, `--prefill auto`, `--spec 4`. It is the only model that fits the rig's RAM and disk.
**Tools:** `rib_pp.py` (needle prompts at 2.7K/8K/16K/28K tokens with a code word at 50% depth, a 2-turn chat that
must reuse its checkpoint, greedy decode of a story and a code prompt) and `rib_norepeat.py` (the Phase A
no-repeat and sanity cases). Raw results are in `data/`.
## Correctness
| Check | Result |
|---|---|
| This branch vs the 0.1.20 release, one GPU (5080), fixed cache, all 10 cases | **10/10 byte-identical** |
| This branch, one GPU vs the split on the same GPU (K=24, `--split-device 0`) | **10/10 byte-identical**: the hand-off is exact |
| 5080 + 3090 (auto) and 5080 + 2080 Ti + 3090 (auto): no-repeat under presence 100 (greedy and sampled), 144 sanity | all pass, 0 repeats |
| Needles at 2.7K-28K tokens, every configuration | all found |
| 2-turn chat (the checkpoint restored on every GPU) | reused 6,631-6,632 of 6,660 tokens, answer right |
Across GPUs the text is not byte-identical to one GPU: experts the CPU computed on one card run on a GPU in the split,
and they round differently (the same effect as `--expert-cache`, bench/results/2026-09-27-cache-parity).
## Speed
**Prompt tok/s** at 16K / 28K tokens, and **decode tok/s** for story / code (greedy; three repeats averaged
in the K sweep):
| Configuration | Split | Prompt 16K | Prompt 28K | Decode story | Decode code | Decode hits |
|---|---|---|---|---|---|---|
| 0.1.20, 5080 alone | - | 2,045 | 2,005 | 83.2 | 88.2 | 70-84% |
| this branch, 5080 alone | - | 2,017 | 1,974 | 83.1 | 88.0 | 70-84% |
| this branch, 5080 alone, 3 repeats (adaptive swaps learn the prompt) | - | 1,726 | 1,970 | 86.7 | 105.2 | 91-92% |
| 3090 alone | - | 1,253 | 1,211 | 71.7 | 92.9 | 86-93% |
| 5080 + 3090 | K=18 | 1,547 | 1,694 | 77.2 | 103.8 | 99.9% |
| 5080 + 3090 | K=22 | 1,840 | 2,074 | 80.2 | 106.4 | 99.8% |
| 5080 + 3090 | K=24 | 1,978 | 2,258 | 83.5 | 109.5 | 99.5% |
| **5080 + 3090** | **K=26** | **2,039** | **2,357** | **83.8** | **109.7** | 99.3% |
| 5080 + 3090 | K=28 | 2,011 | 2,287 | 83.5 | 112.5 | 99.0% |
| 5080 + 3090 | K=30 | 1,935 | 2,218 | 81.1 | 109.6 | 98% |
| 5080 + 3090 | K=32 | 1,818 | 2,063 | 83.0 | 108.7 | 98% |
| 5080 + 3090 | auto (K=22) | 2,037 | 2,073 | 79.5 | 108.9 | 99.7% |
| 3090 + 5080 | auto (K=30) | 1,672 | 1,876 | 90.3 | 105.1 | 99.4% |
| 5080 + 2080 Ti + 3090 | auto (22, 23) | 1,687 | 1,847 | 67.6 | 89.6 | 99.7% |
| 5080 + 3090 + 2080 Ti | first auto (12, 33) | 1,148 | 1,144 | 51.9 | 69.7 | 100% |
| 5080 + 2080 Ti | first auto | 914 | 906 | 45.5 | 58.0 | 97% |
What decides it:
- **The best two-GPU split for the Coder is K=26-28.** Prompts run +18-20% over the 5080 alone at 16-28K;
decode is on par for story and +4-7% for code.
- **Residency saturates quickly: the per-layer GPU speed decides.** From K=18 up, both caches hold 98-100% of
the routed experts. Moving layers to the faster card is what helps: the 3090 costs 0.50 ms per layer per
window, the 5080 0.33 (`STRATA_SPLIT_TIMING`, the "wait for the GPU" per stage).
- **Decode is bounded by that GPU time.** One 5080 with its cache adapted spends 16.3 ms waiting for its GPU and
5.5 ms in the CPU pool per window. The split removes the pool (0.8-1.0 ms) but adds the slower card's layers:
7.9 + 12.1 ms at K=24. The per-layer GPU half (kernels plus the host round trip that publishes each layer's
expert plan) is the next thing to cut, for one GPU as well (perf-review-plan E-6).
- **A third, slower card costs more than it holds.** The 2080 Ti's one-layer stage took 3.1 ms per window.
For a model that already fits two cards, leave it out. It pays only when the two faster cards cannot hold most
of the routed experts (a bigger model; not testable on this rig's 62 GB of RAM).
- **Prompt chunk:** 1024 instead of 2048 frees ~0.7 GB per card, but prompts read 7-12% slower.
## Auto placement
`--layer-split auto` predicts one decode window's time for every placement (every placement up to three GPUs) and
keeps the fastest:
- **per-layer time** from SMs x clock (0.33 ms for an RTX 5080);
- **cache contents:** each card's layers' profiled pairs, hottest first, as the fill does;
- **routed mass** of rank r taken as (r+1)^-1.2 (fits the sweep's hit rates within 0.1-1%);
- **missed mass** costing 190 ms per unit (fitted to the sweep's best K; `STRATA_SPLIT_MISS_MS` overrides).
It picked K=22 on the 5080 + 3090: within 0-12% of the best fixed split on prompts and on par on decode. It gives
a slow third card a single layer.
## Reproduce
On a rig: the `strata-remote` tools, then `rib_matrix.bat`, `rib_ksweep.bat`, `rib_validate.bat` (in the dev
notes, not in this repository).
@@ -0,0 +1,92 @@
[
{
"name": "greedy story 200",
"tokens": 200,
"repeats": 0,
"drafts_accepted": 88,
"drafts_offered": 193,
"tok_s": 74.6,
"text": "The wind didn\u2019t just blow; it screamed.\n\nElias pulled his wool coat tighter around his shoulders, the fabric stiff with salt and age. He was seventy years old, but he moved with the grace of a man who had spent decades on a narrow spiral staircase. His hands, rough as sandpaper, gripped the iron railing as he ascended the final flight of steps to the lantern room.\n\nBelow him, the Atlantic churned in a frenzy, whitecaps exploding against the black rocks like fireworks. Above, the sky was a bruised purple, heavy with rain that hadn\u2019t yet fallen.\n\nElias checked the clock. 11:42 PM. The light was due.\n\nHe didn\u2019t flip a switch. He didn\u2019t press a button. He reached for the heavy brass lever that had worn smooth by his palm over the years. With a grunt that matched the strain of his aging muscles, he pulled it down.\n\n*Clunk.*\n\nThe"
},
{
"name": "greedy code 200",
"tokens": 200,
"repeats": 0,
"drafts_accepted": 136,
"drafts_offered": 181,
"tok_s": 105.5,
"text": "```python\nimport csv\nfrom typing import List, Dict, Any\n\n\ndef parse_csv_to_dicts(filepath: str) -> List[Dict[str, Any]]:\n \"\"\"\n Parse a CSV file into a list of dictionaries.\n \n Each dictionary represents a row, with keys from the header and values from the row.\n \n Args:\n filepath: Path to the CSV file.\n \n Returns:\n A list of dictionaries, one per row (excluding the header).\n \"\"\"\n result: List[Dict[str, Any]] = []\n with open(filepath, 'r', newline='', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n for row in reader:\n result.append(dict(row))\n return result\n\n\n# Tests\ndef test_parse_csv_to_dicts():\n import tempfile\n import os\n \n # Create a temporary CSV file\n with tempfile.NamedTemporaryFile"
},
{
"name": "greedy sanity",
"tokens": 4,
"repeats": 0,
"drafts_accepted": 3,
"drafts_offered": 3,
"tok_s": 85.8,
"text": "144<|im_end|>"
},
{
"name": "greedy, no penalty (sanity)",
"tokens": 4,
"repeats": 0,
"drafts_accepted": 3,
"drafts_offered": 3,
"tok_s": 94.3,
"text": "144<|im_end|>"
},
{
"name": "greedy presence=100 last_n=64",
"tokens": 118,
"repeats": 0,
"drafts_accepted": 37,
"drafts_offered": 105,
"tok_s": 67.9,
"text": "Aardvark \nAlbatross \nAlligator \nAntelope \nBaboon \nBadger \nBeaver \nBear \nBird of Paradise \nBuffalo \nCamel \nCheetah \nChimpanzee \nCrane \nCrocodile \nDeer \nDolphin \nDonkey \nElephant \nFlamingo \nFox \nGiraffe \nGoat \nHippopotamus \nIguana \nJaguar \nKangaroo \nKoala \nLion \nLeopard \nMonkey \nMoose \nOstrich \nOtter\nPanda<|im_end|>"
},
{
"name": "greedy presence=100 last_n=64 story",
"tokens": 147,
"repeats": 0,
"drafts_accepted": 26,
"drafts_offered": 103,
"tok_s": 63.2,
"text": "The wind didn\u2019t just blow; it screamed against the glass of Lantern House 42, rattling like chains in an empty cathedral hall outside my window at midnight on November third this year\u2014so I pulled up to that date and time for context because you asked me about a lighthouse keeper but gave no specifics beyond \u201cshort story,\u201d so let\u2019s set it: isolated coastal station off the Oregon coast during winter storm season, 1987.\n\nMy name is Elias Vane (I made this fictional; real keepers often kept journals signed with initials only), and I\u2019m writing from memory because my logbook got waterlogged when a rogue wave breached lower storage room at dawn\u2014so here\u2019s what happened:<|im_end|>"
},
{
"name": "sampled presence=100 last_n=64 seed 3",
"tokens": 109,
"repeats": 0,
"drafts_accepted": 13,
"drafts_offered": 56,
"tok_s": 54.1,
"text": "The wind didn\u2019t just blow at Cape Hollow; it screamed through the gaps in my cottage\u2019s shutters, rattling them like loose teeth until they settled into silence with every storm break out of three hundred days here being storms that broke something else again but mostly kept me warm because I burned coal and wood whenever we had any left over from last winter when even then if you\u2019re lucky enough to get through it all without freezing yourself so hard your own thoughts become ice too\n\nBut Elias knew better than anyone: fear was useful only until dawn<|im_end|>"
},
{
"name": "sampled presence=100 last_n=64 seed 11",
"tokens": 92,
"repeats": 0,
"drafts_accepted": 26,
"drafts_offered": 48,
"tok_s": 64.7,
"text": "Lion \nTiger \nElephant \nGiraffe \nZebra \nPanda \nKoala \nKangaroo \nHippopotamus \nRhinoceros \nCamel \nCheetah \nLeopard \nPanther \nJaguar \nBison \nElk \nDeer \nFox \nWolf \nBear \nLlama \nDonkey \nGoat \nSheep \nCow \nPig \nRabbit \nCat \nDog<|im_end|>"
},
{
"name": "greedy frequency=100 last_n=16",
"tokens": 11,
"repeats": 0,
"drafts_accepted": 5,
"drafts_offered": 14,
"tok_s": 71.7,
"text": "1 to 50, separated by commas:<|im_end|>"
},
{
"name": "greedy presence=100 last_n=4096 (8-row windows)",
"tokens": 300,
"repeats": 0,
"drafts_accepted": 53,
"drafts_offered": 134,
"tok_s": 66.1,
"text": "The ocean is not merely water; it has been, and continues to be one with humanity\u2019s most profound narrative arc in human civilization: **the story we tell ourselves through our relationship** between landmasses separated by vast expansiveness that defines both isolation from other worlds (and thus their own unique evolutionary paths) as well than connection across them via currents wind patterns tides depth pressure temperature salinity biodiversity geology climate change economic trade routes ecological systems cultural exchange political boundaries scientific discovery technological advancement environmental degradation conservation efforts maritime archaeography historical cartographic evolution legal frameworks international law enforcement mechanisms insurance liability regimes regulatory compliance standards safety protocols navigation aids communication networks emergency response strategies search-and-rescue operations salvage rights ownership disputes heritage protection zones sustainable harvesting practices aquaculture development infrastructure engineering projects coastal management policies urban planning regulations zoning ordinances building codes architectural design guidelines aesthetic considerations functional requirements accessibility provisions energy efficiency measures thermal insulation techniques moisture control methods ventilation optimization lighting solutions acoustic soundproofing structural integrity assessments seismic resistance evaluations flood mitigation plans storm surge barriers levee construction seawall reinforcement breakwater installation harbor dredging channel maintenance buoy placement lighthouse automation radar monitoring sonar mapping bathymetric surveys hydrographic charts nautical almanacs tide tables current predictions weather forecasts marine meteorological models satellite imagery remote sensing data assimilation numerical simulations computational fluid dynamics finite element analysis partial differential equations tensor calculus linear algebra probability theory statistics stochastic processes Markov chains Bayesian inference machine learning artificial neural network deep convolutional recurrent transformer attention heads generative adversarial diffusion transformers large language model tokenization"
}
]
@@ -0,0 +1,92 @@
[
{
"name": "greedy story 200",
"tokens": 200,
"repeats": 0,
"drafts_accepted": 88,
"drafts_offered": 193,
"tok_s": 68.2,
"text": "The wind didn\u2019t just blow; it screamed.\n\nElias pulled his wool coat tighter around his shoulders, the fabric stiff with salt and age. He was seventy years old, but he moved with the grace of a man who had spent decades on a narrow spiral staircase. His hands, rough as sandpaper, gripped the iron railing as he ascended the final flight of steps to the lantern room.\n\nBelow him, the Atlantic churned in a frenzy, whitecaps exploding against the black rocks like fireworks. Above, the sky was a bruised purple, heavy with rain that hadn\u2019t yet fallen.\n\nElias checked the clock. 11:42 PM. The light was due.\n\nHe didn\u2019t flip a switch. He didn\u2019t press a button. He reached for the heavy brass lever that had worn smooth by his palm over the years. With a grunt that matched the strain of his aging muscles, he pulled it down.\n\n*Clunk.*\n\nThe"
},
{
"name": "greedy code 200",
"tokens": 200,
"repeats": 0,
"drafts_accepted": 136,
"drafts_offered": 181,
"tok_s": 99.4,
"text": "```python\nimport csv\nfrom typing import List, Dict, Any\n\n\ndef parse_csv_to_dicts(filepath: str) -> List[Dict[str, Any]]:\n \"\"\"\n Parse a CSV file into a list of dictionaries.\n \n Each dictionary represents a row, with keys from the header and values from the row.\n \n Args:\n filepath: Path to the CSV file.\n \n Returns:\n A list of dictionaries, one per row (excluding the header).\n \"\"\"\n result: List[Dict[str, Any]] = []\n with open(filepath, 'r', newline='', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n for row in reader:\n result.append(dict(row))\n return result\n\n\n# Tests\ndef test_parse_csv_to_dicts():\n import tempfile\n import os\n \n # Create a temporary CSV file\n with tempfile.NamedTemporaryFile"
},
{
"name": "greedy sanity",
"tokens": 4,
"repeats": 0,
"drafts_accepted": 3,
"drafts_offered": 3,
"tok_s": 74.5,
"text": "144<|im_end|>"
},
{
"name": "greedy, no penalty (sanity)",
"tokens": 4,
"repeats": 0,
"drafts_accepted": 3,
"drafts_offered": 3,
"tok_s": 76.3,
"text": "144<|im_end|>"
},
{
"name": "greedy presence=100 last_n=64",
"tokens": 118,
"repeats": 0,
"drafts_accepted": 37,
"drafts_offered": 105,
"tok_s": 65.2,
"text": "Aardvark \nAlbatross \nAlligator \nAntelope \nBaboon \nBadger \nBeaver \nBear \nBird of Paradise \nBuffalo \nCamel \nCheetah \nChimpanzee \nCrane \nCrocodile \nDeer \nDolphin \nDonkey \nElephant \nFlamingo \nFox \nGiraffe \nGoat \nHippopotamus \nIguana \nJaguar \nKangaroo \nKoala \nLion \nLeopard \nMonkey \nMoose \nOstrich \nOtter\nPanda<|im_end|>"
},
{
"name": "greedy presence=100 last_n=64 story",
"tokens": 147,
"repeats": 0,
"drafts_accepted": 26,
"drafts_offered": 103,
"tok_s": 45.7,
"text": "The wind didn\u2019t just blow; it screamed against the glass of Lantern House 42, rattling like chains in an empty cathedral hall outside my window at midnight on November third this year\u2014so I pulled up to that date and time for context because you asked me about a lighthouse keeper but gave no specifics beyond \u201cshort story,\u201d so let\u2019s set it: isolated coastal station off the Oregon coast during winter storm season, 1987.\n\nMy name is Elias Vane (I made this fictional; real keepers often kept journals signed with initials only), and I\u2019m writing from memory because my logbook got waterlogged when a rogue wave breached lower storage room at dawn\u2014so here\u2019s what happened:<|im_end|>"
},
{
"name": "sampled presence=100 last_n=64 seed 3",
"tokens": 109,
"repeats": 0,
"drafts_accepted": 13,
"drafts_offered": 56,
"tok_s": 42.1,
"text": "The wind didn\u2019t just blow at Cape Hollow; it screamed through the gaps in my cottage\u2019s shutters, rattling them like loose teeth until they settled into silence with every storm break out of three hundred days here being storms that broke something else again but mostly kept me warm because I burned coal and wood whenever we had any left over from last winter when even then if you\u2019re lucky enough to get through it all without freezing yourself so hard your own thoughts become ice too\n\nBut Elias knew better than anyone: fear was useful only until dawn<|im_end|>"
},
{
"name": "sampled presence=100 last_n=64 seed 11",
"tokens": 92,
"repeats": 0,
"drafts_accepted": 26,
"drafts_offered": 48,
"tok_s": 43.9,
"text": "Lion \nTiger \nElephant \nGiraffe \nZebra \nPanda \nKoala \nKangaroo \nHippopotamus \nRhinoceros \nCamel \nCheetah \nLeopard \nPanther \nJaguar \nBison \nElk \nDeer \nFox \nWolf \nBear \nLlama \nDonkey \nGoat \nSheep \nCow \nPig \nRabbit \nCat \nDog<|im_end|>"
},
{
"name": "greedy frequency=100 last_n=16",
"tokens": 11,
"repeats": 0,
"drafts_accepted": 5,
"drafts_offered": 14,
"tok_s": 61.4,
"text": "1 to 50, separated by commas:<|im_end|>"
},
{
"name": "greedy presence=100 last_n=4096 (8-row windows)",
"tokens": 300,
"repeats": 0,
"drafts_accepted": 53,
"drafts_offered": 134,
"tok_s": 51.9,
"text": "The ocean is not merely water; it has been, and continues to be one with humanity\u2019s most profound narrative arc in human civilization: **the story we tell ourselves through our relationship** between landmasses separated by vast expansiveness that defines both isolation from other worlds (and thus their own unique evolutionary paths) as well than connection across them via currents wind patterns tides depth pressure temperature salinity biodiversity geology climate change economic trade routes ecological systems cultural exchange political boundaries scientific discovery technological advancement environmental degradation conservation efforts maritime archaeography historical cartographic evolution legal frameworks international law enforcement mechanisms insurance liability regimes regulatory compliance standards safety protocols navigation aids communication networks emergency response strategies search-and-rescue operations salvage rights ownership disputes heritage protection zones sustainable harvesting practices aquaculture development infrastructure engineering projects coastal management policies urban planning regulations zoning ordinances building codes architectural design guidelines aesthetic considerations functional requirements accessibility provisions energy efficiency measures thermal insulation techniques moisture control methods ventilation optimization lighting solutions acoustic soundproofing structural integrity assessments seismic resistance evaluations flood mitigation plans storm surge barriers levee construction seawall reinforcement breakwater installation harbor dredging channel maintenance buoy placement lighthouse automation radar monitoring sonar mapping bathymetric surveys hydrographic charts nautical almanacs tide tables current predictions weather forecasts marine meteorological models satellite imagery remote sensing data assimilation numerical simulations computational fluid dynamics finite element analysis partial differential equations tensor calculus linear algebra probability theory statistics stochastic processes Markov chains Bayesian inference machine learning artificial neural network deep convolutional recurrent transformer attention heads generative adversarial diffusion transformers large language model tokenization"
}
]
@@ -0,0 +1,92 @@
[
{
"name": "greedy story 200",
"tokens": 200,
"repeats": 0,
"drafts_accepted": 91,
"drafts_offered": 190,
"tok_s": 74.0,
"text": "The wind didn\u2019t just blow; it screamed.\n\nElias pulled his wool coat tighter around his shoulders, the fabric stiff with salt and age. He was seventy years old, but he moved with the grace of a man who had spent decades on a narrow spiral staircase. His hands, rough as sandpaper, knew every bolt, every lever, and every creak in the iron bones of the tower.\n\nFor forty years, Elias had been the guardian of the Point. Not just of the light, but of the silence between the waves.\n\nIt was November. The sky was a bruised purple, heavy with rain that hadn\u2019t yet fallen. The sea was a churning cauldron of grey and white, throwing itself against the granite base with a violence that shook the floorboards beneath Elias\u2019s boots.\n\nHe climbed the stairs. One hundred and twelve steps. He counted them by feel, his left hand trailing along the cold iron railing. At the top, the air was"
},
{
"name": "greedy code 200",
"tokens": 200,
"repeats": 0,
"drafts_accepted": 137,
"drafts_offered": 178,
"tok_s": 103.4,
"text": "```python\nimport csv\nfrom typing import List, Dict, Any\n\n\ndef parse_csv_to_dicts(filepath: str) -> List[Dict[str, Any]]:\n \"\"\"\n Parse a CSV file into a list of dictionaries.\n \n Each dictionary represents a row, with keys from the header and values from the row.\n \n Args:\n filepath: Path to the CSV file.\n \n Returns:\n A list of dictionaries, one per row (excluding the header).\n \"\"\"\n result: List[Dict[str, Any]] = []\n with open(filepath, 'r', newline='', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n for row in reader:\n result.append(dict(row))\n return result\n\n\n# Tests\ndef test_parse_csv_to_dicts():\n import tempfile\n import os\n \n # Create a temporary CSV file\n with tempfile.NamedTemporaryFile"
},
{
"name": "greedy sanity",
"tokens": 4,
"repeats": 0,
"drafts_accepted": 3,
"drafts_offered": 3,
"tok_s": 91.1,
"text": "144<|im_end|>"
},
{
"name": "greedy, no penalty (sanity)",
"tokens": 4,
"repeats": 0,
"drafts_accepted": 3,
"drafts_offered": 3,
"tok_s": 70.7,
"text": "144<|im_end|>"
},
{
"name": "greedy presence=100 last_n=64",
"tokens": 118,
"repeats": 0,
"drafts_accepted": 38,
"drafts_offered": 108,
"tok_s": 65.4,
"text": "Aardvark \nAlbatross \nAlligator \nAntelope \nBaboon \nBadger \nBeaver \nBear \nBird of Paradise \nBuffalo \nCamel \nCheetah \nChimpanzee \nCrane \nCrocodile \nDeer \nDolphin \nDonkey \nElephant \nFlamingo \nFox \nGiraffe \nGoat \nHippopotamus \nIguana \nJaguar \nKangaroo \nKoala \nLion \nLeopard \nMonkey \nMoose \nOstrich \nOtter\nPanda<|im_end|>"
},
{
"name": "greedy presence=100 last_n=64 story",
"tokens": 57,
"repeats": 0,
"drafts_accepted": 11,
"drafts_offered": 61,
"tok_s": 57.1,
"text": "The wind didn\u2019t just blow; it screamed against the glass of Lantern House 42, rattling like chains in an empty cathedral vaults below Elias\u2019s boots were heavy with salt and exhaustion as he climbed one hundred twelve steps to reach top room where his world began again.<|im_end|>"
},
{
"name": "sampled presence=100 last_n=64 seed 3",
"tokens": 58,
"repeats": 0,
"drafts_accepted": 14,
"drafts_offered": 54,
"tok_s": 52.9,
"text": "The wind didn\u2019t just blow at Cape Harrow; it screamed through the gaps in stone and steel, carrying with them spray from three miles out into churning black water that no longer resembled an ocean so much as abyssal chaos waiting for its prey to surface beneath waves of salt.<|im_end|>"
},
{
"name": "sampled presence=100 last_n=64 seed 11",
"tokens": 59,
"repeats": 0,
"drafts_accepted": 11,
"drafts_offered": 47,
"tok_s": 51.7,
"text": "Lion \nTiger \n\nElephant\\nGiraffe\\Zebra\\rHippopotamus\\tRhinoceros\\vCrocodile\\fAlligator\\bSnake\\aSpider\\u2615CoffeeCup\u2615\ufe0fTeaTime\ud83d\udc09DragonFishWhaleDolph<|im_end|>"
},
{
"name": "greedy frequency=100 last_n=16",
"tokens": 11,
"repeats": 0,
"drafts_accepted": 5,
"drafts_offered": 12,
"tok_s": 73.5,
"text": "1 to 50, separated by commas:<|im_end|>"
},
{
"name": "greedy presence=100 last_n=4096 (8-row windows)",
"tokens": 300,
"repeats": 0,
"drafts_accepted": 43,
"drafts_offered": 126,
"tok_s": 63.4,
"text": "The ocean is not merely water; it has been, and continues to be one with humanity\u2019s most profound narrative arc in human civilization: **the story told by waves**. It serves as both witness (through sedimentary layers)and participant( through currents that carry messages across continents). Its \"history\" spans geological epochs where life first emerged from primordial pools into complex ecosystems today\u2014and cultural histories shaped entirely around its roleas connector between distant lands or barrier separating them forever depending on perspective taken at different times throughout millennia since ancient civilizations began building boats outof logs carved using stone tools until modern container ships carrying billions tons cargo daily cross Atlantic routes once navigated onlyby stars alone under moonlight during nights when sailors feared monsters lurking beneath hulls made fragile against storms they could barely predict even now despite advanced meteorology systems developed over past century after industrial revolution transformed maritime trade patterns globally due economic pressures driving demand for faster logistics chains linking factories producing electronics manufactured inside cities located far inland near rivers feeding estuaries opening directly onto open seas connected via canals engineered centuries ago like Suez Canal built originallyfor military purposes later adapted commercial use post-World War II era reshaping global supply networks dramatically because geopolitical shifts altered which ports remained accessible politically versus physically blocked temporarily then reopened permanently following treaty agreements signed among nations agreeing upon shared responsibilities regarding environmental stewardship including coral reef restoration efforts funded jointly international organizations coordinating scientific research conducted aboard vessels specifically designed deep-sea exploration equipped robotic submersibles capable descending trenches deeper than any previous technology allowed"
}
]
@@ -0,0 +1,92 @@
[
{
"name": "greedy story 200",
"tokens": 200,
"repeats": 0,
"drafts_accepted": 98,
"drafts_offered": 166,
"tok_s": 54.4,
"text": "The wind didn\u2019t just blow; it screamed. It tore through the iron gallery of the Blackwood Point Lighthouse, rattling the glass panes like a prisoner shaking its bars.\n\nElias Thorne was seventy years old, and he had been the keeper for forty of them. He knew every creak in the spiral staircase, every draft that slipped under the door, and every shift in the weather that the barometer couldn\u2019t quite predict. He knew that when the fog rolled in thick enough to swallow the horizon, the world ceased to exist. There was only the light, the stone, and him.\n\nIt was November, the month of storms. The sea was a churning cauldron of grey and white, throwing itself against the granite base with a violence that made the floorboards tremble. Elias stood by the lantern room, his hands wrapped around a mug of coffee that had gone cold hours ago. He wasn\u2019t drinking it. He was holding it for warmth."
},
{
"name": "greedy code 200",
"tokens": 200,
"repeats": 0,
"drafts_accepted": 137,
"drafts_offered": 163,
"tok_s": 70.7,
"text": "```python\nimport csv\nfrom typing import List, Dict, Any\n\n\ndef parse_csv(file_path: str) -> List[Dict[str, Any]]:\n \"\"\"\n Parse a CSV file into a list of dictionaries.\n \n Each dictionary represents a row, with keys being the header values\n and values being the corresponding cell values.\n \n Args:\n file_path: Path to the CSV file.\n \n Returns:\n A list of dictionaries, one per row (excluding the header).\n \"\"\"\n result = []\n with open(file_path, 'r', newline='', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n for row in reader:\n result.append(dict(row))\n return result\n\n\n# Tests\ndef test_parse_csv_basic():\n \"\"\"Test basic CSV parsing with simple data.\"\"\"\n import tempfile\n import os\n \n # Create a temporary CSV"
},
{
"name": "greedy sanity",
"tokens": 4,
"repeats": 0,
"drafts_accepted": 3,
"drafts_offered": 3,
"tok_s": 56.7,
"text": "144<|im_end|>"
},
{
"name": "greedy, no penalty (sanity)",
"tokens": 4,
"repeats": 0,
"drafts_accepted": 3,
"drafts_offered": 3,
"tok_s": 57.6,
"text": "144<|im_end|>"
},
{
"name": "greedy presence=100 last_n=64",
"tokens": 59,
"repeats": 0,
"drafts_accepted": 13,
"drafts_offered": 47,
"tok_s": 39.7,
"text": "Aardvark \nAlbatross \n\nAlligator \nAntelope \nBaboon \nBadger \nBeaver \nBear \nBird \nBuffalo \n\nCat \nCheetah \n\nChimpanzee \nClam \n\n\nCrocodile \n\n\n\nDolphin \n\n\n\n\nDonkey <|im_end|>"
},
{
"name": "greedy presence=100 last_n=64 story",
"tokens": 57,
"repeats": 0,
"drafts_accepted": 12,
"drafts_offered": 58,
"tok_s": 39.9,
"text": "The wind didn\u2019t just blow; it screamed against the glass of Lantern House 42, rattling like chains in an empty cathedral vaults below Elias\u2019s boots were heavy with salt and exhaustion as he climbed one hundred twelve steps to reach topmost gallery where his world began.<|im_end|>"
},
{
"name": "sampled presence=100 last_n=64 seed 3",
"tokens": 160,
"repeats": 0,
"drafts_accepted": 30,
"drafts_offered": 111,
"tok_s": 34.7,
"text": "The wind didn\u2019t just blow at Blackwood Rock; it screamed through the gaps in his stone cottage like an angry ghost trying to get inside, yet finding no door left unlocked because Elias never let one stay that way for more than three seconds after entering or leaving during storm season. He had been doing this since 1987\u2014since before he even knew why\u2014and now it was muscle memory so deep into his bones and fingers\u2019 calloused skin texture patterns worn thin by years of gripping the same iron railing at midnight watch that if you asked him what time exactly, down to quarter-hour increments when fog rolled in thick enough where light beams bent around itself like old rope coiling tight on a winch drum spooling out slowly from below deck floor level storage locker number seven under stairs leading"
},
{
"name": "sampled presence=100 last_n=64 seed 11",
"tokens": 96,
"repeats": 0,
"drafts_accepted": 31,
"drafts_offered": 48,
"tok_s": 42.3,
"text": "Lion \nTiger \nElephant \nGiraffe \nZebra \nPanda \nKoala \nKangaroo \nCheetah \nHippopotamus \nRhinoceros \nLeopard \nJaguar \nWolf \nFox \nBear \nDeer \nOtter \nBeaver \nBadger \nMongoose \nMeerkat \nChimpanzee \nGorilla \nOrangetutan \nAntelope \nBison \nElk <|im_end|>"
},
{
"name": "greedy frequency=100 last_n=16",
"tokens": 11,
"repeats": 0,
"drafts_accepted": 5,
"drafts_offered": 13,
"tok_s": 43.0,
"text": "1 to 50, separated by commas:<|im_end|>"
},
{
"name": "greedy presence=100 last_n=4096 (8-row windows)",
"tokens": 300,
"repeats": 0,
"drafts_accepted": 29,
"drafts_offered": 127,
"tok_s": 37.6,
"text": "The ocean is not merely water; it has been, and remains to this day one giant machine for making life possible on Earth.\u201d \u2014 *David Attenborough* \n\nThis quote captures perhaps more than any other single sentence in human language: that which we call \u201cthe Sea\u201d or \"Ocean\" (terms often used interchangeably but distinct geographically) was never just an element within our planet\u2019s geography\u2014it became its engine itself\u2014a living system whose existence predates humanity by billions years yet continues shaping us today through climate regulation oxygen production food supply trade routes cultural identity ecological resilience economic development political boundaries scientific discovery technological innovation environmental crisis conservation efforts historical memory philosophical reflection poetic metaphor musical composition cinematic storytelling literary narrative visual art architectural inspiration culinary tradition spiritual symbolism psychological influence sociological transformation educational foundation medical necessity agricultural dependency industrial revolution urban planning infrastructural engineering legal framework regulatory policy international cooperation global governance planetary stewardship existential risk mitigation sustainable practice ethical consideration moral imperative social justice democratic participation civic engagement public health community welfare individual responsibility personal accountability collective action shared experience common understanding mutual respect reciprocal trust collaborative effort cooperative endeavor joint venture unified mission integrated strategy coordinated plan synchronized schedule aligned timeline concurrent execution simultaneous operation parallel processing sequential progression iterative refinement recursive improvement continuous adaptation dynamic adjustment flexible response adaptive behavior responsive conduct proactive initiative anticipatory foresight predictive modeling probabilistic assessment statistical analysis empirical observation experimental verification theoretical hypothesis conceptual abstraction logical deduction rational inference critical evaluation analytical reasoning systematic investigation methodical inquiry structured exploration comprehensive examination detailed scrutiny meticulous inspection thorough review exhaustive audit complete survey full census total enumeration absolute"
}
]
@@ -0,0 +1,92 @@
[
{
"name": "greedy story 200",
"tokens": 200,
"repeats": 0,
"drafts_accepted": 88,
"drafts_offered": 187,
"tok_s": 59.2,
"text": "The wind didn\u2019t just blow; it screamed.\n\nElias pulled his wool coat tighter around his shoulders, the fabric stiff with salt and age. He was seventy years old, but he moved with the grace of a man who had spent decades on a narrow spiral staircase. His hands, mapped with veins like the roots of an ancient oak, gripped the cold iron railing.\n\nFor forty years, Elias had tended the light at the end of the world. Not the end of the map, but the end of the land. Here, the Atlantic threw itself against the granite cliffs with such violence that the spray reached the gallery, thirty feet above the rocks.\n\nHe checked the clock. 11:42 PM. The fog was thickening, a gray soup that swallowed the horizon. In the old days, he would have climbed the stairs to the lantern room, polished the Fresnel lens, and wound the clockwork mechanism that turned the great beam. But the automation had changed"
},
{
"name": "greedy code 200",
"tokens": 200,
"repeats": 0,
"drafts_accepted": 135,
"drafts_offered": 179,
"tok_s": 81.6,
"text": "```python\nimport csv\nfrom typing import List, Dict, Any\n\n\ndef parse_csv_to_dicts(filepath: str) -> List[Dict[str, Any]]:\n \"\"\"\n Parse a CSV file into a list of dictionaries.\n \n Each dictionary represents a row, with keys from the header and values from the row.\n \n Args:\n filepath: Path to the CSV file.\n \n Returns:\n A list of dictionaries, one per row (excluding the header).\n \"\"\"\n result: List[Dict[str, Any]] = []\n with open(filepath, 'r', newline='', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n for row in reader:\n result.append(dict(row))\n return result\n\n\n# Tests\ndef test_parse_csv_to_dicts():\n import tempfile\n import os\n \n # Create a temporary CSV file\n with tempfile.NamedTemporaryFile"
},
{
"name": "greedy sanity",
"tokens": 4,
"repeats": 0,
"drafts_accepted": 3,
"drafts_offered": 3,
"tok_s": 74.9,
"text": "144<|im_end|>"
},
{
"name": "greedy, no penalty (sanity)",
"tokens": 4,
"repeats": 0,
"drafts_accepted": 3,
"drafts_offered": 3,
"tok_s": 74.3,
"text": "144<|im_end|>"
},
{
"name": "greedy presence=100 last_n=64",
"tokens": 61,
"repeats": 0,
"drafts_accepted": 19,
"drafts_offered": 49,
"tok_s": 49.6,
"text": "Aardvark \nAlbatross \n\nAlligator \nAntelope \nBaboon \nBadger \nBeaver \nBear \nBird \nBoar \nBuffalo \nButterfly \n\nCamel \nCat \nCheetah \n\nDolphin \n\n\nElephant \n\n\n\nFlamingo<|im_end|>"
},
{
"name": "greedy presence=100 last_n=64 story",
"tokens": 80,
"repeats": 0,
"drafts_accepted": 6,
"drafts_offered": 52,
"tok_s": 44.9,
"text": "The wind didn\u2019t just blow; it screamed against the glass of Lantern House 42, rattling like chains in an empty cathedral hall outside my window at midnight on November third this year\u2014so I pulled up to check what was happening with that storm and why they were calling me out there when all we had left for tonight\u2019s shift is one last pass through our logs before dawn breaks.<|im_end|>"
},
{
"name": "sampled presence=100 last_n=64 seed 3",
"tokens": 160,
"repeats": 0,
"drafts_accepted": 17,
"drafts_offered": 83,
"tok_s": 44.1,
"text": "The wind didn\u2019t just blow at Cape Harrow; it screamed through the gaps in its teeth, hungry and relentless for three centuries of stone salt-spray history that had long since forgotten to be polite or gentle with itself against anything smaller than God Himself was willing acknowledge as divine protection from drowning souls who were already lost before they ever touched water.\n\nElias knew this rhythm better he himself could hear it sometimes; his boots worn smooth by pacing the spiral stair\u2019s center-post groove where thousands upon countless steps had rubbed away texture until all that remained beneath sole leather now felt like polished bone carved out slowly but surely over time into something more hollow than wood ever really should be allowed become when used so often enough within such confined circular spaces meant only for vertical ascent down always descending again back upward never truly leaving"
},
{
"name": "sampled presence=100 last_n=64 seed 11",
"tokens": 62,
"repeats": 0,
"drafts_accepted": 18,
"drafts_offered": 31,
"tok_s": 49.5,
"text": "Lion \nTiger \n\nElephant \n \nGiraffe \n\n\n\nHippopotamus \nZebra \nRhinoceros \n\n\nPanda Bear \n\n\n\n\nKangaroo \nKoala \nSloth \n\n\n\n\n\nCheetah \nLeopard \nPanther \n\n\n\n\n\n\nWolf \nFox \nBear <|im_end|>"
},
{
"name": "greedy frequency=100 last_n=16",
"tokens": 11,
"repeats": 0,
"drafts_accepted": 5,
"drafts_offered": 13,
"tok_s": 65.2,
"text": "1 to 50, separated by commas:<|im_end|>"
},
{
"name": "greedy presence=100 last_n=4096 (8-row windows)",
"tokens": 300,
"repeats": 0,
"drafts_accepted": 47,
"drafts_offered": 153,
"tok_s": 52.5,
"text": "The ocean is not merely water; it has been, and remains to this day one giant machine for making life possible on Earth.\" \u2014 *David Attenborough* \n\nThis quote captures perhaps more than any other single sentence in human language: that which we call \"the Sea\" or \u201cOcean\u201d was never just an element within our planet\u2019s geography but rather its most fundamental biological engine\u2014a living system whose existence predates humanity by billions years yet continues shaping every aspect from climate regulation through evolutionary biology all way down modern industrial supply chains today's global shipping lanes carry cargo across vast distances because they were once crossed only slowly via coastal hopping routes used since antiquity until steamships revolutionized travel during late nineteenth century when diesel engines replaced wind power entirely allowing container ships carrying thousands TEUs (Twenty-foot Equivalent Units) per voyage crossing entire Pacific basins without stopping at intermediate ports anymore unlike earlier sailing vessels needing favorable winds currents seasonal patterns dictating departure windows based upon monsoon calendars established centuries ago before technological advances changed those constraints fundamentally altering logistics networks worldwide thereby transforming economic structures globally while simultaneously creating ecological disruptions due invasive species transported unintentionally alongside legitimate goods causing cascading ecosystem collapses locally where non-native organisms outcompete native fauna disrupting food webs leading population declines affecting biodiversity indices measured scientifically using standardized metrics like Shannon-Wiener diversity index calculations applied consistently over decades revealing trends showing significant decreases post-introduction events correl strongly with introduction dates matching historical records documenting first sightings confirmed taxonomically classified formally according International Code Nomenclature rules"
}
]
@@ -0,0 +1,92 @@
[
{
"name": "greedy story 200",
"tokens": 200,
"repeats": 0,
"drafts_accepted": 88,
"drafts_offered": 187,
"tok_s": 58.4,
"text": "The wind didn\u2019t just blow; it screamed.\n\nElias pulled his wool coat tighter around his shoulders, the fabric stiff with salt and age. He was seventy years old, but he moved with the grace of a man who had spent decades on a narrow spiral staircase. His hands, mapped with veins like the roots of an ancient oak, gripped the cold iron railing.\n\nFor forty years, Elias had tended the light at the end of the world. Not the end of the map, but the end of the land. Here, the Atlantic threw itself against the granite cliffs with such violence that the spray reached the gallery, thirty feet above the rocks.\n\nHe checked the clock. 11:42 PM. The fog was thickening, a gray soup that swallowed the horizon. In the old days, he would have climbed the stairs to the lantern room, polished the Fresnel lens, and wound the clockwork mechanism that turned the great beam. But the automation had changed"
},
{
"name": "greedy code 200",
"tokens": 200,
"repeats": 0,
"drafts_accepted": 135,
"drafts_offered": 179,
"tok_s": 80.6,
"text": "```python\nimport csv\nfrom typing import List, Dict, Any\n\n\ndef parse_csv_to_dicts(filepath: str) -> List[Dict[str, Any]]:\n \"\"\"\n Parse a CSV file into a list of dictionaries.\n \n Each dictionary represents a row, with keys from the header and values from the row.\n \n Args:\n filepath: Path to the CSV file.\n \n Returns:\n A list of dictionaries, one per row (excluding the header).\n \"\"\"\n result: List[Dict[str, Any]] = []\n with open(filepath, 'r', newline='', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n for row in reader:\n result.append(dict(row))\n return result\n\n\n# Tests\ndef test_parse_csv_to_dicts():\n import tempfile\n import os\n \n # Create a temporary CSV file\n with tempfile.NamedTemporaryFile"
},
{
"name": "greedy sanity",
"tokens": 4,
"repeats": 0,
"drafts_accepted": 3,
"drafts_offered": 3,
"tok_s": 75.2,
"text": "144<|im_end|>"
},
{
"name": "greedy, no penalty (sanity)",
"tokens": 4,
"repeats": 0,
"drafts_accepted": 3,
"drafts_offered": 3,
"tok_s": 74.2,
"text": "144<|im_end|>"
},
{
"name": "greedy presence=100 last_n=64",
"tokens": 61,
"repeats": 0,
"drafts_accepted": 19,
"drafts_offered": 49,
"tok_s": 48.5,
"text": "Aardvark \nAlbatross \n\nAlligator \nAntelope \nBaboon \nBadger \nBeaver \nBear \nBird \nBoar \nBuffalo \nButterfly \n\nCamel \nCat \nCheetah \n\nDolphin \n\n\nElephant \n\n\n\nFlamingo<|im_end|>"
},
{
"name": "greedy presence=100 last_n=64 story",
"tokens": 80,
"repeats": 0,
"drafts_accepted": 6,
"drafts_offered": 52,
"tok_s": 46.4,
"text": "The wind didn\u2019t just blow; it screamed against the glass of Lantern House 42, rattling like chains in an empty cathedral hall outside my window at midnight on November third this year\u2014so I pulled up to check what was happening with that storm and why they were calling me out there when all we had left for tonight\u2019s shift is one last pass through our logs before dawn breaks.<|im_end|>"
},
{
"name": "sampled presence=100 last_n=64 seed 3",
"tokens": 160,
"repeats": 0,
"drafts_accepted": 17,
"drafts_offered": 83,
"tok_s": 43.3,
"text": "The wind didn\u2019t just blow at Cape Harrow; it screamed through the gaps in its teeth, hungry and relentless for three centuries of stone salt-spray history that had long since forgotten to be polite or gentle with itself against anything smaller than God Himself was willing acknowledge as divine protection from drowning souls who were already lost before they ever touched water.\n\nElias knew this rhythm better he himself could hear it sometimes; his boots worn smooth by pacing the spiral stair\u2019s center-post groove where thousands upon countless steps had rubbed away texture until all that remained beneath sole leather now felt like polished bone carved out slowly but surely over time into something more hollow than wood ever really should be allowed become when used so often enough within such confined circular spaces meant only for vertical ascent down always descending again back upward never truly leaving"
},
{
"name": "sampled presence=100 last_n=64 seed 11",
"tokens": 62,
"repeats": 0,
"drafts_accepted": 18,
"drafts_offered": 31,
"tok_s": 49.8,
"text": "Lion \nTiger \n\nElephant \n \nGiraffe \n\n\n\nHippopotamus \nZebra \nRhinoceros \n\n\nPanda Bear \n\n\n\n\nKangaroo \nKoala \nSloth \n\n\n\n\n\nCheetah \nLeopard \nPanther \n\n\n\n\n\n\nWolf \nFox \nBear <|im_end|>"
},
{
"name": "greedy frequency=100 last_n=16",
"tokens": 11,
"repeats": 0,
"drafts_accepted": 5,
"drafts_offered": 13,
"tok_s": 62.5,
"text": "1 to 50, separated by commas:<|im_end|>"
},
{
"name": "greedy presence=100 last_n=4096 (8-row windows)",
"tokens": 300,
"repeats": 0,
"drafts_accepted": 47,
"drafts_offered": 153,
"tok_s": 52.6,
"text": "The ocean is not merely water; it has been, and remains to this day one giant machine for making life possible on Earth.\" \u2014 *David Attenborough* \n\nThis quote captures perhaps more than any other single sentence in human language: that which we call \"the Sea\" or \u201cOcean\u201d was never just an element within our planet\u2019s geography but rather its most fundamental biological engine\u2014a living system whose existence predates humanity by billions years yet continues shaping every aspect from climate regulation through evolutionary biology all way down modern industrial supply chains today's global shipping lanes carry cargo across vast distances because they were once crossed only slowly via coastal hopping routes used since antiquity until steamships revolutionized travel during late nineteenth century when diesel engines replaced wind power entirely allowing container ships carrying thousands TEUs (Twenty-foot Equivalent Units) per voyage crossing entire Pacific basins without stopping at intermediate ports anymore unlike earlier sailing vessels needing favorable winds currents seasonal patterns dictating departure windows based upon monsoon calendars established centuries ago before technological advances changed those constraints fundamentally altering logistics networks worldwide thereby transforming economic structures globally while simultaneously creating ecological disruptions due invasive species transported unintentionally alongside legitimate goods causing cascading ecosystem collapses locally where non-native organisms outcompete native fauna disrupting food webs leading population declines affecting biodiversity indices measured scientifically using standardized metrics like Shannon-Wiener diversity index calculations applied consistently over decades revealing trends showing significant decreases post-introduction events correl strongly with introduction dates matching historical records documenting first sightings confirmed taxonomically classified formally according International Code Nomenclature rules"
}
]
@@ -0,0 +1,92 @@
[
{
"name": "greedy story 200",
"tokens": 200,
"repeats": 0,
"drafts_accepted": 88,
"drafts_offered": 187,
"tok_s": 58.4,
"text": "The wind didn\u2019t just blow; it screamed.\n\nElias pulled his wool coat tighter around his shoulders, the fabric stiff with salt and age. He was seventy years old, but he moved with the grace of a man who had spent decades on a narrow spiral staircase. His hands, mapped with veins like the roots of an ancient oak, gripped the cold iron railing.\n\nFor forty years, Elias had tended the light at the end of the world. Not the end of the map, but the end of the land. Here, the Atlantic threw itself against the granite cliffs with such violence that the spray reached the gallery, thirty feet above the rocks.\n\nHe checked the clock. 11:42 PM. The fog was thickening, a gray soup that swallowed the horizon. In the old days, he would have climbed the stairs to the lantern room, polished the Fresnel lens, and wound the clockwork mechanism that turned the great beam. But the automation had changed"
},
{
"name": "greedy code 200",
"tokens": 200,
"repeats": 0,
"drafts_accepted": 135,
"drafts_offered": 179,
"tok_s": 80.5,
"text": "```python\nimport csv\nfrom typing import List, Dict, Any\n\n\ndef parse_csv_to_dicts(filepath: str) -> List[Dict[str, Any]]:\n \"\"\"\n Parse a CSV file into a list of dictionaries.\n \n Each dictionary represents a row, with keys from the header and values from the row.\n \n Args:\n filepath: Path to the CSV file.\n \n Returns:\n A list of dictionaries, one per row (excluding the header).\n \"\"\"\n result: List[Dict[str, Any]] = []\n with open(filepath, 'r', newline='', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n for row in reader:\n result.append(dict(row))\n return result\n\n\n# Tests\ndef test_parse_csv_to_dicts():\n import tempfile\n import os\n \n # Create a temporary CSV file\n with tempfile.NamedTemporaryFile"
},
{
"name": "greedy sanity",
"tokens": 4,
"repeats": 0,
"drafts_accepted": 3,
"drafts_offered": 3,
"tok_s": 74.5,
"text": "144<|im_end|>"
},
{
"name": "greedy, no penalty (sanity)",
"tokens": 4,
"repeats": 0,
"drafts_accepted": 3,
"drafts_offered": 3,
"tok_s": 76.8,
"text": "144<|im_end|>"
},
{
"name": "greedy presence=100 last_n=64",
"tokens": 61,
"repeats": 0,
"drafts_accepted": 19,
"drafts_offered": 49,
"tok_s": 49.6,
"text": "Aardvark \nAlbatross \n\nAlligator \nAntelope \nBaboon \nBadger \nBeaver \nBear \nBird \nBoar \nBuffalo \nButterfly \n\nCamel \nCat \nCheetah \n\nDolphin \n\n\nElephant \n\n\n\nFlamingo<|im_end|>"
},
{
"name": "greedy presence=100 last_n=64 story",
"tokens": 80,
"repeats": 0,
"drafts_accepted": 6,
"drafts_offered": 52,
"tok_s": 46.7,
"text": "The wind didn\u2019t just blow; it screamed against the glass of Lantern House 42, rattling like chains in an empty cathedral hall outside my window at midnight on November third this year\u2014so I pulled up to check what was happening with that storm and why they were calling me out there when all we had left for tonight\u2019s shift is one last pass through our logs before dawn breaks.<|im_end|>"
},
{
"name": "sampled presence=100 last_n=64 seed 3",
"tokens": 160,
"repeats": 0,
"drafts_accepted": 17,
"drafts_offered": 83,
"tok_s": 44.3,
"text": "The wind didn\u2019t just blow at Cape Harrow; it screamed through the gaps in its teeth, hungry and relentless for three centuries of stone salt-spray history that had long since forgotten to be polite or gentle with itself against anything smaller than God Himself was willing acknowledge as divine protection from drowning souls who were already lost before they ever touched water.\n\nElias knew this rhythm better he himself could hear it sometimes; his boots worn smooth by pacing the spiral stair\u2019s center-post groove where thousands upon countless steps had rubbed away texture until all that remained beneath sole leather now felt like polished bone carved out slowly but surely over time into something more hollow than wood ever really should be allowed become when used so often enough within such confined circular spaces meant only for vertical ascent down always descending again back upward never truly leaving"
},
{
"name": "sampled presence=100 last_n=64 seed 11",
"tokens": 62,
"repeats": 0,
"drafts_accepted": 18,
"drafts_offered": 31,
"tok_s": 50.0,
"text": "Lion \nTiger \n\nElephant \n \nGiraffe \n\n\n\nHippopotamus \nZebra \nRhinoceros \n\n\nPanda Bear \n\n\n\n\nKangaroo \nKoala \nSloth \n\n\n\n\n\nCheetah \nLeopard \nPanther \n\n\n\n\n\n\nWolf \nFox \nBear <|im_end|>"
},
{
"name": "greedy frequency=100 last_n=16",
"tokens": 11,
"repeats": 0,
"drafts_accepted": 5,
"drafts_offered": 13,
"tok_s": 62.0,
"text": "1 to 50, separated by commas:<|im_end|>"
},
{
"name": "greedy presence=100 last_n=4096 (8-row windows)",
"tokens": 300,
"repeats": 0,
"drafts_accepted": 47,
"drafts_offered": 153,
"tok_s": 52.1,
"text": "The ocean is not merely water; it has been, and remains to this day one giant machine for making life possible on Earth.\" \u2014 *David Attenborough* \n\nThis quote captures perhaps more than any other single sentence in human language: that which we call \"the Sea\" or \u201cOcean\u201d was never just an element within our planet\u2019s geography but rather its most fundamental biological engine\u2014a living system whose existence predates humanity by billions years yet continues shaping every aspect from climate regulation through evolutionary biology all way down modern industrial supply chains today's global shipping lanes carry cargo across vast distances because they were once crossed only slowly via coastal hopping routes used since antiquity until steamships revolutionized travel during late nineteenth century when diesel engines replaced wind power entirely allowing container ships carrying thousands TEUs (Twenty-foot Equivalent Units) per voyage crossing entire Pacific basins without stopping at intermediate ports anymore unlike earlier sailing vessels needing favorable winds currents seasonal patterns dictating departure windows based upon monsoon calendars established centuries ago before technological advances changed those constraints fundamentally altering logistics networks worldwide thereby transforming economic structures globally while simultaneously creating ecological disruptions due invasive species transported unintentionally alongside legitimate goods causing cascading ecosystem collapses locally where non-native organisms outcompete native fauna disrupting food webs leading population declines affecting biodiversity indices measured scientifically using standardized metrics like Shannon-Wiener diversity index calculations applied consistently over decades revealing trends showing significant decreases post-introduction events correl strongly with introduction dates matching historical records documenting first sightings confirmed taxonomically classified formally according International Code Nomenclature rules"
}
]
@@ -0,0 +1,72 @@
{
"label": "dev-3090",
"extra": [],
"gpus": "2",
"needles": [
{
"tokens": 2726,
"prompt_ms": 3424.8,
"pp_tok_s": 796.0,
"found": true,
"answer": "meadow-254<|im_end|>"
},
{
"tokens": 8379,
"prompt_ms": 8617.9,
"pp_tok_s": 972.3,
"found": true,
"answer": "juniper-766<|im_end|>"
},
{
"tokens": 16066,
"prompt_ms": 12820.1,
"pp_tok_s": 1253.2,
"found": true,
"answer": "falcon-174<|im_end|>"
},
{
"tokens": 27743,
"prompt_ms": 22901.1,
"pp_tok_s": 1211.4,
"found": true,
"answer": "willow-474<|im_end|>"
}
],
"chat": {
"turn1_tokens": 6584,
"turn1_ms": 5657.8,
"turn2_tokens": 6660,
"reused": 6631,
"turn2_ms": 238.7,
"answer": "lantern-417<|im_end|>",
"ok": true
},
"decode": [
{
"name": "story",
"tok_s": 71.7,
"hit_rate": 0.9266798230257504,
"text": "The wind didn\u2019t just blow; it screamed.\n\nElias had lived in the tower for thirty years, and he knew the difference between a gale and a hurricane by the way the glass rattled in its frame. Tonight, it was the latter. The sea was a churning black void, punctuated by whitecaps that looked like teeth.\n\nHe climbed the spiral stairs, his knees protesting with every step. The iron rungs were cold, slick with condensation that had leaked through the hairline cracks in the masonry. At the top, the lantern room hummed with the low, steady thrum of the rotating lens. It was a sound Elias had learned to sleep with, a heartbeat of the house.\n\nHe checked the clock. 11:42 PM. The light swept across the water, a slow, hypnotic arc. *Flash. Darkness. Flash. Darkness.*\n\nFor a moment, the beam caught something that wasn\u2019t water. A shape"
},
{
"name": "code",
"tok_s": 92.9,
"hit_rate": 0.8636411400016346,
"text": "```python\nimport csv\nfrom typing import List, Dict\n\n\ndef parse_csv(filepath: str) -> List[Dict[str, str]]:\n \"\"\"\n Parse a CSV file into a list of dictionaries.\n \n Each dictionary represents a row, with keys from the header row\n and values from the corresponding row.\n \n Args:\n filepath: Path to the CSV file.\n \n Returns:\n A list of dictionaries, one per row (excluding the header).\n \"\"\"\n result = []\n with open(filepath, 'r', newline='') as f:\n reader = csv.DictReader(f)\n for row in reader:\n result.append(dict(row))\n return result\n```<|im_end|>"
}
],
"engine_lines": [
"strata generate: expert cache auto: 17.31 GiB free, 700 MiB reserved -> 6704 slots",
"strata generate: expert cache 8730 slots, 16.62 GiB of VRAM; policy is",
"strata serve: the prompt path borrows 2315 cache slots (4.42 GiB)",
"strata serve: decode expert cache hit rate: 86.6% (4986 hits / 5756 lookups)",
"strata serve: decode expert cache hit rate: 87.1% (3757 hits / 4315 lookups)",
"strata serve: decode expert cache hit rate: 88.7% (3827 hits / 4314 lookups)",
"strata serve: decode expert cache hit rate: 92.1% (3975 hits / 4318 lookups)",
"strata serve: decode expert cache hit rate: 95.5% (4124 hits / 4320 lookups)",
"strata serve: decode expert cache hit rate: 94.3% (25793 hits / 27360 lookups)",
"strata serve: decode expert cache hit rate: 94.6% (4088 hits / 4320 lookups)",
"strata serve: decode expert cache hit rate: 92.7% (116454 hits / 125668 lookups)",
"strata serve: decode expert cache hit rate: 86.4% (73970 hits / 85649 lookups)"
]
}
@@ -0,0 +1,72 @@
{
"label": "dev-5080",
"extra": [],
"gpus": "0",
"needles": [
{
"tokens": 2726,
"prompt_ms": 2129.8,
"pp_tok_s": 1279.9,
"found": true,
"answer": "meadow-254<|im_end|>"
},
{
"tokens": 8379,
"prompt_ms": 5057.2,
"pp_tok_s": 1656.8,
"found": true,
"answer": "juniper-766<|im_end|>"
},
{
"tokens": 16066,
"prompt_ms": 7964.7,
"pp_tok_s": 2017.2,
"found": true,
"answer": "falcon-174<|im_end|>"
},
{
"tokens": 27743,
"prompt_ms": 14053.6,
"pp_tok_s": 1974.1,
"found": true,
"answer": "willow-474<|im_end|>"
}
],
"chat": {
"turn1_tokens": 6584,
"turn1_ms": 3366.4,
"turn2_tokens": 6660,
"reused": 6632,
"turn2_ms": 197.3,
"answer": "lantern-417<|im_end|>",
"ok": true
},
"decode": [
{
"name": "story",
"tok_s": 83.1,
"hit_rate": 0.8418683557684931,
"text": "The wind didn\u2019t just blow; it screamed.\n\nElias had lived in the tower for thirty years, and he knew the difference between a gale and a hurricane by the way the glass rattled in its frame. Tonight, it was the latter. The sea was a churning cauldron of black ink and white foam, throwing itself against the granite base with the force of a battering ram.\n\nInside the lantern room, the air was thick with the smell of kerosene and old brass. Elias wiped his hands on his woolen trousers, leaving faint, oily smears on the fabric. He checked the clock. 11:42 PM. The rotation was due.\n\nHe reached for the crank handle, a heavy iron wheel worn smooth by decades of his own palms. With a grunt that matched the strain of his shoulders, he began to turn it. *Click. Click. Click.* The gears engaged, and the massive Fresnel lens began its slow"
},
{
"name": "code",
"tok_s": 88.0,
"hit_rate": 0.7011385968111186,
"text": "```python\nimport csv\nfrom typing import List, Dict, Any\n\n\ndef parse_csv_to_dicts(filepath: str) -> List[Dict[str, Any]]:\n \"\"\"\n Parse a CSV file into a list of dictionaries.\n \n Each dictionary represents a row, with keys from the header and values from the row.\n \n Args:\n filepath: Path to the CSV file.\n \n Returns:\n A list of dictionaries, one per row (excluding the header).\n \"\"\"\n result: List[Dict[str, Any]] = []\n with open(filepath, 'r', newline='', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n for row in reader:\n result.append(dict(row))\n return result\n\n\n# Tests\ndef test_parse_csv_to_dicts():\n import tempfile\n import os\n \n # Create a temporary CSV file\n with tempfile.NamedTemporaryFile"
}
],
"engine_lines": [
"strata generate: expert cache auto: 9.15 GiB free, 700 MiB reserved -> 3415 slots",
"strata generate: expert cache 4293 slots, 8.17 GiB of VRAM; policy is",
"strata serve: the prompt path borrows 2343 cache slots (4.42 GiB)",
"strata serve: decode expert cache hit rate: 59.6% (2749 hits / 4610 lookups)",
"strata serve: decode expert cache hit rate: 64.2% (2136 hits / 3329 lookups)",
"strata serve: decode expert cache hit rate: 67.3% (2309 hits / 3432 lookups)",
"strata serve: decode expert cache hit rate: 72.5% (2539 hits / 3504 lookups)",
"strata serve: decode expert cache hit rate: 77.3% (2817 hits / 3642 lookups)",
"strata serve: decode expert cache hit rate: 75.3% (16500 hits / 21916 lookups)",
"strata serve: decode expert cache hit rate: 80.0% (2978 hits / 3723 lookups)",
"strata serve: decode expert cache hit rate: 84.2% (98752 hits / 117301 lookups)",
"strata serve: decode expert cache hit rate: 70.1% (69215 hits / 98718 lookups)"
]
}
@@ -0,0 +1,85 @@
{
"label": "ks-k18",
"extra": [
"--layer-split",
"18"
],
"gpus": "0,2",
"needles": [
{
"tokens": 16066,
"prompt_ms": 10383.7,
"pp_tok_s": 1547.2,
"found": true,
"answer": "meadow-254<|im_end|>"
},
{
"tokens": 27743,
"prompt_ms": 16371.9,
"pp_tok_s": 1694.5,
"found": true,
"answer": "juniper-766<|im_end|>"
}
],
"chat": {
"turn1_tokens": 6584,
"turn1_ms": 4308.0,
"turn2_tokens": 6660,
"reused": 6632,
"turn2_ms": 239.8,
"answer": "lantern-417<|im_end|>",
"ok": true
},
"decode": [
{
"name": "story",
"tok_s": 77.2,
"reps": [
74.6,
78.1,
79.0
],
"hit_rate": 0.9985378787878788,
"text": "The wind didn\u2019t just blow; it screamed.\n\nElias had lived in the tower for thirty years, and he knew the difference between a gale and a hurricane by the way the glass rattled in its frame. Tonight, the glass was vibrating like a plucked cello string. The sea, usually a rhythmic pulse against the granite base, was now a chaotic, frothing beast, throwing itself against the rocks with the force of a battering ram.\n\nElias climbed the spiral stairs, his hand gripping the cold iron railing. His knees ached, not from age, but from the constant vibration of the floor beneath his feet. He reached the lantern room, the heart of the lighthouse, and checked the clockwork mechanism. The great lens, a prism of Fresnel glass, rotated with a steady, hypnotic *whirrr-click, whirrr-click*. It was the only sound that mattered. The light swept out into the black void, cutting through"
},
{
"name": "code",
"tok_s": 103.8,
"reps": [
97.6,
106.4,
107.3
],
"hit_rate": 0.9984863945578232,
"text": "```python\nimport csv\nfrom typing import List, Dict, Any\n\n\ndef parse_csv_to_dicts(filepath: str) -> List[Dict[str, Any]]:\n \"\"\"\n Parse a CSV file into a list of dictionaries.\n \n Each dictionary represents a row, with keys from the header and values from the row.\n \n Args:\n filepath: Path to the CSV file.\n \n Returns:\n A list of dictionaries, one per row (excluding the header).\n \"\"\"\n result: List[Dict[str, Any]] = []\n with open(filepath, 'r', newline='', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n for row in reader:\n result.append(dict(row))\n return result\n\n\n# Tests\ndef test_parse_csv_to_dicts():\n import tempfile\n import os\n \n # Create a temporary CSV file\n with tempfile.NamedTemporaryFile"
}
],
"stage_timing": [
"strata serve: stage 0: 524 windows; per window: wait for the GPU 6.456 ms, pool + plan 0.181 ms, host staging 0.581 ms, commit 0.206 ms",
"strata serve: stage 1: 524 windows; per window: wait for the GPU 14.872 ms, pool + plan 0.501 ms, host staging 0.000 ms, commit 0.387 ms",
"strata serve: stage 0: 593 windows; per window: wait for the GPU 6.479 ms, pool + plan 0.185 ms, host staging 0.515 ms, commit 0.207 ms",
"strata serve: stage 1: 593 windows; per window: wait for the GPU 15.053 ms, pool + plan 0.496 ms, host staging 0.000 ms, commit 0.390 ms"
],
"engine_lines": [
"strata generate: layer split across 2 GPUs: CUDA0, then CUDA1 (split 18)",
"strata generate: layer split: CUDA1 PCIe probe 6.4 GB/s -> pcie_frac 0.14",
"strata generate: layer split: CUDA1 holds its weights, session and head; 18.13 GiB free",
"strata generate: expert cache auto: 10.46 GiB free, 700 MiB reserved -> 3345 slots",
"strata generate: expert cache 4608 slots, 8.24 GiB of VRAM; policy is",
"strata generate: layer split: CUDA1 runs layers 18-47, expert cache 7152 slots (14.14 GiB), 7152 of its 7680 profiled pairs; slot 0 verified",
"strata generate: layer split: CUDA0 runs layers 0-17",
"strata serve: the prompt path allocates its own buffers (too few cache slots to borrow)",
"strata serve: layer split: layers 0-17 (CUDA0), 18-47 (CUDA1), one hand-off per window",
"strata serve: decode expert cache hit rate: 98.7% (6159 hits / 6240 lookups)",
"strata serve: decode expert cache hit rate: 98.9% (3796 hits / 3840 lookups)",
"strata serve: decode expert cache hit rate: 99.2% (3811 hits / 3840 lookups)",
"strata serve: decode expert cache hit rate: 99.6% (27723 hits / 27840 lookups)",
"strata serve: decode expert cache hit rate: 99.1% (3807 hits / 3840 lookups)",
"strata serve: decode expert cache hit rate: 99.3% (142002 hits / 143040 lookups)",
"strata serve: decode expert cache hit rate: 99.8% (139882 hits / 140160 lookups)",
"strata serve: decode expert cache hit rate: 99.9% (131807 hits / 132000 lookups)",
"strata serve: decode expert cache hit rate: 99.0% (119278 hits / 120480 lookups)",
"strata serve: decode expert cache hit rate: 99.8% (117410 hits / 117600 lookups)",
"strata serve: decode expert cache hit rate: 99.8% (117422 hits / 117600 lookups)"
]
}
@@ -0,0 +1,85 @@
{
"label": "ks-k19",
"extra": [
"--layer-split",
"19"
],
"gpus": "0,2",
"needles": [
{
"tokens": 16066,
"prompt_ms": 9875.2,
"pp_tok_s": 1626.9,
"found": true,
"answer": "meadow-254<|im_end|>"
},
{
"tokens": 27743,
"prompt_ms": 15498.0,
"pp_tok_s": 1790.1,
"found": true,
"answer": "juniper-766<|im_end|>"
}
],
"chat": {
"turn1_tokens": 6584,
"turn1_ms": 4085.5,
"turn2_tokens": 6660,
"reused": 6632,
"turn2_ms": 238.7,
"answer": "lantern-417<|im_end|>",
"ok": true
},
"decode": [
{
"name": "story",
"tok_s": 79.3,
"reps": [
74.8,
85.6,
77.4
],
"hit_rate": 0.9977757017660928,
"text": "The wind didn\u2019t just blow; it screamed.\n\nElias had lived in the tower for thirty years, and he knew the difference between a gale and a hurricane by the way the glass rattled in its frame. Tonight, the glass was vibrating like a plucked cello string. The sea, usually a rhythmic pulse against the granite base, was now a chaotic, frothing beast, throwing itself against the rocks with the force of a battering ram.\n\nElias stood at the top of the spiral staircase, his hand resting on the cold iron railing. He was not afraid. Fear was for the tourists who came in summer, wide-eyed and clutching their cameras. Elias was a keeper. He was a guardian.\n\nHe checked the logbook. The entry for the night was simple: *Wind, 60 knots. Visibility, zero. Light, operational.*\n\nHe walked down the stairs, his boots heavy on the metal grating. The smell of salt"
},
{
"name": "code",
"tok_s": 107.4,
"reps": [
98.9,
111.3,
112.1
],
"hit_rate": 0.9986286196529353,
"text": "```python\nimport csv\nfrom typing import List, Dict, Any\n\n\ndef parse_csv_to_dicts(filepath: str) -> List[Dict[str, Any]]:\n \"\"\"\n Parse a CSV file into a list of dictionaries.\n \n Each dictionary represents a row, with keys from the header row\n and values from the corresponding row.\n \n Args:\n filepath: Path to the CSV file.\n \n Returns:\n A list of dictionaries, one per row (excluding the header).\n \"\"\"\n result: List[Dict[str, Any]] = []\n with open(filepath, 'r', newline='', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n for row in reader:\n result.append(dict(row))\n return result\n\n\n# Tests\ndef test_parse_csv_to_dicts():\n import tempfile\n import os\n \n # Create a temporary CSV file\n with"
}
],
"stage_timing": [
"strata serve: stage 0: 533 windows; per window: wait for the GPU 6.226 ms, pool + plan 0.294 ms, host staging 0.561 ms, commit 0.225 ms",
"strata serve: stage 1: 533 windows; per window: wait for the GPU 14.156 ms, pool + plan 0.373 ms, host staging 0.000 ms, commit 0.371 ms",
"strata serve: stage 0: 599 windows; per window: wait for the GPU 6.314 ms, pool + plan 0.292 ms, host staging 0.501 ms, commit 0.225 ms",
"strata serve: stage 1: 599 windows; per window: wait for the GPU 14.366 ms, pool + plan 0.375 ms, host staging 0.000 ms, commit 0.374 ms"
],
"engine_lines": [
"strata generate: layer split across 2 GPUs: CUDA0, then CUDA1 (split 19)",
"strata generate: layer split: CUDA1 PCIe probe 6.4 GB/s -> pcie_frac 0.14",
"strata generate: layer split: CUDA1 holds its weights, session and head; 18.13 GiB free",
"strata generate: expert cache auto: 10.46 GiB free, 700 MiB reserved -> 3345 slots",
"strata generate: expert cache 4606 slots, 8.29 GiB of VRAM; policy is",
"strata generate: layer split: CUDA1 runs layers 19-47, expert cache 7160 slots (14.14 GiB), 7160 of its 7424 profiled pairs; slot 0 verified",
"strata generate: layer split: CUDA0 runs layers 0-18",
"strata serve: the prompt path allocates its own buffers (too few cache slots to borrow)",
"strata serve: layer split: layers 0-18 (CUDA0), 19-47 (CUDA1), one hand-off per window",
"strata serve: decode expert cache hit rate: 98.5% (6141 hits / 6234 lookups)",
"strata serve: decode expert cache hit rate: 99.0% (3797 hits / 3836 lookups)",
"strata serve: decode expert cache hit rate: 99.4% (3814 hits / 3837 lookups)",
"strata serve: decode expert cache hit rate: 99.5% (28659 hits / 28795 lookups)",
"strata serve: decode expert cache hit rate: 99.3% (3814 hits / 3839 lookups)",
"strata serve: decode expert cache hit rate: 99.2% (141378 hits / 142508 lookups)",
"strata serve: decode expert cache hit rate: 99.8% (124963 hits / 125273 lookups)",
"strata serve: decode expert cache hit rate: 99.8% (134574 hits / 134874 lookups)",
"strata serve: decode expert cache hit rate: 99.0% (121132 hits / 122322 lookups)",
"strata serve: decode expert cache hit rate: 99.9% (114073 hits / 114235 lookups)",
"strata serve: decode expert cache hit rate: 99.9% (113598 hits / 113754 lookups)"
]
}
@@ -0,0 +1,85 @@
{
"label": "ks-k20",
"extra": [
"--layer-split",
"20"
],
"gpus": "0,2",
"needles": [
{
"tokens": 16066,
"prompt_ms": 9044.3,
"pp_tok_s": 1776.4,
"found": true,
"answer": "meadow-254<|im_end|>"
},
{
"tokens": 27743,
"prompt_ms": 13928.7,
"pp_tok_s": 1991.8,
"found": true,
"answer": "juniper-766<|im_end|>"
}
],
"chat": {
"turn1_tokens": 6584,
"turn1_ms": 3743.5,
"turn2_tokens": 6660,
"reused": 6632,
"turn2_ms": 236.8,
"answer": "lantern-417<|im_end|>",
"ok": true
},
"decode": [
{
"name": "story",
"tok_s": 76.4,
"reps": [
74.2,
78.4,
76.7
],
"hit_rate": 0.9979911611088791,
"text": "The wind didn\u2019t just blow; it screamed. It clawed at the granite base of the tower, trying to pry it from the bedrock, and it howled through the narrow slits of the lantern room glass.\n\nElias Thorne was seventy years old, but he moved with the grace of a man who had spent fifty years listening to the sea. He knew the rhythm of the waves against the rocks below\u2014the *thud, thud, crash*\u2014and he knew when that rhythm changed. When the sea grew too quiet, or too violent, he knew a storm was coming, or a ship was in trouble.\n\nTonight, the storm was a Category 4. The rain lashed against the windows like gravel thrown by a giant. Elias stood in the center of the lantern room, his hand resting on the cold brass railing. He wasn\u2019t afraid. He was a guardian.\n\nHe checked the clockwork mechanism. It was old, a relic from 1"
},
{
"name": "code",
"tok_s": 105.9,
"reps": [
103.2,
106.8,
107.7
],
"hit_rate": 0.9981849807154201,
"text": "```python\nimport csv\nfrom typing import List, Dict, Any\n\n\ndef parse_csv_to_dicts(filepath: str) -> List[Dict[str, Any]]:\n \"\"\"\n Parse a CSV file into a list of dictionaries.\n \n Each dictionary represents a row, with keys from the header and values from the row.\n \n Args:\n filepath: Path to the CSV file.\n \n Returns:\n A list of dictionaries, one per row (excluding the header).\n \"\"\"\n result: List[Dict[str, Any]] = []\n with open(filepath, 'r', newline='', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n for row in reader:\n result.append(dict(row))\n return result\n\n\n# Tests\ndef test_parse_csv_to_dicts():\n import tempfile\n import os\n \n # Create a temporary CSV file\n with tempfile.NamedTemporaryFile"
}
],
"stage_timing": [
"strata serve: stage 0: 537 windows; per window: wait for the GPU 6.685 ms, pool + plan 0.403 ms, host staging 0.593 ms, commit 0.215 ms",
"strata serve: stage 1: 537 windows; per window: wait for the GPU 13.928 ms, pool + plan 0.282 ms, host staging 0.000 ms, commit 0.359 ms",
"strata serve: stage 0: 606 windows; per window: wait for the GPU 6.764 ms, pool + plan 0.399 ms, host staging 0.528 ms, commit 0.216 ms",
"strata serve: stage 1: 606 windows; per window: wait for the GPU 14.108 ms, pool + plan 0.288 ms, host staging 0.000 ms, commit 0.362 ms"
],
"engine_lines": [
"strata generate: layer split across 2 GPUs: CUDA0, then CUDA1 (split 20)",
"strata generate: layer split: CUDA1 PCIe probe 6.4 GB/s -> pcie_frac 0.14",
"strata generate: layer split: CUDA1 holds its weights, session and head; 18.13 GiB free",
"strata generate: expert cache auto: 10.46 GiB free, 700 MiB reserved -> 3345 slots",
"strata generate: expert cache 4603 slots, 8.29 GiB of VRAM; policy is",
"strata generate: layer split: CUDA1 runs layers 20-47, expert cache 7142 slots (14.14 GiB), 7142 of its 7168 profiled pairs; slot 0 verified",
"strata generate: layer split: CUDA0 runs layers 0-19",
"strata serve: the prompt path allocates its own buffers (too few cache slots to borrow)",
"strata serve: layer split: layers 0-19 (CUDA0), 20-47 (CUDA1), one hand-off per window",
"strata serve: decode expert cache hit rate: 98.9% (6162 hits / 6230 lookups)",
"strata serve: decode expert cache hit rate: 99.2% (3803 hits / 3835 lookups)",
"strata serve: decode expert cache hit rate: 99.3% (3810 hits / 3836 lookups)",
"strata serve: decode expert cache hit rate: 99.5% (27185 hits / 27330 lookups)",
"strata serve: decode expert cache hit rate: 99.3% (3807 hits / 3835 lookups)",
"strata serve: decode expert cache hit rate: 99.2% (139825 hits / 140938 lookups)",
"strata serve: decode expert cache hit rate: 99.7% (142122 hits / 142524 lookups)",
"strata serve: decode expert cache hit rate: 99.8% (146556 hits / 146851 lookups)",
"strata serve: decode expert cache hit rate: 99.0% (118026 hits / 119233 lookups)",
"strata serve: decode expert cache hit rate: 99.8% (118729 hits / 118997 lookups)",
"strata serve: decode expert cache hit rate: 99.8% (118791 hits / 119007 lookups)"
]
}
@@ -0,0 +1,85 @@
{
"label": "ks-k21",
"extra": [
"--layer-split",
"21"
],
"gpus": "0,2",
"needles": [
{
"tokens": 16066,
"prompt_ms": 8880.5,
"pp_tok_s": 1809.1,
"found": true,
"answer": "meadow-254<|im_end|>"
},
{
"tokens": 27743,
"prompt_ms": 13654.6,
"pp_tok_s": 2031.8,
"found": true,
"answer": "juniper-766<|im_end|>"
}
],
"chat": {
"turn1_tokens": 6584,
"turn1_ms": 3680.6,
"turn2_tokens": 6660,
"reused": 6632,
"turn2_ms": 235.4,
"answer": "lantern-417<|im_end|>",
"ok": true
},
"decode": [
{
"name": "story",
"tok_s": 77.4,
"reps": [
80.2,
80.8,
71.2
],
"hit_rate": 0.9973622550922132,
"text": "The wind didn\u2019t just blow; it screamed.\n\nElias pulled his woolen collar up to his chin, the rough fabric scratching against his stubble. Outside the thick, salt-crusted glass of the lantern room, the Atlantic was a churning cauldron of black and white foam. Rain lashed against the panes in horizontal sheets, blurring the world into a smear of gray and shadow.\n\nInside, however, it was warm. The smell of kerosene and old wood hung heavy in the air, a scent that had defined his life for thirty years.\n\nElias was not a man of many words. He was a man of many tasks. His hands, mapped with scars from rope burns and broken glass, moved with a rhythm that predated his memory. He checked the clockwork mechanism of the Fresnel lens, ensuring the rotation was smooth, silent, and steady. *Tick. Tick. Tick.* The sound was the heartbeat of the coast.\n\n"
},
{
"name": "code",
"tok_s": 106.7,
"reps": [
100.6,
110.5,
109.0
],
"hit_rate": 0.998218711927068,
"text": "```python\nimport csv\nfrom typing import List, Dict, Any\n\n\ndef parse_csv_to_dicts(filepath: str) -> List[Dict[str, Any]]:\n \"\"\"\n Parse a CSV file into a list of dictionaries.\n \n Each dictionary represents a row, with keys from the header and values from the row.\n \n Args:\n filepath: Path to the CSV file.\n \n Returns:\n A list of dictionaries, one per row (excluding the header).\n \"\"\"\n result: List[Dict[str, Any]] = []\n with open(filepath, 'r', newline='', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n for row in reader:\n result.append(dict(row))\n return result\n\n\n# Tests\ndef test_parse_csv_to_dicts():\n import tempfile\n import os\n \n # Create a temporary CSV file\n with tempfile.NamedTemporaryFile"
}
],
"stage_timing": [
"strata serve: stage 0: 527 windows; per window: wait for the GPU 6.934 ms, pool + plan 0.620 ms, host staging 0.530 ms, commit 0.245 ms",
"strata serve: stage 1: 527 windows; per window: wait for the GPU 13.912 ms, pool + plan 0.365 ms, host staging 0.000 ms, commit 0.359 ms",
"strata serve: stage 0: 595 windows; per window: wait for the GPU 7.031 ms, pool + plan 0.595 ms, host staging 0.472 ms, commit 0.245 ms",
"strata serve: stage 1: 595 windows; per window: wait for the GPU 14.051 ms, pool + plan 0.361 ms, host staging 0.000 ms, commit 0.361 ms"
],
"engine_lines": [
"strata generate: layer split across 2 GPUs: CUDA0, then CUDA1 (split 21)",
"strata generate: layer split: CUDA1 PCIe probe 6.4 GB/s -> pcie_frac 0.14",
"strata generate: layer split: CUDA1 holds its weights, session and head; 18.13 GiB free",
"strata generate: expert cache auto: 10.46 GiB free, 700 MiB reserved -> 3345 slots",
"strata generate: expert cache 4627 slots, 8.29 GiB of VRAM; policy is",
"strata generate: layer split: CUDA1 runs layers 21-47, expert cache 6912 slots (13.78 GiB), 6912 of its 6912 profiled pairs; slot 0 verified",
"strata generate: layer split: CUDA0 runs layers 0-20",
"strata serve: the prompt path allocates its own buffers (too few cache slots to borrow)",
"strata serve: layer split: layers 0-20 (CUDA0), 21-47 (CUDA1), one hand-off per window",
"strata serve: decode expert cache hit rate: 98.7% (5672 hits / 5748 lookups)",
"strata serve: decode expert cache hit rate: 99.4% (4287 hits / 4314 lookups)",
"strata serve: decode expert cache hit rate: 99.4% (4287 hits / 4315 lookups)",
"strata serve: decode expert cache hit rate: 99.3% (29024 hits / 29229 lookups)",
"strata serve: decode expert cache hit rate: 99.1% (4278 hits / 4316 lookups)",
"strata serve: decode expert cache hit rate: 98.8% (131499 hits / 133085 lookups)",
"strata serve: decode expert cache hit rate: 99.6% (130974 hits / 131451 lookups)",
"strata serve: decode expert cache hit rate: 99.7% (138767 hits / 139134 lookups)",
"strata serve: decode expert cache hit rate: 98.8% (121454 hits / 122932 lookups)",
"strata serve: decode expert cache hit rate: 99.8% (116809 hits / 117079 lookups)",
"strata serve: decode expert cache hit rate: 99.8% (118803 hits / 119015 lookups)"
]
}
@@ -0,0 +1,85 @@
{
"label": "ks-k22",
"extra": [
"--layer-split",
"22"
],
"gpus": "0,2",
"needles": [
{
"tokens": 16066,
"prompt_ms": 8730.9,
"pp_tok_s": 1840.1,
"found": true,
"answer": "meadow-254<|im_end|>"
},
{
"tokens": 27743,
"prompt_ms": 13375.4,
"pp_tok_s": 2074.2,
"found": true,
"answer": "juniper-766<|im_end|>"
}
],
"chat": {
"turn1_tokens": 6584,
"turn1_ms": 3611.4,
"turn2_tokens": 6660,
"reused": 6632,
"turn2_ms": 234.8,
"answer": "lantern-417<|im_end|>",
"ok": true
},
"decode": [
{
"name": "story",
"tok_s": 80.2,
"reps": [
78.4,
80.9,
81.5
],
"hit_rate": 0.9977764197412695,
"text": "The wind didn\u2019t just blow; it screamed.\n\nElias pulled his wool coat tighter around his shoulders, the fabric stiff with salt and age. He was seventy-three years old, and he had been the keeper of the Blackwood Point Light for forty of those years. The other thirty-three had been spent in a small, drafty cottage on the mainland, waiting for the day when the sea would finally claim him.\n\nTonight, the storm was a Category 4. The waves weren\u2019t just crashing; they were climbing. They reached the gallery railing, ten feet above the base of the tower, spraying a fine mist of brine that coated Elias\u2019s glasses.\n\nHe climbed the spiral stairs, his knees protesting with every step. The iron steps were cold, slick with condensation. At the top, in the lantern room, the air was thin and sharp. The great Fresnel lens, a cathedral of glass and brass, stood silent. It was time to wake it.\n\n"
},
{
"name": "code",
"tok_s": 106.4,
"reps": [
100.7,
110.3,
108.1
],
"hit_rate": 0.9971798370756316,
"text": "```python\nimport csv\nfrom typing import List, Dict, Any\n\n\ndef parse_csv_to_dicts(filepath: str) -> List[Dict[str, Any]]:\n \"\"\"\n Parse a CSV file into a list of dictionaries.\n \n Each dictionary represents a row, with keys from the header and values from the row.\n \n Args:\n filepath: Path to the CSV file.\n \n Returns:\n A list of dictionaries, one per row (excluding the header).\n \"\"\"\n result: List[Dict[str, Any]] = []\n with open(filepath, 'r', newline='', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n for row in reader:\n result.append(dict(row))\n return result\n\n\n# Tests\ndef test_parse_csv_to_dicts():\n import tempfile\n import os\n \n # Create a temporary CSV file\n with tempfile.NamedTemporaryFile"
}
],
"stage_timing": [
"strata serve: stage 0: 529 windows; per window: wait for the GPU 7.229 ms, pool + plan 0.561 ms, host staging 0.529 ms, commit 0.233 ms",
"strata serve: stage 1: 529 windows; per window: wait for the GPU 13.058 ms, pool + plan 0.253 ms, host staging 0.000 ms, commit 0.334 ms",
"strata serve: stage 0: 598 windows; per window: wait for the GPU 7.325 ms, pool + plan 0.552 ms, host staging 0.469 ms, commit 0.235 ms",
"strata serve: stage 1: 598 windows; per window: wait for the GPU 13.238 ms, pool + plan 0.260 ms, host staging 0.000 ms, commit 0.337 ms"
],
"engine_lines": [
"strata generate: layer split across 2 GPUs: CUDA0, then CUDA1 (split 22)",
"strata generate: layer split: CUDA1 PCIe probe 6.4 GB/s -> pcie_frac 0.14",
"strata generate: layer split: CUDA1 holds its weights, session and head; 18.13 GiB free",
"strata generate: expert cache auto: 10.46 GiB free, 700 MiB reserved -> 3345 slots",
"strata generate: expert cache 4640 slots, 8.29 GiB of VRAM; policy is",
"strata generate: layer split: CUDA1 runs layers 22-47, expert cache 6656 slots (13.34 GiB), 6656 of its 6656 profiled pairs; slot 0 verified",
"strata generate: layer split: CUDA0 runs layers 0-21",
"strata serve: the prompt path allocates its own buffers (too few cache slots to borrow)",
"strata serve: layer split: layers 0-21 (CUDA0), 22-47 (CUDA1), one hand-off per window",
"strata serve: decode expert cache hit rate: 97.7% (5609 hits / 5739 lookups)",
"strata serve: decode expert cache hit rate: 98.7% (4256 hits / 4310 lookups)",
"strata serve: decode expert cache hit rate: 99.1% (4278 hits / 4315 lookups)",
"strata serve: decode expert cache hit rate: 99.0% (27008 hits / 27289 lookups)",
"strata serve: decode expert cache hit rate: 98.8% (4260 hits / 4313 lookups)",
"strata serve: decode expert cache hit rate: 98.5% (135183 hits / 137274 lookups)",
"strata serve: decode expert cache hit rate: 99.6% (136132 hits / 136708 lookups)",
"strata serve: decode expert cache hit rate: 99.8% (135515 hits / 135817 lookups)",
"strata serve: decode expert cache hit rate: 98.4% (121287 hits / 123280 lookups)",
"strata serve: decode expert cache hit rate: 99.7% (117658 hits / 118026 lookups)",
"strata serve: decode expert cache hit rate: 99.7% (120574 hits / 120915 lookups)"
]
}
@@ -0,0 +1,85 @@
{
"label": "ks-k23",
"extra": [
"--layer-split",
"23"
],
"gpus": "0,2",
"needles": [
{
"tokens": 16066,
"prompt_ms": 8742.6,
"pp_tok_s": 1837.7,
"found": true,
"answer": "meadow-254<|im_end|>"
},
{
"tokens": 27743,
"prompt_ms": 13084.2,
"pp_tok_s": 2120.3,
"found": true,
"answer": "juniper-766<|im_end|>"
}
],
"chat": {
"turn1_tokens": 6584,
"turn1_ms": 3566.1,
"turn2_tokens": 6660,
"reused": 6631,
"turn2_ms": 238.0,
"answer": "lantern-417<|im_end|>",
"ok": true
},
"decode": [
{
"name": "story",
"tok_s": 81.0,
"reps": [
76.8,
85.2,
81.1
],
"hit_rate": 0.9961302681992337,
"text": "The wind didn\u2019t just blow; it screamed.\n\nElias had lived in the tower for thirty years, and he knew the difference between a gale and a hurricane by the way the glass rattled in its frame. Tonight, it was the latter. The sea was a churning black void, swallowing the horizon, throwing white foam against the granite base like a tantrum.\n\nInside the lantern room, the air was thick with the smell of kerosene and old brass. Elias wiped his hands on his wool trousers, the fabric stiff with salt and grease. He checked the clockwork mechanism that turned the lens. It was a beast of a thing, a Fresnel lens of eight feet in diameter, a cathedral of glass and crystal that focused the beam of the lamp into a single, piercing needle of light.\n\nHe wound the weights. *Click, click, click.* The gears engaged. The massive lens began its slow, hypnotic rotation.\n\nFor a moment, the"
},
{
"name": "code",
"tok_s": 108.4,
"reps": [
105.1,
109.0,
111.1
],
"hit_rate": 0.9961698810290479,
"text": "```python\nimport csv\nfrom typing import List, Dict, Any\n\n\ndef parse_csv_to_dicts(filepath: str) -> List[Dict[str, Any]]:\n \"\"\"\n Parse a CSV file into a list of dictionaries.\n \n Each dictionary represents a row, with keys from the header and values from the row.\n \n Args:\n filepath: Path to the CSV file.\n \n Returns:\n A list of dictionaries, one per row (excluding the header).\n \"\"\"\n result: List[Dict[str, Any]] = []\n with open(filepath, 'r', newline='', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n for row in reader:\n result.append(dict(row))\n return result\n\n\n# Tests\ndef test_parse_csv_to_dicts():\n import tempfile\n import os\n \n # Create a temporary CSV file\n with tempfile.NamedTemporaryFile"
}
],
"stage_timing": [
"strata serve: stage 0: 537 windows; per window: wait for the GPU 7.423 ms, pool + plan 0.953 ms, host staging 0.542 ms, commit 0.237 ms",
"strata serve: stage 1: 537 windows; per window: wait for the GPU 12.223 ms, pool + plan 0.236 ms, host staging 0.000 ms, commit 0.324 ms",
"strata serve: stage 0: 606 windows; per window: wait for the GPU 7.519 ms, pool + plan 0.904 ms, host staging 0.483 ms, commit 0.239 ms",
"strata serve: stage 1: 606 windows; per window: wait for the GPU 12.395 ms, pool + plan 0.242 ms, host staging 0.000 ms, commit 0.326 ms"
],
"engine_lines": [
"strata generate: layer split across 2 GPUs: CUDA0, then CUDA1 (split 23)",
"strata generate: layer split: CUDA1 PCIe probe 6.4 GB/s -> pcie_frac 0.14",
"strata generate: layer split: CUDA1 holds its weights, session and head; 18.13 GiB free",
"strata generate: expert cache auto: 10.46 GiB free, 700 MiB reserved -> 3345 slots",
"strata generate: expert cache 4609 slots, 8.29 GiB of VRAM; policy is",
"strata generate: layer split: CUDA1 runs layers 23-47, expert cache 6400 slots (12.82 GiB), 6400 of its 6400 profiled pairs; slot 0 verified",
"strata generate: layer split: CUDA0 runs layers 0-22",
"strata serve: the prompt path allocates its own buffers (too few cache slots to borrow)",
"strata serve: layer split: layers 0-22 (CUDA0), 23-47 (CUDA1), one hand-off per window",
"strata serve: decode expert cache hit rate: 96.8% (5528 hits / 5709 lookups)",
"strata serve: decode expert cache hit rate: 98.4% (4235 hits / 4303 lookups)",
"strata serve: decode expert cache hit rate: 98.9% (4260 hits / 4308 lookups)",
"strata serve: decode expert cache hit rate: 98.6% (30643 hits / 31089 lookups)",
"strata serve: decode expert cache hit rate: 98.6% (4248 hits / 4309 lookups)",
"strata serve: decode expert cache hit rate: 98.3% (134776 hits / 137143 lookups)",
"strata serve: decode expert cache hit rate: 99.4% (133024 hits / 133766 lookups)",
"strata serve: decode expert cache hit rate: 99.6% (129995 hits / 130500 lookups)",
"strata serve: decode expert cache hit rate: 97.9% (115782 hits / 118287 lookups)",
"strata serve: decode expert cache hit rate: 99.6% (117953 hits / 118464 lookups)",
"strata serve: decode expert cache hit rate: 99.6% (117560 hits / 118012 lookups)"
]
}
@@ -0,0 +1,85 @@
{
"label": "ks-k24",
"extra": [
"--layer-split",
"24"
],
"gpus": "0,2",
"needles": [
{
"tokens": 16066,
"prompt_ms": 8122.5,
"pp_tok_s": 1978.0,
"found": true,
"answer": "meadow-254<|im_end|>"
},
{
"tokens": 27743,
"prompt_ms": 12285.8,
"pp_tok_s": 2258.1,
"found": true,
"answer": "juniper-766<|im_end|>"
}
],
"chat": {
"turn1_tokens": 6584,
"turn1_ms": 3407.0,
"turn2_tokens": 6660,
"reused": 6632,
"turn2_ms": 234.0,
"answer": "lantern-417<|im_end|>",
"ok": true
},
"decode": [
{
"name": "story",
"tok_s": 83.5,
"reps": [
84.9,
83.3,
82.3
],
"hit_rate": 0.9949381327334084,
"text": "The wind didn\u2019t just blow; it screamed.\n\nElias pulled his woolen collar up to his chin, the rough fabric scratching against his stubble. Outside the thick, salt-crusted glass of the lantern room, the Atlantic was a churning cauldron of black and white foam. Rain lashed against the panes in horizontal sheets, blurring the world into a smear of gray and shadow.\n\nInside, however, it was warm. The smell of kerosene and old wood hung heavy in the air, a scent that had defined Elias\u2019s life for thirty years. He was not merely a keeper; he was a guardian of silence.\n\nHe checked the clock. 11:42 PM. The rotation of the great Fresnel lens was due. With a practiced hand, he engaged the brake on the clockwork mechanism. The heavy brass gears groaned, then began their slow, rhythmic turn. *Tick. Tick. Tick.* It was a heartbeat"
},
{
"name": "code",
"tok_s": 109.5,
"reps": [
104.9,
111.0,
112.6
],
"hit_rate": 0.9953796671318199,
"text": "```python\nimport csv\nfrom typing import List, Dict, Any\n\n\ndef parse_csv_to_dicts(filepath: str) -> List[Dict[str, Any]]:\n \"\"\"\n Parse a CSV file into a list of dictionaries.\n \n Each dictionary represents a row, with keys from the header and values from the row.\n \n Args:\n filepath: Path to the CSV file.\n \n Returns:\n A list of dictionaries, one per row (excluding the header).\n \"\"\"\n result: List[Dict[str, Any]] = []\n with open(filepath, 'r', newline='', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n for row in reader:\n result.append(dict(row))\n return result\n\n\n# Tests\ndef test_parse_csv_to_dicts():\n import tempfile\n import os\n \n # Create a temporary CSV file\n with tempfile.NamedTemporaryFile"
}
],
"stage_timing": [
"strata serve: stage 0: 519 windows; per window: wait for the GPU 7.835 ms, pool + plan 0.767 ms, host staging 0.557 ms, commit 0.244 ms",
"strata serve: stage 1: 519 windows; per window: wait for the GPU 11.922 ms, pool + plan 0.232 ms, host staging 0.000 ms, commit 0.325 ms",
"strata serve: stage 0: 587 windows; per window: wait for the GPU 7.943 ms, pool + plan 0.744 ms, host staging 0.497 ms, commit 0.246 ms",
"strata serve: stage 1: 587 windows; per window: wait for the GPU 12.073 ms, pool + plan 0.238 ms, host staging 0.000 ms, commit 0.326 ms"
],
"engine_lines": [
"strata generate: layer split across 2 GPUs: CUDA0, then CUDA1 (split 24)",
"strata generate: layer split: CUDA1 PCIe probe 6.4 GB/s -> pcie_frac 0.14",
"strata generate: layer split: CUDA1 holds its weights, session and head; 18.13 GiB free",
"strata generate: expert cache auto: 10.46 GiB free, 700 MiB reserved -> 3345 slots",
"strata generate: expert cache 4603 slots, 8.29 GiB of VRAM; policy is",
"strata generate: layer split: CUDA1 runs layers 24-47, expert cache 6144 slots (12.35 GiB), 6144 of its 6144 profiled pairs; slot 0 verified",
"strata generate: layer split: CUDA0 runs layers 0-23",
"strata serve: the prompt path allocates its own buffers (too few cache slots to borrow)",
"strata serve: layer split: layers 0-23 (CUDA0), 24-47 (CUDA1), one hand-off per window",
"strata serve: decode expert cache hit rate: 95.9% (5916 hits / 6171 lookups)",
"strata serve: decode expert cache hit rate: 97.4% (3717 hits / 3815 lookups)",
"strata serve: decode expert cache hit rate: 98.4% (3761 hits / 3823 lookups)",
"strata serve: decode expert cache hit rate: 98.1% (27138 hits / 27664 lookups)",
"strata serve: decode expert cache hit rate: 97.8% (3742 hits / 3826 lookups)",
"strata serve: decode expert cache hit rate: 97.9% (126114 hits / 128875 lookups)",
"strata serve: decode expert cache hit rate: 99.2% (133626 hits / 134658 lookups)",
"strata serve: decode expert cache hit rate: 99.5% (132675 hits / 133350 lookups)",
"strata serve: decode expert cache hit rate: 96.8% (113593 hits / 117399 lookups)",
"strata serve: decode expert cache hit rate: 99.5% (118372 hits / 118949 lookups)",
"strata serve: decode expert cache hit rate: 99.5% (116981 hits / 117524 lookups)"
]
}
@@ -0,0 +1,85 @@
{
"label": "ks-k26",
"extra": [
"--layer-split",
"26"
],
"gpus": "0,2",
"needles": [
{
"tokens": 16066,
"prompt_ms": 7880.9,
"pp_tok_s": 2038.6,
"found": true,
"answer": "meadow-254<|im_end|>"
},
{
"tokens": 27743,
"prompt_ms": 11771.9,
"pp_tok_s": 2356.7,
"found": true,
"answer": "juniper-766<|im_end|>"
}
],
"chat": {
"turn1_tokens": 6584,
"turn1_ms": 3323.9,
"turn2_tokens": 6660,
"reused": 6632,
"turn2_ms": 231.5,
"answer": "lantern-417<|im_end|>",
"ok": true
},
"decode": [
{
"name": "story",
"tok_s": 83.8,
"reps": [
85.9,
83.1,
82.4
],
"hit_rate": 0.9933966961830739,
"text": "The wind didn\u2019t just blow; it screamed.\n\nElias pulled his woolen collar up to his chin, the rough fabric scratching against his stubble. Outside the thick, salt-crusted glass of the lantern room, the Atlantic was a churning cauldron of black and white foam. Rain lashed against the panes in horizontal sheets, blurring the world into a smear of gray and green.\n\nInside, however, it was warm. The smell of kerosene and old wood hung heavy in the air, a scent that had defined Elias\u2019s life for thirty years. He was a keeper of the light, but more importantly, he was a keeper of time.\n\nHe checked his pocket watch. 11:42 PM. The rotation of the great Fresnel lens was due. With a practiced, rhythmic *clunk-clunk-clunk*, he wound the clockwork mechanism. The massive lens began its slow, hypnotic turn, sweeping its beam across"
},
{
"name": "code",
"tok_s": 109.7,
"reps": [
104.4,
112.7,
111.9
],
"hit_rate": 0.9938670271352759,
"text": "```python\nimport csv\nfrom typing import List, Dict, Any\n\n\ndef parse_csv_to_dicts(filepath: str) -> List[Dict[str, Any]]:\n \"\"\"\n Parse a CSV file into a list of dictionaries.\n \n Each dictionary represents a row, with keys from the header and values from the row.\n \n Args:\n filepath: Path to the CSV file.\n \n Returns:\n A list of dictionaries, one per row (excluding the header).\n \"\"\"\n result: List[Dict[str, Any]] = []\n with open(filepath, 'r', newline='', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n for row in reader:\n result.append(dict(row))\n return result\n\n\n# Tests\ndef test_parse_csv_to_dicts():\n import tempfile\n import os\n \n # Create a temporary CSV file\n with tempfile.NamedTemporaryFile"
}
],
"stage_timing": [
"strata serve: stage 0: 519 windows; per window: wait for the GPU 8.376 ms, pool + plan 1.030 ms, host staging 0.566 ms, commit 0.273 ms",
"strata serve: stage 1: 519 windows; per window: wait for the GPU 10.886 ms, pool + plan 0.214 ms, host staging 0.000 ms, commit 0.297 ms",
"strata serve: stage 0: 588 windows; per window: wait for the GPU 8.506 ms, pool + plan 0.985 ms, host staging 0.505 ms, commit 0.274 ms",
"strata serve: stage 1: 588 windows; per window: wait for the GPU 11.036 ms, pool + plan 0.219 ms, host staging 0.000 ms, commit 0.298 ms"
],
"engine_lines": [
"strata generate: layer split across 2 GPUs: CUDA0, then CUDA1 (split 26)",
"strata generate: layer split: CUDA1 PCIe probe 6.4 GB/s -> pcie_frac 0.14",
"strata generate: layer split: CUDA1 holds its weights, session and head; 18.13 GiB free",
"strata generate: expert cache auto: 10.46 GiB free, 700 MiB reserved -> 3345 slots",
"strata generate: expert cache 4568 slots, 8.29 GiB of VRAM; policy is",
"strata generate: layer split: CUDA1 runs layers 26-47, expert cache 5632 slots (11.32 GiB), 5632 of its 5632 profiled pairs; slot 0 verified",
"strata generate: layer split: CUDA0 runs layers 0-25",
"strata serve: the prompt path allocates its own buffers (too few cache slots to borrow)",
"strata serve: layer split: layers 0-25 (CUDA0), 26-47 (CUDA1), one hand-off per window",
"strata serve: decode expert cache hit rate: 92.8% (5259 hits / 5665 lookups)",
"strata serve: decode expert cache hit rate: 95.2% (4056 hits / 4259 lookups)",
"strata serve: decode expert cache hit rate: 97.2% (4164 hits / 4285 lookups)",
"strata serve: decode expert cache hit rate: 97.1% (27690 hits / 28510 lookups)",
"strata serve: decode expert cache hit rate: 97.5% (4184 hits / 4293 lookups)",
"strata serve: decode expert cache hit rate: 96.9% (121839 hits / 125724 lookups)",
"strata serve: decode expert cache hit rate: 98.9% (132165 hits / 133648 lookups)",
"strata serve: decode expert cache hit rate: 99.3% (137652 hits / 138567 lookups)",
"strata serve: decode expert cache hit rate: 95.3% (111307 hits / 116822 lookups)",
"strata serve: decode expert cache hit rate: 99.3% (118436 hits / 119329 lookups)",
"strata serve: decode expert cache hit rate: 99.4% (119109 hits / 119844 lookups)"
]
}
@@ -0,0 +1,85 @@
{
"label": "ks-k28",
"extra": [
"--layer-split",
"28"
],
"gpus": "0,2",
"needles": [
{
"tokens": 16066,
"prompt_ms": 7989.3,
"pp_tok_s": 2010.9,
"found": true,
"answer": "meadow-254<|im_end|>"
},
{
"tokens": 27743,
"prompt_ms": 12132.9,
"pp_tok_s": 2286.6,
"found": true,
"answer": "juniper-766<|im_end|>"
}
],
"chat": {
"turn1_tokens": 6584,
"turn1_ms": 3433.5,
"turn2_tokens": 6660,
"reused": 6632,
"turn2_ms": 228.0,
"answer": "lantern-417<|im_end|>",
"ok": true
},
"decode": [
{
"name": "story",
"tok_s": 83.5,
"reps": [
83.7,
82.4,
84.5
],
"hit_rate": 0.9900441970361987,
"text": "The wind didn\u2019t just blow; it screamed.\n\nElias had lived in the tower for thirty years, and he knew the difference between a gale and a hurricane by the way the glass rattled in its frame. Tonight, the glass was vibrating like a plucked cello string. The sea, usually a rhythmic pulse against the granite base, was now a chaotic, frothing beast, throwing itself against the rocks with the force of a battering ram.\n\nElias climbed the spiral stairs, his hand gripping the cold iron railing. His knees ached, not from age, but from the constant vibration of the floor beneath his feet. He reached the lantern room, the heart of the lighthouse, and checked the clockwork mechanism. The great lens, a prism of Fresnel glass, rotated with a steady, hypnotic *whirrr-click, whirrr-click*. It was the only sound that mattered. The light swept out into the black void, cutting through"
},
{
"name": "code",
"tok_s": 112.5,
"reps": [
101.4,
112.1,
124.1
],
"hit_rate": 0.9896636587366694,
"text": "```python\nimport csv\nfrom typing import List, Dict, Any\n\n\ndef parse_csv(file_path: str) -> List[Dict[str, Any]]:\n \"\"\"\n Parse a CSV file into a list of dictionaries.\n \n Each dictionary represents a row, with keys being the header values\n and values being the corresponding cell values.\n \n Args:\n file_path: Path to the CSV file.\n \n Returns:\n A list of dictionaries, one per row (excluding the header).\n \"\"\"\n result = []\n with open(file_path, 'r', newline='', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n for row in reader:\n result.append(dict(row))\n return result\n```<|im_end|>"
}
],
"stage_timing": [
"strata serve: stage 0: 527 windows; per window: wait for the GPU 8.765 ms, pool + plan 1.401 ms, host staging 0.593 ms, commit 0.281 ms",
"strata serve: stage 1: 527 windows; per window: wait for the GPU 9.893 ms, pool + plan 0.195 ms, host staging 0.000 ms, commit 0.276 ms",
"strata serve: stage 0: 578 windows; per window: wait for the GPU 8.908 ms, pool + plan 1.359 ms, host staging 0.559 ms, commit 0.284 ms",
"strata serve: stage 1: 578 windows; per window: wait for the GPU 10.025 ms, pool + plan 0.200 ms, host staging 0.000 ms, commit 0.278 ms"
],
"engine_lines": [
"strata generate: layer split across 2 GPUs: CUDA0, then CUDA1 (split 28)",
"strata generate: layer split: CUDA1 PCIe probe 6.4 GB/s -> pcie_frac 0.14",
"strata generate: layer split: CUDA1 holds its weights, session and head; 18.13 GiB free",
"strata generate: expert cache auto: 10.46 GiB free, 700 MiB reserved -> 3345 slots",
"strata generate: expert cache 4537 slots, 8.29 GiB of VRAM; policy is",
"strata generate: layer split: CUDA1 runs layers 28-47, expert cache 5120 slots (10.30 GiB), 5120 of its 5120 profiled pairs; slot 0 verified",
"strata generate: layer split: CUDA0 runs layers 0-27",
"strata serve: the prompt path allocates its own buffers (too few cache slots to borrow)",
"strata serve: layer split: layers 0-27 (CUDA0), 28-47 (CUDA1), one hand-off per window",
"strata serve: decode expert cache hit rate: 88.8% (5405 hits / 6086 lookups)",
"strata serve: decode expert cache hit rate: 93.8% (3539 hits / 3773 lookups)",
"strata serve: decode expert cache hit rate: 94.2% (3560 hits / 3781 lookups)",
"strata serve: decode expert cache hit rate: 95.7% (26702 hits / 27899 lookups)",
"strata serve: decode expert cache hit rate: 95.9% (3647 hits / 3804 lookups)",
"strata serve: decode expert cache hit rate: 95.7% (122618 hits / 128160 lookups)",
"strata serve: decode expert cache hit rate: 98.6% (135035 hits / 136884 lookups)",
"strata serve: decode expert cache hit rate: 99.0% (129476 hits / 130778 lookups)",
"strata serve: decode expert cache hit rate: 93.9% (111037 hits / 118290 lookups)",
"strata serve: decode expert cache hit rate: 98.9% (118829 hits / 120140 lookups)",
"strata serve: decode expert cache hit rate: 99.0% (90480 hits / 91425 lookups)"
]
}
@@ -0,0 +1,85 @@
{
"label": "ks-k30",
"extra": [
"--layer-split",
"30"
],
"gpus": "0,2",
"needles": [
{
"tokens": 16066,
"prompt_ms": 8301.3,
"pp_tok_s": 1935.4,
"found": true,
"answer": "meadow-254<|im_end|>"
},
{
"tokens": 27743,
"prompt_ms": 12507.6,
"pp_tok_s": 2218.1,
"found": true,
"answer": "juniper-766<|im_end|>"
}
],
"chat": {
"turn1_tokens": 6584,
"turn1_ms": 3448.7,
"turn2_tokens": 6660,
"reused": 6632,
"turn2_ms": 229.4,
"answer": "lantern-417<|im_end|>",
"ok": true
},
"decode": [
{
"name": "story",
"tok_s": 81.1,
"reps": [
78.0,
84.4,
81.0
],
"hit_rate": 0.9798126997277784,
"text": "The wind didn\u2019t just blow; it screamed.\n\nElias had lived in the tower for thirty years, and he knew the difference between a gale and a hurricane by the way the glass rattled in its frame. Tonight, the glass was vibrating like a plucked cello string. The sea, usually a rhythmic pulse against the granite base, was now a chaotic, frothing beast, throwing itself against the rocks with the force of a battering ram.\n\nElias stood at the top of the spiral staircase, his hand resting on the cold iron railing. He was not afraid. Fear was for the tourists who came in summer, wide-eyed and clutching their cameras. Elias was a keeper. He was a guardian.\n\nHe checked the logbook. The entry for the night was simple: *Wind, 60 knots. Visibility, zero. Light, operational.*\n\nHe walked down the stairs, his boots heavy on the metal grating. The smell of salt"
},
{
"name": "code",
"tok_s": 109.6,
"reps": [
100.7,
114.1,
114.1
],
"hit_rate": 0.9825370343977666,
"text": "```python\nimport csv\nfrom typing import List, Dict, Any\n\n\ndef parse_csv_to_dicts(filepath: str) -> List[Dict[str, Any]]:\n \"\"\"\n Parse a CSV file into a list of dictionaries.\n \n Each dictionary represents a row, with keys from the header and values from the row.\n \n Args:\n filepath: Path to the CSV file.\n \n Returns:\n A list of dictionaries, one per row (excluding the header).\n \"\"\"\n result: List[Dict[str, Any]] = []\n with open(filepath, 'r', newline='', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n for row in reader:\n result.append(dict(row))\n return result\n\n\n# Tests\ndef test_parse_csv_to_dicts():\n import tempfile\n import os\n \n # Create a temporary CSV file\n with tempfile.NamedTemporaryFile"
}
],
"stage_timing": [
"strata serve: stage 0: 540 windows; per window: wait for the GPU 9.206 ms, pool + plan 1.794 ms, host staging 0.525 ms, commit 0.298 ms",
"strata serve: stage 1: 540 windows; per window: wait for the GPU 8.767 ms, pool + plan 0.173 ms, host staging 0.000 ms, commit 0.242 ms",
"strata serve: stage 0: 608 windows; per window: wait for the GPU 9.390 ms, pool + plan 1.733 ms, host staging 0.471 ms, commit 0.301 ms",
"strata serve: stage 1: 608 windows; per window: wait for the GPU 8.910 ms, pool + plan 0.178 ms, host staging 0.000 ms, commit 0.244 ms"
],
"engine_lines": [
"strata generate: layer split across 2 GPUs: CUDA0, then CUDA1 (split 30)",
"strata generate: layer split: CUDA1 PCIe probe 6.4 GB/s -> pcie_frac 0.14",
"strata generate: layer split: CUDA1 holds its weights, session and head; 18.13 GiB free",
"strata generate: expert cache auto: 10.46 GiB free, 700 MiB reserved -> 3345 slots",
"strata generate: expert cache 4479 slots, 8.29 GiB of VRAM; policy is",
"strata generate: layer split: CUDA1 runs layers 30-47, expert cache 4608 slots (9.19 GiB), 4608 of its 4608 profiled pairs; slot 0 verified",
"strata generate: layer split: CUDA0 runs layers 0-29",
"strata serve: the prompt path allocates its own buffers (too few cache slots to borrow)",
"strata serve: layer split: layers 0-29 (CUDA0), 30-47 (CUDA1), one hand-off per window",
"strata serve: decode expert cache hit rate: 84.6% (5099 hits / 6028 lookups)",
"strata serve: decode expert cache hit rate: 90.7% (3403 hits / 3751 lookups)",
"strata serve: decode expert cache hit rate: 91.1% (3415 hits / 3747 lookups)",
"strata serve: decode expert cache hit rate: 94.1% (26108 hits / 27737 lookups)",
"strata serve: decode expert cache hit rate: 93.4% (3534 hits / 3784 lookups)",
"strata serve: decode expert cache hit rate: 94.6% (127532 hits / 134866 lookups)",
"strata serve: decode expert cache hit rate: 98.6% (135828 hits / 137786 lookups)",
"strata serve: decode expert cache hit rate: 98.0% (132455 hits / 135184 lookups)",
"strata serve: decode expert cache hit rate: 91.0% (106351 hits / 116917 lookups)",
"strata serve: decode expert cache hit rate: 97.9% (115791 hits / 118246 lookups)",
"strata serve: decode expert cache hit rate: 98.3% (117198 hits / 119281 lookups)"
]
}
@@ -0,0 +1,85 @@
{
"label": "ks-k32",
"extra": [
"--layer-split",
"32"
],
"gpus": "0,2",
"needles": [
{
"tokens": 16066,
"prompt_ms": 8837.1,
"pp_tok_s": 1818.0,
"found": true,
"answer": "meadow-254<|im_end|>"
},
{
"tokens": 27743,
"prompt_ms": 13449.8,
"pp_tok_s": 2062.7,
"found": true,
"answer": "juniper-766<|im_end|>"
}
],
"chat": {
"turn1_tokens": 6584,
"turn1_ms": 3565.7,
"turn2_tokens": 6660,
"reused": 6632,
"turn2_ms": 227.0,
"answer": "lantern-417<|im_end|>",
"ok": true
},
"decode": [
{
"name": "story",
"tok_s": 83.0,
"reps": [
75.0,
86.1,
87.8
],
"hit_rate": 0.9796934433020751,
"text": "The wind didn\u2019t just blow; it screamed.\n\nElias had lived in the tower for thirty years, and he knew the difference between a gale and a hurricane by the way the glass rattled in its frame. Tonight, it was the latter. The sea was a churning cauldron of black ink and white foam, throwing itself against the granite base with the force of a battering ram.\n\nInside the lantern room, the air was thick with the smell of kerosene and old brass. Elias wiped his hands on his woolen trousers, leaving faint, oily smears on the fabric. He checked the clock. 11:42 PM. The rotation was due.\n\nHe reached for the crank handle, a heavy iron wheel worn smooth by decades of his own palms. With a grunt that matched the strain of his shoulders, he began to turn it. *Click. Click. Click.* The gears engaged, and the massive Fresnel lens began its slow"
},
{
"name": "code",
"tok_s": 108.7,
"reps": [
98.8,
112.2,
115.1
],
"hit_rate": 0.9777186846317358,
"text": "```python\nimport csv\nfrom typing import List, Dict, Any\n\n\ndef parse_csv_to_dicts(filepath: str) -> List[Dict[str, Any]]:\n \"\"\"\n Parse a CSV file into a list of dictionaries.\n \n Each dictionary represents a row, with keys from the header and values from the row.\n \n Args:\n filepath: Path to the CSV file.\n \n Returns:\n A list of dictionaries, one per row (excluding the header).\n \"\"\"\n result: List[Dict[str, Any]] = []\n with open(filepath, 'r', newline='', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n for row in reader:\n result.append(dict(row))\n return result\n\n\n# Tests\ndef test_parse_csv_to_dicts():\n import tempfile\n import os\n \n # Create a temporary CSV file\n with tempfile.NamedTemporaryFile"
}
],
"stage_timing": [
"strata serve: stage 0: 533 windows; per window: wait for the GPU 9.839 ms, pool + plan 2.192 ms, host staging 0.585 ms, commit 0.318 ms",
"strata serve: stage 1: 533 windows; per window: wait for the GPU 8.014 ms, pool + plan 0.156 ms, host staging 0.000 ms, commit 0.231 ms",
"strata serve: stage 0: 602 windows; per window: wait for the GPU 10.014 ms, pool + plan 2.111 ms, host staging 0.520 ms, commit 0.321 ms",
"strata serve: stage 1: 602 windows; per window: wait for the GPU 8.094 ms, pool + plan 0.160 ms, host staging 0.000 ms, commit 0.232 ms"
],
"engine_lines": [
"strata generate: layer split across 2 GPUs: CUDA0, then CUDA1 (split 32)",
"strata generate: layer split: CUDA1 PCIe probe 6.4 GB/s -> pcie_frac 0.14",
"strata generate: layer split: CUDA1 holds its weights, session and head; 18.13 GiB free",
"strata generate: expert cache auto: 10.46 GiB free, 700 MiB reserved -> 3345 slots",
"strata generate: expert cache 4488 slots, 8.29 GiB of VRAM; policy is",
"strata generate: layer split: CUDA1 runs layers 32-47, expert cache 4096 slots (8.26 GiB), 4096 of its 4096 profiled pairs; slot 0 verified",
"strata generate: layer split: CUDA0 runs layers 0-31",
"strata serve: the prompt path allocates its own buffers (too few cache slots to borrow)",
"strata serve: layer split: layers 0-31 (CUDA0), 32-47 (CUDA1), one hand-off per window",
"strata serve: decode expert cache hit rate: 81.3% (4889 hits / 6010 lookups)",
"strata serve: decode expert cache hit rate: 87.7% (3272 hits / 3733 lookups)",
"strata serve: decode expert cache hit rate: 87.9% (3280 hits / 3733 lookups)",
"strata serve: decode expert cache hit rate: 92.3% (24238 hits / 26254 lookups)",
"strata serve: decode expert cache hit rate: 91.5% (3457 hits / 3778 lookups)",
"strata serve: decode expert cache hit rate: 93.7% (133254 hits / 142240 lookups)",
"strata serve: decode expert cache hit rate: 98.0% (130112 hits / 132812 lookups)",
"strata serve: decode expert cache hit rate: 98.0% (130069 hits / 132765 lookups)",
"strata serve: decode expert cache hit rate: 89.1% (105263 hits / 118190 lookups)",
"strata serve: decode expert cache hit rate: 97.2% (116970 hits / 120309 lookups)",
"strata serve: decode expert cache hit rate: 97.8% (115450 hits / 118081 lookups)"
]
}
@@ -0,0 +1,76 @@
{
"label": "ks-single",
"extra": [],
"gpus": "0",
"needles": [
{
"tokens": 16066,
"prompt_ms": 9310.4,
"pp_tok_s": 1725.6,
"found": true,
"answer": "meadow-254<|im_end|>"
},
{
"tokens": 27743,
"prompt_ms": 14085.4,
"pp_tok_s": 1969.6,
"found": true,
"answer": "juniper-766<|im_end|>"
}
],
"chat": {
"turn1_tokens": 6584,
"turn1_ms": 3388.7,
"turn2_tokens": 6660,
"reused": 6632,
"turn2_ms": 200.7,
"answer": "lantern-417<|im_end|>",
"ok": true
},
"decode": [
{
"name": "story",
"tok_s": 86.7,
"reps": [
78.2,
91.4,
90.4
],
"hit_rate": 0.921882148773369,
"text": "The wind didn\u2019t just blow; it screamed.\n\nElias pulled his wool coat tighter around his shoulders, the fabric stiff with salt and age. He was seventy-three years old, and his hands were a map of the coastline he had guarded for fifty years. The knuckles were swollen, the skin cracked like dry earth, but they moved with a precision that no machine could replicate.\n\nHe stood on the gallery of the *Harrow Point* lighthouse, the iron railing cold against his palms. Below, the Atlantic churned in a frenzy, whitecaps exploding against the black rocks like fireworks. Above, the sky was a bruised purple, heavy with rain that hadn\u2019t yet fallen.\n\nElias checked the clock. 11:42 PM. The light was due.\n\nHe didn\u2019t look at the gauge. He didn\u2019t check the fuel levels. He knew. He always knew. The rhythm of the ocean, the pressure of the air, the subtle vibration"
},
{
"name": "code",
"tok_s": 105.2,
"reps": [
87.8,
113.5,
114.2
],
"hit_rate": 0.9109811738648947,
"text": "```python\nimport csv\nfrom typing import List, Dict, Any\n\n\ndef parse_csv_to_dicts(filepath: str) -> List[Dict[str, Any]]:\n \"\"\"\n Parse a CSV file into a list of dictionaries.\n \n Each dictionary represents a row, with keys from the header and values from the row.\n \n Args:\n filepath: Path to the CSV file.\n \n Returns:\n A list of dictionaries, one per row (excluding the header).\n \"\"\"\n result: List[Dict[str, Any]] = []\n with open(filepath, 'r', newline='', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n for row in reader:\n result.append(dict(row))\n return result\n\n\n# Tests\ndef test_parse_csv_to_dicts():\n import tempfile\n import os\n \n # Create a temporary CSV file\n with tempfile.NamedTemporaryFile"
}
],
"stage_timing": [
"strata serve: stage 0: 382 windows; per window: wait for the GPU 15.155 ms, pool + plan 4.331 ms, host staging 0.656 ms, commit 0.458 ms",
"strata serve: stage 0: 455 windows; per window: wait for the GPU 15.790 ms, pool + plan 5.552 ms, host staging 0.688 ms, commit 0.467 ms",
"strata serve: stage 0: 524 windows; per window: wait for the GPU 16.064 ms, pool + plan 5.529 ms, host staging 0.606 ms, commit 0.472 ms",
"strata serve: stage 0: 593 windows; per window: wait for the GPU 16.305 ms, pool + plan 5.462 ms, host staging 0.540 ms, commit 0.475 ms"
],
"engine_lines": [
"strata generate: expert cache auto: 9.15 GiB free, 700 MiB reserved -> 3415 slots",
"strata generate: expert cache 4293 slots, 8.17 GiB of VRAM; policy is",
"strata serve: the prompt path borrows 2343 cache slots (4.42 GiB)",
"strata serve: decode expert cache hit rate: 59.6% (2749 hits / 4610 lookups)",
"strata serve: decode expert cache hit rate: 67.8% (2299 hits / 3391 lookups)",
"strata serve: decode expert cache hit rate: 68.5% (2345 hits / 3424 lookups)",
"strata serve: decode expert cache hit rate: 75.6% (16605 hits / 21959 lookups)",
"strata serve: decode expert cache hit rate: 73.3% (2608 hits / 3558 lookups)",
"strata serve: decode expert cache hit rate: 83.7% (104878 hits / 125346 lookups)",
"strata serve: decode expert cache hit rate: 92.7% (119739 hits / 129184 lookups)",
"strata serve: decode expert cache hit rate: 92.2% (116867 hits / 126770 lookups)",
"strata serve: decode expert cache hit rate: 68.1% (64111 hits / 94116 lookups)",
"strata serve: decode expert cache hit rate: 89.0% (96370 hits / 108259 lookups)",
"strata serve: decode expert cache hit rate: 91.1% (102827 hits / 112875 lookups)"
]
}
@@ -0,0 +1,72 @@
{
"label": "rel-5080",
"extra": [],
"gpus": "0",
"needles": [
{
"tokens": 2726,
"prompt_ms": 2132.8,
"pp_tok_s": 1278.1,
"found": true,
"answer": "meadow-254<|im_end|>"
},
{
"tokens": 8379,
"prompt_ms": 5008.9,
"pp_tok_s": 1672.8,
"found": true,
"answer": "juniper-766<|im_end|>"
},
{
"tokens": 16066,
"prompt_ms": 7855.4,
"pp_tok_s": 2045.2,
"found": true,
"answer": "falcon-174<|im_end|>"
},
{
"tokens": 27743,
"prompt_ms": 13833.9,
"pp_tok_s": 2005.4,
"found": true,
"answer": "willow-474<|im_end|>"
}
],
"chat": {
"turn1_tokens": 6584,
"turn1_ms": 3298.2,
"turn2_tokens": 6660,
"reused": 6632,
"turn2_ms": 197.9,
"answer": "lantern-417<|im_end|>",
"ok": true
},
"decode": [
{
"name": "story",
"tok_s": 83.2,
"hit_rate": 0.8418683557684931,
"text": "The wind didn\u2019t just blow; it screamed.\n\nElias had lived in the tower for thirty years, and he knew the difference between a gale and a hurricane by the way the glass rattled in its frame. Tonight, it was the latter. The sea was a churning cauldron of black ink and white foam, throwing itself against the granite base with the force of a battering ram.\n\nInside the lantern room, the air was thick with the smell of kerosene and old brass. Elias wiped his hands on his woolen trousers, leaving faint, oily smears on the fabric. He checked the clock. 11:42 PM. The rotation was due.\n\nHe reached for the crank handle, a heavy iron wheel worn smooth by decades of his own palms. With a grunt that matched the strain of his shoulders, he began to turn it. *Click. Click. Click.* The gears engaged, and the massive Fresnel lens began its slow"
},
{
"name": "code",
"tok_s": 88.2,
"hit_rate": 0.7011385968111186,
"text": "```python\nimport csv\nfrom typing import List, Dict, Any\n\n\ndef parse_csv_to_dicts(filepath: str) -> List[Dict[str, Any]]:\n \"\"\"\n Parse a CSV file into a list of dictionaries.\n \n Each dictionary represents a row, with keys from the header and values from the row.\n \n Args:\n filepath: Path to the CSV file.\n \n Returns:\n A list of dictionaries, one per row (excluding the header).\n \"\"\"\n result: List[Dict[str, Any]] = []\n with open(filepath, 'r', newline='', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n for row in reader:\n result.append(dict(row))\n return result\n\n\n# Tests\ndef test_parse_csv_to_dicts():\n import tempfile\n import os\n \n # Create a temporary CSV file\n with tempfile.NamedTemporaryFile"
}
],
"engine_lines": [
"strata generate: expert cache auto: 9.15 GiB free, 700 MiB reserved -> 3415 slots",
"strata generate: expert cache 4293 slots, 8.17 GiB of VRAM; policy is",
"strata serve: the prompt path borrows 2343 cache slots (4.42 GiB)",
"strata serve: decode expert cache hit rate: 59.6% (2749 hits / 4610 lookups)",
"strata serve: decode expert cache hit rate: 64.2% (2136 hits / 3329 lookups)",
"strata serve: decode expert cache hit rate: 67.3% (2309 hits / 3432 lookups)",
"strata serve: decode expert cache hit rate: 72.5% (2539 hits / 3504 lookups)",
"strata serve: decode expert cache hit rate: 77.3% (2817 hits / 3642 lookups)",
"strata serve: decode expert cache hit rate: 75.3% (16500 hits / 21916 lookups)",
"strata serve: decode expert cache hit rate: 80.0% (2978 hits / 3723 lookups)",
"strata serve: decode expert cache hit rate: 84.2% (98752 hits / 117301 lookups)",
"strata serve: decode expert cache hit rate: 70.1% (69215 hits / 98718 lookups)"
]
}
@@ -0,0 +1,78 @@
{
"label": "t75-5080",
"extra": [],
"gpus": "0",
"needles": [
{
"tokens": 2726,
"prompt_ms": 2158.8,
"pp_tok_s": 1262.7,
"found": true,
"answer": "meadow-254<|im_end|>"
},
{
"tokens": 8379,
"prompt_ms": 5060.0,
"pp_tok_s": 1655.9,
"found": true,
"answer": "juniper-766<|im_end|>"
}
],
"chat": {
"turn1_tokens": 6584,
"turn1_ms": 3389.7,
"turn2_tokens": 6660,
"reused": 6632,
"turn2_ms": 213.5,
"answer": "lantern-417<|im_end|>",
"ok": true
},
"decode": [
{
"name": "story",
"tok_s": 80.4,
"reps": [
80.4
],
"hit_rate": 0.855538332528651,
"text": "The wind didn\u2019t just blow; it screamed.\n\nElias had lived in the tower for thirty years, and he knew the difference between a gale and a hurricane by the way the glass rattled in its frame. Tonight, it was the latter. The sea was a churning black void, punctuated by whitecaps that looked like teeth.\n\nHe climbed the spiral stairs, his boots heavy with salt and exhaustion. The iron steps groaned under his weight, a sound that had become so familiar to him that he could sleep to it. But tonight, he wasn\u2019t sleeping. He was working.\n\nThe lens was the heart of the lighthouse. A massive, rotating Fresnel lens, a cathedral of glass and brass that turned with a mechanical hum. Elias checked the clockwork mechanism. It was smooth, rhythmic, a heartbeat in the dark. But the light itself was failing.\n\nHe climbed higher, into the lantern room. The wind battered the glass panes,"
},
{
"name": "code",
"tok_s": 87.5,
"reps": [
87.5
],
"hit_rate": 0.7075461363025227,
"text": "```python\nimport csv\nfrom typing import List, Dict, Any\n\n\ndef parse_csv_to_dicts(filepath: str) -> List[Dict[str, Any]]:\n \"\"\"\n Parse a CSV file into a list of dictionaries.\n \n Each dictionary represents a row, with keys from the header and values from the row.\n \n Args:\n filepath: Path to the CSV file.\n \n Returns:\n A list of dictionaries, one per row (excluding the header).\n \"\"\"\n result: List[Dict[str, Any]] = []\n with open(filepath, 'r', newline='', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n for row in reader:\n result.append(dict(row))\n return result\n\n\n# Tests\ndef test_parse_csv_to_dicts():\n import tempfile\n import os\n \n # Create a temporary CSV file\n with tempfile.NamedTemporaryFile"
}
],
"stage_timing": [
"strata serve: stage 0: 39 windows; per window: wait for the GPU 17.252 ms, pool + plan 8.473 ms, host staging 0.611 ms, commit 0.525 ms",
"strata serve: stage 0: 47 windows; per window: wait for the GPU 18.018 ms, pool + plan 8.616 ms, host staging 0.607 ms, commit 0.534 ms",
"strata serve: stage 0: 169 windows; per window: wait for the GPU 15.223 ms, pool + plan 5.717 ms, host staging 0.688 ms, commit 0.467 ms",
"strata serve: stage 0: 243 windows; per window: wait for the GPU 16.500 ms, pool + plan 7.316 ms, host staging 0.742 ms, commit 0.481 ms"
],
"engine_lines": [
"strata generate: expert cache auto: 9.15 GiB free, 700 MiB reserved -> 3415 slots",
"strata generate: expert cache 4294 slots, 8.18 GiB of VRAM; policy is",
"strata serve: the prompt path borrows 2343 cache slots (4.42 GiB)",
"strata serve: decode expert cache hit rate: 59.6% (2749 hits / 4610 lookups)",
"strata serve: decode expert cache hit rate: 64.2% (2136 hits / 3329 lookups)",
"strata serve: decode expert cache hit rate: 67.3% (2309 hits / 3432 lookups)",
"strata serve: decode expert cache hit rate: 75.5% (16316 hits / 21602 lookups)",
"strata serve: decode expert cache hit rate: 74.4% (2660 hits / 3573 lookups)",
"strata serve: decode expert cache hit rate: 83.4% (92664 hits / 111086 lookups)",
"strata serve: decode expert cache hit rate: 72.0% (70946 hits / 98601 lookups)",
"strata generate: expert cache auto: 9.15 GiB free, 700 MiB reserved -> 3415 slots",
"strata generate: expert cache 4293 slots, 8.17 GiB of VRAM; policy is",
"strata serve: the prompt path borrows 2343 cache slots (4.42 GiB)",
"strata serve: decode expert cache hit rate: 59.6% (2749 hits / 4610 lookups)",
"strata serve: decode expert cache hit rate: 64.2% (2136 hits / 3329 lookups)",
"strata serve: decode expert cache hit rate: 67.3% (2309 hits / 3432 lookups)",
"strata serve: decode expert cache hit rate: 75.5% (16316 hits / 21602 lookups)",
"strata serve: decode expert cache hit rate: 74.4% (2659 hits / 3572 lookups)",
"strata serve: decode expert cache hit rate: 85.6% (101004 hits / 118059 lookups)",
"strata serve: decode expert cache hit rate: 70.8% (71044 hits / 100409 lookups)"
]
}
@@ -0,0 +1,103 @@
{
"label": "v-x2-auto",
"extra": [
"--layer-split",
"auto"
],
"gpus": "0,2",
"needles": [
{
"tokens": 2726,
"prompt_ms": 2577.0,
"pp_tok_s": 1057.8,
"found": true,
"answer": "meadow-254<|im_end|>"
},
{
"tokens": 8379,
"prompt_ms": 4433.5,
"pp_tok_s": 1889.9,
"found": true,
"answer": "juniper-766<|im_end|>"
},
{
"tokens": 16066,
"prompt_ms": 7885.8,
"pp_tok_s": 2037.3,
"found": true,
"answer": "falcon-174<|im_end|>"
},
{
"tokens": 27743,
"prompt_ms": 13384.6,
"pp_tok_s": 2072.8,
"found": true,
"answer": "willow-474<|im_end|>"
}
],
"chat": {
"turn1_tokens": 6584,
"turn1_ms": 3667.5,
"turn2_tokens": 6660,
"reused": 6632,
"turn2_ms": 253.8,
"answer": "lantern-417<|im_end|>",
"ok": true
},
"decode": [
{
"name": "story",
"tok_s": 79.5,
"reps": [
78.9,
79.0,
80.7
],
"hit_rate": 0.9969903443250137,
"text": "The wind didn\u2019t just blow; it screamed.\n\nElias pulled his woolen collar up to his chin, the rough fabric scratching against his stubble. Outside the thick, salt-crusted glass of the lantern room, the Atlantic was a churning cauldron of black and white foam. Rain lashed against the panes in horizontal sheets, blurring the world into a smear of gray and shadow.\n\nInside, however, it was warm. The smell of kerosene and old wood hung heavy in the air, a scent that had defined his life for thirty years.\n\nElias was not a man of many words. He was a man of many tasks. His hands, mapped with scars from rope burns and broken glass, moved with a rhythm that predated his memory. He checked the clockwork mechanism of the Fresnel lens, listening for the faint, rhythmic *tick-tick-tick* that signaled the rotation was true. If the rhythm faltered, ships"
},
{
"name": "code",
"tok_s": 108.9,
"reps": [
103.3,
113.7,
109.5
],
"hit_rate": 0.9971795386830036,
"text": "```python\nimport csv\nfrom typing import List, Dict, Any\n\n\ndef parse_csv_to_dicts(filepath: str) -> List[Dict[str, Any]]:\n \"\"\"\n Parse a CSV file into a list of dictionaries.\n \n Each dictionary represents a row, with keys from the header and values from the row.\n \n Args:\n filepath: Path to the CSV file.\n \n Returns:\n A list of dictionaries, one per row (excluding the header).\n \"\"\"\n result: List[Dict[str, Any]] = []\n with open(filepath, 'r', newline='', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n for row in reader:\n result.append(dict(row))\n return result\n\n\n# Tests\ndef test_parse_csv_to_dicts():\n import tempfile\n import os\n \n # Create a temporary CSV file\n with tempfile.NamedTemporaryFile"
}
],
"stage_timing": [
"strata serve: stage 0: 542 windows; per window: wait for the GPU 7.167 ms, pool + plan 0.572 ms, host staging 0.550 ms, commit 0.232 ms",
"strata serve: stage 1: 542 windows; per window: wait for the GPU 12.855 ms, pool + plan 0.250 ms, host staging 0.000 ms, commit 0.334 ms",
"strata serve: stage 0: 610 windows; per window: wait for the GPU 7.276 ms, pool + plan 0.559 ms, host staging 0.493 ms, commit 0.234 ms",
"strata serve: stage 1: 610 windows; per window: wait for the GPU 13.048 ms, pool + plan 0.258 ms, host staging 0.000 ms, commit 0.337 ms"
],
"engine_lines": [
"strata generate: layer split across 2 GPUs: CUDA0, then CUDA1 (split auto)",
"strata generate: layer split: CUDA1 PCIe probe 6.4 GB/s -> pcie_frac 0.14",
"strata generate: layer split: CUDA1 holds its weights, session and head; 18.13 GiB free",
"strata generate: layer split auto: CUDA0 84 SMs at 2.81 GHz -> 0.31 ms per layer, 8.30 GiB for experts",
"strata generate: layer split auto: CUDA1 82 SMs at 1.86 GHz -> 0.48 ms per layer, 14.14 GiB for experts",
"strata generate: layer split auto: K=22 - predicted 19.7 ms per decode window; the caches hold 11298 of 12288 profiled pairs (~99.7% of the routed mass)",
"strata generate: expert cache auto: 10.46 GiB free, 700 MiB reserved -> 3345 slots",
"strata generate: expert cache 4640 slots, 8.29 GiB of VRAM; policy is",
"strata generate: layer split: CUDA1 runs layers 22-47, expert cache 6656 slots (13.34 GiB), 6656 of its 6656 profiled pairs; slot 0 verified",
"strata generate: layer split: CUDA0 runs layers 0-21",
"strata serve: the prompt path allocates its own buffers (too few cache slots to borrow)",
"strata serve: layer split: layers 0-21 (CUDA0), 22-47 (CUDA1), one hand-off per window",
"strata serve: decode expert cache hit rate: 97.7% (5609 hits / 5739 lookups)",
"strata serve: decode expert cache hit rate: 98.5% (4241 hits / 4306 lookups)",
"strata serve: decode expert cache hit rate: 98.9% (4262 hits / 4309 lookups)",
"strata serve: decode expert cache hit rate: 99.4% (4290 hits / 4315 lookups)",
"strata serve: decode expert cache hit rate: 99.6% (4303 hits / 4319 lookups)",
"strata serve: decode expert cache hit rate: 98.8% (28379 hits / 28721 lookups)",
"strata serve: decode expert cache hit rate: 99.3% (4285 hits / 4314 lookups)",
"strata serve: decode expert cache hit rate: 98.5% (136232 hits / 138256 lookups)",
"strata serve: decode expert cache hit rate: 99.5% (135540 hits / 136216 lookups)",
"strata serve: decode expert cache hit rate: 99.7% (136812 hits / 137225 lookups)",
"strata serve: decode expert cache hit rate: 98.4% (118437 hits / 120406 lookups)",
"strata serve: decode expert cache hit rate: 99.7% (112836 hits / 113230 lookups)"
]
}
@@ -0,0 +1,88 @@
{
"label": "v-x2r-auto",
"extra": [
"--layer-split",
"auto"
],
"gpus": "2,0",
"needles": [
{
"tokens": 16066,
"prompt_ms": 9610.0,
"pp_tok_s": 1671.8,
"found": true,
"answer": "meadow-254<|im_end|>"
},
{
"tokens": 27743,
"prompt_ms": 14786.1,
"pp_tok_s": 1876.3,
"found": true,
"answer": "juniper-766<|im_end|>"
}
],
"chat": {
"turn1_tokens": 6584,
"turn1_ms": 3850.8,
"turn2_tokens": 6660,
"reused": 6632,
"turn2_ms": 245.0,
"answer": "lantern-417<|im_end|>",
"ok": true
},
"decode": [
{
"name": "story",
"tok_s": 90.3,
"reps": [
91.0,
97.0,
82.8
],
"hit_rate": 0.993905472636816,
"text": "The wind didn\u2019t just blow; it screamed. It tore through the iron gallery of the Blackwood Point Lighthouse, rattling the glass panes like a prisoner shaking its bars.\n\nElias Thorne was seventy years old, and he had been the keeper for forty of them. He knew every creak of the spiral staircase, every draft that slipped under the door, and every shift in the barometric pressure that preceded a storm. He was not merely a worker; he was the heartbeat of the coast.\n\nIt was November, the month of grey seas and shorter days. The fog had rolled in thick, a wall of white so dense that even the beam of the Fresnel lens seemed to struggle against it. Elias stood in the lantern room, his hands roughened by salt and rope, adjusting the rotation mechanism. The great lens, a cathedral of glass and brass, began its slow, rhythmic sweep. *Flash. Dark. Flash. Dark.*\n\nHe checked the log"
},
{
"name": "code",
"tok_s": 105.1,
"reps": [
98.4,
109.9,
107.0
],
"hit_rate": 0.9949900793650793,
"text": "```python\nimport csv\nfrom typing import List, Dict, Any\n\n\ndef parse_csv_to_dicts(filepath: str) -> List[Dict[str, Any]]:\n \"\"\"\n Parse a CSV file into a list of dictionaries.\n \n Each dictionary represents a row, with keys from the header and values from the row.\n \n Args:\n filepath: Path to the CSV file.\n \n Returns:\n A list of dictionaries, one per row (excluding the header).\n \"\"\"\n result: List[Dict[str, Any]] = []\n with open(filepath, 'r', newline='', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n for row in reader:\n result.append(dict(row))\n return result\n\n\n# Tests\ndef test_parse_csv_to_dicts():\n import tempfile\n import os\n \n # Create a temporary CSV file\n with tempfile.NamedTemporaryFile"
}
],
"stage_timing": [
"strata serve: stage 0: 492 windows; per window: wait for the GPU 15.016 ms, pool + plan 0.297 ms, host staging 0.517 ms, commit 0.404 ms",
"strata serve: stage 1: 492 windows; per window: wait for the GPU 5.775 ms, pool + plan 0.854 ms, host staging 0.001 ms, commit 0.210 ms",
"strata serve: stage 0: 561 windows; per window: wait for the GPU 15.257 ms, pool + plan 0.306 ms, host staging 0.455 ms, commit 0.408 ms",
"strata serve: stage 1: 561 windows; per window: wait for the GPU 5.852 ms, pool + plan 0.820 ms, host staging 0.001 ms, commit 0.210 ms"
],
"engine_lines": [
"strata generate: layer split across 2 GPUs: CUDA0, then CUDA1 (split auto)",
"strata generate: layer split: CUDA1 PCIe probe 57.7 GB/s -> pcie_frac 0.55",
"strata generate: layer split: CUDA1 holds its weights, session and head; 9.98 GiB free",
"strata generate: layer split auto: CUDA0 82 SMs at 1.86 GHz -> 0.48 ms per layer, 16.45 GiB for experts",
"strata generate: layer split auto: CUDA1 84 SMs at 2.81 GHz -> 0.31 ms per layer, 5.99 GiB for experts",
"strata generate: layer split auto: K=30 - predicted 20.8 ms per decode window; the caches hold 10684 of 12288 profiled pairs (~99.5% of the routed mass)",
"strata generate: expert cache auto: 18.62 GiB free, 700 MiB reserved -> 6634 slots",
"strata generate: expert cache 7680 slots, 14.23 GiB of VRAM; policy is",
"strata generate: layer split: CUDA1 runs layers 30-47, expert cache 3004 slots (5.98 GiB), 3004 of its 4608 profiled pairs; slot 0 verified",
"strata generate: layer split: CUDA0 runs layers 0-29",
"strata serve: the prompt path allocates its own buffers (too few cache slots to borrow)",
"strata serve: layer split: layers 0-29 (CUDA0), 30-47 (CUDA1), one hand-off per window",
"strata serve: decode expert cache hit rate: 93.2% (5367 hits / 5760 lookups)",
"strata serve: decode expert cache hit rate: 93.7% (4049 hits / 4320 lookups)",
"strata serve: decode expert cache hit rate: 96.7% (4178 hits / 4320 lookups)",
"strata serve: decode expert cache hit rate: 97.7% (26272 hits / 26880 lookups)",
"strata serve: decode expert cache hit rate: 97.1% (4194 hits / 4320 lookups)",
"strata serve: decode expert cache hit rate: 97.8% (110748 hits / 113280 lookups)",
"strata serve: decode expert cache hit rate: 99.6% (113787 hits / 114240 lookups)",
"strata serve: decode expert cache hit rate: 99.4% (127856 hits / 128640 lookups)",
"strata serve: decode expert cache hit rate: 96.3% (116528 hits / 120960 lookups)",
"strata serve: decode expert cache hit rate: 99.4% (116423 hits / 117120 lookups)",
"strata serve: decode expert cache hit rate: 99.5% (120354 hits / 120960 lookups)"
]
}
@@ -0,0 +1,89 @@
{
"label": "v-x3-auto",
"extra": [
"--layer-split",
"auto"
],
"gpus": "0,1,2",
"needles": [
{
"tokens": 16066,
"prompt_ms": 9525.4,
"pp_tok_s": 1686.6,
"found": true,
"answer": "meadow-254<|im_end|>"
},
{
"tokens": 27743,
"prompt_ms": 15017.1,
"pp_tok_s": 1847.4,
"found": true,
"answer": "juniper-766<|im_end|>"
}
],
"chat": {
"turn1_tokens": 6584,
"turn1_ms": 4039.6,
"turn2_tokens": 6660,
"reused": 6631,
"turn2_ms": 360.2,
"answer": "lantern-417<|im_end|>",
"ok": true
},
"decode": [
{
"name": "story",
"tok_s": 67.6,
"reps": [
59.8,
83.1,
59.9
],
"hit_rate": 0.9967581085879247,
"text": "The wind didn\u2019t just blow; it screamed. It tore through the iron gallery of the Blackwood Point Lighthouse, rattling the glass panes like a prisoner shaking its bars.\n\nElias Thorne was seventy years old, and he had been the keeper for forty of them. He knew every creak in the spiral staircase, every draft that slipped under the door, and every shift in the weather that the barometer couldn\u2019t quite predict. He knew that when the fog rolled in thick enough to swallow the horizon, the world ceased to exist. There was only the light, the stone, and him.\n\nIt was November, the month of storms. The sea was a churning cauldron of grey and white, throwing itself against the granite base with the force of a battering ram. Elias stood in the lantern room, his hands rough as sandpaper, adjusting the massive Fresnel lens. The rotation mechanism groaned, a sound like a ship\u2019s anchor chain being h"
},
{
"name": "code",
"tok_s": 89.6,
"reps": [
81.0,
104.8,
83.0
],
"hit_rate": 0.9975041387598007,
"text": "```python\nimport csv\nfrom typing import List, Dict, Any\n\n\ndef parse_csv_to_dicts(filepath: str) -> List[Dict[str, Any]]:\n \"\"\"\n Parse a CSV file into a list of dictionaries.\n \n Each dictionary represents a row, with keys from the header and values from the row.\n \n Args:\n filepath: Path to the CSV file.\n \n Returns:\n A list of dictionaries, one per row (excluding the header).\n \"\"\"\n result: List[Dict[str, Any]] = []\n with open(filepath, 'r', newline='', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n for row in reader:\n result.append(dict(row))\n return result\n\n\n# Tests\ndef test_parse_csv_to_dicts():\n import tempfile\n import os\n \n # Create a temporary CSV file\n with tempfile.NamedTemporaryFile"
}
],
"stage_timing": [
"strata serve: stage 2: 522 windows; per window: wait for the GPU 12.405 ms, pool + plan 0.242 ms, host staging 0.006 ms, commit 0.375 ms",
"strata serve: stage 0: 590 windows; per window: wait for the GPU 7.387 ms, pool + plan 0.549 ms, host staging 0.517 ms, commit 0.248 ms",
"strata serve: stage 1: 590 windows; per window: wait for the GPU 3.071 ms, pool + plan 0.011 ms, host staging 0.000 ms, commit 0.606 ms",
"strata serve: stage 2: 590 windows; per window: wait for the GPU 12.592 ms, pool + plan 0.250 ms, host staging 0.006 ms, commit 0.377 ms"
],
"engine_lines": [
"strata generate: layer split across 3 GPUs: CUDA0, then CUDA1,2 (split auto)",
"strata generate: layer split: CUDA1 PCIe probe 3.2 GB/s -> pcie_frac 0.00",
"strata generate: layer split: CUDA1 holds its weights, session; 16.67 GiB free",
"strata generate: layer split: CUDA2 PCIe probe 6.4 GB/s -> pcie_frac 0.14",
"strata generate: layer split: CUDA2 holds its weights, session and head; 18.13 GiB free",
"strata generate: layer split auto: CUDA0 84 SMs at 2.81 GHz -> 0.31 ms per layer, 8.30 GiB for experts",
"strata generate: layer split auto: CUDA1 68 SMs at 1.54 GHz -> 0.69 ms per layer, 13.50 GiB for experts",
"strata generate: layer split auto: CUDA2 82 SMs at 1.86 GHz -> 0.48 ms per layer, 14.14 GiB for experts",
"strata generate: layer split auto: K=22,23 - predicted 19.9 ms per decode window; the caches hold 11298 of 12288 profiled pairs (~99.7% of the routed mass)",
"strata generate: expert cache auto: 10.46 GiB free, 700 MiB reserved -> 3345 slots",
"strata generate: expert cache 4640 slots, 8.29 GiB of VRAM; policy is",
"strata generate: layer split: CUDA1 runs layers 22-22, expert cache 256 slots (0.52 GiB), 256 of its 256 profiled pairs; slot 0 verified",
"strata generate: layer split: CUDA2 runs layers 23-47, expert cache 6400 slots (12.82 GiB), 6400 of its 6400 profiled pairs; slot 0 verified",
"strata generate: layer split: CUDA0 runs layers 0-21",
"strata serve: the prompt path allocates its own buffers (too few cache slots to borrow)",
"strata serve: layer split: layers 0-21 (CUDA0), 22-22 (CUDA1), 23-47 (CUDA2), one hand-off per window",
"strata serve: decode expert cache hit rate: 97.7% (5609 hits / 5739 lookups)",
"strata serve: decode expert cache hit rate: 98.7% (4256 hits / 4310 lookups)",
"strata serve: decode expert cache hit rate: 99.1% (4278 hits / 4315 lookups)",
"strata serve: decode expert cache hit rate: 99.0% (29862 hits / 30170 lookups)",
"strata serve: decode expert cache hit rate: 98.9% (4264 hits / 4313 lookups)",
"strata serve: decode expert cache hit rate: 98.6% (134418 hits / 136339 lookups)",
"strata serve: decode expert cache hit rate: 99.5% (116986 hits / 117525 lookups)",
"strata serve: decode expert cache hit rate: 99.7% (129134 hits / 129554 lookups)"
]
}
@@ -0,0 +1,84 @@
{
"label": "x2-auto-c1024",
"extra": [
"--layer-split",
"auto",
"--prefill",
"1024"
],
"gpus": "0,2",
"needles": [
{
"tokens": 2726,
"prompt_ms": 2648.7,
"pp_tok_s": 1029.2,
"found": true,
"answer": "meadow-254<|im_end|>"
},
{
"tokens": 8379,
"prompt_ms": 5547.0,
"pp_tok_s": 1510.5,
"found": true,
"answer": "juniper-766<|im_end|>"
},
{
"tokens": 16066,
"prompt_ms": 10123.3,
"pp_tok_s": 1587.0,
"found": true,
"answer": "falcon-174<|im_end|>"
},
{
"tokens": 27743,
"prompt_ms": 17652.0,
"pp_tok_s": 1571.7,
"found": true,
"answer": "willow-474<|im_end|>"
}
],
"chat": {
"turn1_tokens": 6584,
"turn1_ms": 4379.9,
"turn2_tokens": 6660,
"reused": 6632,
"turn2_ms": 235.5,
"answer": "lantern-417<|im_end|>",
"ok": true
},
"decode": [
{
"name": "story",
"tok_s": 82.7,
"hit_rate": 1.0,
"text": "The wind didn\u2019t just blow; it screamed. It tore through the iron gallery of the Blackwood Point Lighthouse, rattling the glass panes like a prisoner shaking its bars.\n\nElias Thorne was seventy years old, and he had been the keeper for forty of them. He knew every creak in the spiral staircase, every draft that slipped under the door, and every shift in the weather that the barometer couldn\u2019t quite predict. He knew that when the fog rolled in thick enough to swallow the horizon, the world ceased to exist. There was only the light, the stone, and him.\n\nIt was November, the month of storms. The sea was a churning cauldron of grey and white, throwing itself against the granite base with a violence that made the floorboards tremble. Elias stood by the lantern room, his hands wrapped around a mug of coffee that had gone cold hours ago. He wasn\u2019t drinking it. He was holding it for warmth."
},
{
"name": "code",
"tok_s": 111.1,
"hit_rate": 1.0,
"text": "```python\nimport csv\nfrom typing import List, Dict, Any\n\n\ndef parse_csv(file_path: str) -> List[Dict[str, Any]]:\n \"\"\"\n Parse a CSV file into a list of dictionaries.\n \n Each dictionary represents a row, with keys being the header values\n and values being the corresponding cell values.\n \n Args:\n file_path: Path to the CSV file.\n \n Returns:\n A list of dictionaries, one per row (excluding the header).\n \"\"\"\n result = []\n with open(file_path, 'r', newline='', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n for row in reader:\n result.append(dict(row))\n return result\n\n\n# Tests\ndef test_parse_csv_basic():\n \"\"\"Test basic CSV parsing with simple data.\"\"\"\n import tempfile\n import os\n \n # Create a temporary CSV"
}
],
"engine_lines": [
"strata generate: layer split across 2 GPUs: CUDA0, then CUDA1 (split auto)",
"strata generate: layer split: CUDA1 PCIe probe 6.4 GB/s -> pcie_frac 0.14",
"strata generate: layer split: CUDA1 holds its weights, session and head; 18.13 GiB free",
"strata generate: layer split auto: K=19 - the caches hold 12288 of 12288 profiled pairs (fullest device 99%)",
"strata generate: expert cache auto: 10.46 GiB free, 700 MiB reserved -> 3613 slots",
"strata generate: expert cache 4864 slots, 8.76 GiB of VRAM; policy is",
"strata generate: layer split: CUDA1 runs layers 19-47, expert cache 7424 slots (14.66 GiB), 7424 of its 7424 profiled pairs; slot 0 verified",
"strata generate: layer split: CUDA0 runs layers 0-18",
"strata serve: the prompt path allocates its own buffers (too few cache slots to borrow)",
"strata serve: layer split: layers 0-18 (CUDA0), 19-47 (CUDA1), one hand-off per window",
"strata serve: decode expert cache hit rate: 100.0% (5760 hits / 5760 lookups)",
"strata serve: decode expert cache hit rate: 100.0% (4320 hits / 4320 lookups)",
"strata serve: decode expert cache hit rate: 100.0% (4320 hits / 4320 lookups)",
"strata serve: decode expert cache hit rate: 100.0% (4320 hits / 4320 lookups)",
"strata serve: decode expert cache hit rate: 100.0% (4320 hits / 4320 lookups)",
"strata serve: decode expert cache hit rate: 100.0% (26400 hits / 26400 lookups)",
"strata serve: decode expert cache hit rate: 100.0% (4320 hits / 4320 lookups)",
"strata serve: decode expert cache hit rate: 100.0% (130080 hits / 130080 lookups)",
"strata serve: decode expert cache hit rate: 100.0% (110400 hits / 110400 lookups)"
]
}
@@ -0,0 +1,82 @@
{
"label": "x2-auto",
"extra": [
"--layer-split",
"auto"
],
"gpus": "0,2",
"needles": [
{
"tokens": 2726,
"prompt_ms": 2711.7,
"pp_tok_s": 1005.3,
"found": true,
"answer": "meadow-254<|im_end|>"
},
{
"tokens": 8379,
"prompt_ms": 5010.3,
"pp_tok_s": 1672.4,
"found": true,
"answer": "juniper-766<|im_end|>"
},
{
"tokens": 16066,
"prompt_ms": 8994.8,
"pp_tok_s": 1786.1,
"found": true,
"answer": "falcon-174<|im_end|>"
},
{
"tokens": 27743,
"prompt_ms": 15497.2,
"pp_tok_s": 1790.2,
"found": true,
"answer": "willow-474<|im_end|>"
}
],
"chat": {
"turn1_tokens": 6584,
"turn1_ms": 4050.3,
"turn2_tokens": 6660,
"reused": 6632,
"turn2_ms": 249.7,
"answer": "lantern-417<|im_end|>",
"ok": true
},
"decode": [
{
"name": "story",
"tok_s": 83.0,
"hit_rate": 0.9920220447608565,
"text": "The wind didn\u2019t just blow; it screamed.\n\nElias had lived in the tower for thirty years, and he knew the difference between a gale and a hurricane by the way the glass rattled in its frame. Tonight, it was the latter. The sea was a churning cauldron of black ink and white foam, throwing itself against the granite base with the force of a battering ram.\n\nInside the lantern room, the air was thick with the smell of kerosene and old brass. Elias wiped his hands on his woolen trousers, leaving faint, oily smears on the fabric. He checked the clock. 11:42 PM. The rotation was due.\n\nHe reached for the crank handle, a heavy iron wheel worn smooth by decades of his own palms. With a grunt that matched the strain of his shoulders, he began to turn it. The massive Fresnel lens, a cathedral of glass and crystal, began its slow, hypnotic sweep"
},
{
"name": "code",
"tok_s": 101.8,
"hit_rate": 0.991767530342175,
"text": "```python\nimport csv\nfrom typing import List, Dict, Any\n\n\ndef parse_csv_to_dicts(filepath: str) -> List[Dict[str, Any]]:\n \"\"\"\n Parse a CSV file into a list of dictionaries.\n \n Each dictionary represents a row, with keys from the header and values from the row.\n \n Args:\n filepath: Path to the CSV file.\n \n Returns:\n A list of dictionaries, one per row (excluding the header).\n \"\"\"\n result: List[Dict[str, Any]] = []\n with open(filepath, 'r', newline='', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n for row in reader:\n result.append(dict(row))\n return result\n\n\n# Tests\ndef test_parse_csv_to_dicts():\n import tempfile\n import os\n \n # Create a temporary CSV file\n with tempfile.NamedTemporaryFile"
}
],
"engine_lines": [
"strata generate: layer split across 2 GPUs: CUDA0, then CUDA1 (split auto)",
"strata generate: layer split: CUDA1 PCIe probe 6.4 GB/s -> pcie_frac 0.14",
"strata generate: layer split: CUDA1 holds its weights, session and head; 18.13 GiB free",
"strata generate: layer split auto: K=19 - the caches hold 11767 of 12288 profiled pairs (fullest device 100%)",
"strata generate: expert cache auto: 10.46 GiB free, 700 MiB reserved -> 3345 slots",
"strata generate: expert cache 4606 slots, 8.29 GiB of VRAM; policy is",
"strata generate: layer split: CUDA1 runs layers 19-47, expert cache 7160 slots (14.14 GiB), 7160 of its 7424 profiled pairs; slot 0 verified",
"strata generate: layer split: CUDA0 runs layers 0-18",
"strata serve: the prompt path allocates its own buffers (too few cache slots to borrow)",
"strata serve: layer split: layers 0-18 (CUDA0), 19-47 (CUDA1), one hand-off per window",
"strata serve: decode expert cache hit rate: 98.5% (6141 hits / 6234 lookups)",
"strata serve: decode expert cache hit rate: 98.9% (3797 hits / 3838 lookups)",
"strata serve: decode expert cache hit rate: 99.1% (3804 hits / 3838 lookups)",
"strata serve: decode expert cache hit rate: 99.4% (3816 hits / 3839 lookups)",
"strata serve: decode expert cache hit rate: 99.6% (3825 hits / 3839 lookups)",
"strata serve: decode expert cache hit rate: 99.5% (27693 hits / 27832 lookups)",
"strata serve: decode expert cache hit rate: 99.4% (3815 hits / 3839 lookups)",
"strata serve: decode expert cache hit rate: 99.2% (127081 hits / 128103 lookups)",
"strata serve: decode expert cache hit rate: 99.2% (119386 hits / 120377 lookups)"
]
}
@@ -0,0 +1,81 @@
{
"label": "x2-k16",
"extra": [
"--layer-split",
"16"
],
"gpus": "0,2",
"needles": [
{
"tokens": 2726,
"prompt_ms": 2987.1,
"pp_tok_s": 912.6,
"found": true,
"answer": "meadow-254<|im_end|>"
},
{
"tokens": 8379,
"prompt_ms": 5908.6,
"pp_tok_s": 1418.1,
"found": true,
"answer": "juniper-766<|im_end|>"
},
{
"tokens": 16066,
"prompt_ms": 10655.8,
"pp_tok_s": 1507.7,
"found": true,
"answer": "falcon-174<|im_end|>"
},
{
"tokens": 27743,
"prompt_ms": 18484.6,
"pp_tok_s": 1500.9,
"found": true,
"answer": "willow-474<|im_end|>"
}
],
"chat": {
"turn1_tokens": 6584,
"turn1_ms": 4790.8,
"turn2_tokens": 6660,
"reused": 6632,
"turn2_ms": 243.3,
"answer": "lantern-417<|im_end|>",
"ok": true
},
"decode": [
{
"name": "story",
"tok_s": 71.4,
"hit_rate": 0.9814261642156863,
"text": "The wind didn\u2019t just blow; it screamed. It clawed at the granite base of the tower, trying to pry the lighthouse from its bedrock. Inside, however, the air was still, smelling faintly of kerosene and old paper.\n\nElias had been the keeper of the Point for thirty-two years. He knew every creak in the floorboards, every draft that slipped through the window seals, and exactly how much oil the great Fresnel lens needed to turn its slow, hypnotic rotation.\n\nIt was 2:00 AM. The storm was at its peak. The sea was a chaotic black mirror, whipped into whitecaps that looked like jagged teeth.\n\nElias climbed the spiral stairs, his hand gripping the cold iron railing. His boots, heavy with salt and wear, thumped a steady rhythm against the stone. *Thump. Thump. Thump.*\n\nAt the top, the lantern room hummed with the vibration of"
},
{
"name": "code",
"tok_s": 86.4,
"hit_rate": 0.9798276439089693,
"text": "```python\nimport csv\nfrom typing import List, Dict, Any\n\n\ndef parse_csv_to_dicts(filepath: str) -> List[Dict[str, Any]]:\n \"\"\"\n Parse a CSV file into a list of dictionaries.\n \n Each dictionary represents a row, with keys from the header and values from the row.\n \n Args:\n filepath: Path to the CSV file.\n \n Returns:\n A list of dictionaries, one per row (excluding the header).\n \"\"\"\n result: List[Dict[str, Any]] = []\n with open(filepath, 'r', newline='', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n for row in reader:\n result.append(dict(row))\n return result\n\n\n# Tests\ndef test_parse_csv_to_dicts():\n import tempfile\n import os\n \n # Create a temporary CSV file\n with tempfile.NamedTemporaryFile"
}
],
"engine_lines": [
"strata generate: layer split across 2 GPUs: CUDA0, then CUDA1 (split 16)",
"strata generate: layer split: CUDA1 PCIe probe 6.4 GB/s -> pcie_frac 0.14",
"strata generate: layer split: CUDA1 holds its weights, session and head; 18.13 GiB free",
"strata generate: expert cache auto: 10.46 GiB free, 700 MiB reserved -> 3345 slots",
"strata generate: expert cache 4096 slots, 7.17 GiB of VRAM; policy is",
"strata generate: layer split: CUDA1 runs layers 16-47, expert cache 7124 slots (14.14 GiB), 7124 of its 8192 profiled pairs; slot 0 verified",
"strata generate: layer split: CUDA0 runs layers 0-15",
"strata serve: the prompt path allocates its own buffers (too few cache slots to borrow)",
"strata serve: layer split: layers 0-15 (CUDA0), 16-47 (CUDA1), one hand-off per window",
"strata serve: decode expert cache hit rate: 97.7% (5626 hits / 5760 lookups)",
"strata serve: decode expert cache hit rate: 98.1% (4237 hits / 4320 lookups)",
"strata serve: decode expert cache hit rate: 98.8% (4269 hits / 4320 lookups)",
"strata serve: decode expert cache hit rate: 99.1% (4279 hits / 4320 lookups)",
"strata serve: decode expert cache hit rate: 99.5% (4298 hits / 4320 lookups)",
"strata serve: decode expert cache hit rate: 98.9% (28471 hits / 28800 lookups)",
"strata serve: decode expert cache hit rate: 99.1% (4282 hits / 4320 lookups)",
"strata serve: decode expert cache hit rate: 98.1% (128135 hits / 130560 lookups)",
"strata serve: decode expert cache hit rate: 98.0% (117109 hits / 119520 lookups)"
]
}
@@ -0,0 +1,81 @@
{
"label": "x2-k22",
"extra": [
"--layer-split",
"22"
],
"gpus": "0,2",
"needles": [
{
"tokens": 2726,
"prompt_ms": 2537.7,
"pp_tok_s": 1074.2,
"found": true,
"answer": "meadow-254<|im_end|>"
},
{
"tokens": 8379,
"prompt_ms": 4440.0,
"pp_tok_s": 1887.2,
"found": true,
"answer": "juniper-766<|im_end|>"
},
{
"tokens": 16066,
"prompt_ms": 7872.5,
"pp_tok_s": 2040.8,
"found": true,
"answer": "falcon-174<|im_end|>"
},
{
"tokens": 27743,
"prompt_ms": 13405.0,
"pp_tok_s": 2069.6,
"found": true,
"answer": "willow-474<|im_end|>"
}
],
"chat": {
"turn1_tokens": 6584,
"turn1_ms": 3610.4,
"turn2_tokens": 6660,
"reused": 6632,
"turn2_ms": 236.0,
"answer": "lantern-417<|im_end|>",
"ok": true
},
"decode": [
{
"name": "story",
"tok_s": 78.1,
"hit_rate": 0.9851122208995093,
"text": "The wind didn\u2019t just blow; it screamed.\n\nElias pulled his wool coat tighter around his shoulders, the fabric stiff with salt and age. He was seventy-three years old, and he had been the keeper of the Blackwood Point Light for forty of those years. The other thirty-three had been spent in a hospital bed, waiting for the end.\n\nEvery night, Elias climbed the one hundred and twelve steps of the spiral staircase. His knees, worn smooth by the friction of his own boots over decades, protested with a sharp ache. He counted them. *One, two, three\u2026* The rhythm was his heartbeat. If he lost the count, he lost the light.\n\nAt the top, the lantern room hummed with the low, electric thrum of the Fresnel lens. It was a cathedral of glass and brass, a machine that had never failed him. Not once. Not in four decades.\n\nHe polished the brass railing with a rag that smelled of lemon and"
},
{
"name": "code",
"tok_s": 104.3,
"hit_rate": 0.9842543225870746,
"text": "```python\nimport csv\nfrom typing import List, Dict, Any\n\n\ndef parse_csv_to_dicts(filepath: str) -> List[Dict[str, Any]]:\n \"\"\"\n Parse a CSV file into a list of dictionaries.\n \n Each dictionary represents a row, with keys from the header and values from the row.\n \n Args:\n filepath: Path to the CSV file.\n \n Returns:\n A list of dictionaries, one per row (excluding the header).\n \"\"\"\n result: List[Dict[str, Any]] = []\n with open(filepath, 'r', newline='', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n for row in reader:\n result.append(dict(row))\n return result\n\n\n# Tests\ndef test_parse_csv_to_dicts():\n import tempfile\n import os\n \n # Create a temporary CSV file\n with tempfile.NamedTemporaryFile"
}
],
"engine_lines": [
"strata generate: layer split across 2 GPUs: CUDA0, then CUDA1 (split 22)",
"strata generate: layer split: CUDA1 PCIe probe 6.4 GB/s -> pcie_frac 0.14",
"strata generate: layer split: CUDA1 holds its weights, session and head; 18.13 GiB free",
"strata generate: expert cache auto: 10.46 GiB free, 700 MiB reserved -> 3345 slots",
"strata generate: expert cache 4640 slots, 8.29 GiB of VRAM; policy is",
"strata generate: layer split: CUDA1 runs layers 22-47, expert cache 6656 slots (13.34 GiB), 6656 of its 6656 profiled pairs; slot 0 verified",
"strata generate: layer split: CUDA0 runs layers 0-21",
"strata serve: the prompt path allocates its own buffers (too few cache slots to borrow)",
"strata serve: layer split: layers 0-21 (CUDA0), 22-47 (CUDA1), one hand-off per window",
"strata serve: decode expert cache hit rate: 97.7% (5609 hits / 5739 lookups)",
"strata serve: decode expert cache hit rate: 98.5% (4241 hits / 4306 lookups)",
"strata serve: decode expert cache hit rate: 98.9% (4262 hits / 4309 lookups)",
"strata serve: decode expert cache hit rate: 99.4% (4290 hits / 4315 lookups)",
"strata serve: decode expert cache hit rate: 99.6% (4303 hits / 4319 lookups)",
"strata serve: decode expert cache hit rate: 98.8% (28379 hits / 28721 lookups)",
"strata serve: decode expert cache hit rate: 99.3% (4285 hits / 4314 lookups)",
"strata serve: decode expert cache hit rate: 98.5% (135713 hits / 137764 lookups)",
"strata serve: decode expert cache hit rate: 98.4% (118518 hits / 120414 lookups)"
]
}
@@ -0,0 +1,82 @@
{
"label": "x2r-auto",
"extra": [
"--layer-split",
"auto"
],
"gpus": "2,0",
"needles": [
{
"tokens": 2726,
"prompt_ms": 2804.2,
"pp_tok_s": 972.1,
"found": true,
"answer": "meadow-254<|im_end|>"
},
{
"tokens": 8379,
"prompt_ms": 5641.7,
"pp_tok_s": 1485.2,
"found": true,
"answer": "juniper-766<|im_end|>"
},
{
"tokens": 16066,
"prompt_ms": 10643.4,
"pp_tok_s": 1509.5,
"found": true,
"answer": "falcon-174<|im_end|>"
},
{
"tokens": 27743,
"prompt_ms": 18363.5,
"pp_tok_s": 1510.8,
"found": true,
"answer": "willow-474<|im_end|>"
}
],
"chat": {
"turn1_tokens": 6584,
"turn1_ms": 4517.2,
"turn2_tokens": 6660,
"reused": 6632,
"turn2_ms": 245.6,
"answer": "lantern-417<|im_end|>",
"ok": true
},
"decode": [
{
"name": "story",
"tok_s": 78.9,
"hit_rate": 0.9913117283950618,
"text": "The wind didn\u2019t just blow; it screamed.\n\nElias pulled his woolen collar up to his chin, the fabric stiff with salt and ice. Outside the thick glass of the lantern room, the Atlantic was a churning cauldron of black and white, the waves crashing against the granite base with a violence that shook the very foundations of the tower. It was November, 1924, and the storm had been raging for three days.\n\nElias was not a man who feared the dark. He was a man who feared the silence.\n\nHe checked the clock on the wall. 11:42 PM. The rotation of the great Fresnel lens was steady, a rhythmic *whirrr-click, whirrr-click* that seemed to sync with his own heartbeat. He had been doing this for forty years. His hands were mapped with scars from rope burns and broken glass, his eyes permanently squinted against the glare of the bulb."
},
{
"name": "code",
"tok_s": 93.4,
"hit_rate": 0.990286044973545,
"text": "```python\nimport csv\nfrom typing import List, Dict, Any\n\n\ndef parse_csv_to_dicts(filepath: str) -> List[Dict[str, Any]]:\n \"\"\"\n Parse a CSV file into a list of dictionaries.\n \n Each dictionary represents a row, with keys from the header and values from the row.\n \n Args:\n filepath: Path to the CSV file.\n \n Returns:\n A list of dictionaries, one per row (excluding the header).\n \"\"\"\n result: List[Dict[str, Any]] = []\n with open(filepath, 'r', newline='', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n for row in reader:\n result.append(dict(row))\n return result\n\n\n# Tests\ndef test_parse_csv_to_dicts():\n import tempfile\n import os\n \n # Create a temporary CSV file\n with tempfile.NamedTemporaryFile"
}
],
"engine_lines": [
"strata generate: layer split across 2 GPUs: CUDA0, then CUDA1 (split auto)",
"strata generate: layer split: CUDA1 PCIe probe 57.7 GB/s -> pcie_frac 0.55",
"strata generate: layer split: CUDA1 holds its weights, session and head; 9.98 GiB free",
"strata generate: layer split auto: K=36 - the caches hold 11775 of 12288 profiled pairs (fullest device 100%)",
"strata generate: expert cache auto: 18.62 GiB free, 700 MiB reserved -> 6634 slots",
"strata generate: expert cache 8828 slots, 16.45 GiB of VRAM; policy is",
"strata generate: layer split: CUDA1 runs layers 36-47, expert cache 2945 slots (5.99 GiB), 2945 of its 3072 profiled pairs; slot 0 verified",
"strata generate: layer split: CUDA0 runs layers 0-35",
"strata serve: the prompt path allocates its own buffers (too few cache slots to borrow)",
"strata serve: layer split: layers 0-35 (CUDA0), 36-47 (CUDA1), one hand-off per window",
"strata serve: decode expert cache hit rate: 98.6% (5678 hits / 5760 lookups)",
"strata serve: decode expert cache hit rate: 99.1% (4283 hits / 4320 lookups)",
"strata serve: decode expert cache hit rate: 99.2% (4285 hits / 4320 lookups)",
"strata serve: decode expert cache hit rate: 99.6% (4303 hits / 4320 lookups)",
"strata serve: decode expert cache hit rate: 99.7% (4306 hits / 4320 lookups)",
"strata serve: decode expert cache hit rate: 99.5% (27700 hits / 27840 lookups)",
"strata serve: decode expert cache hit rate: 99.4% (4295 hits / 4320 lookups)",
"strata serve: decode expert cache hit rate: 99.1% (128474 hits / 129600 lookups)",
"strata serve: decode expert cache hit rate: 99.0% (119785 hits / 120960 lookups)"
]
}
@@ -0,0 +1,99 @@
{
"label": "x2t-auto",
"extra": [
"--layer-split",
"auto"
],
"gpus": "0,1",
"needles": [
{
"tokens": 2726,
"prompt_ms": 4201.1,
"pp_tok_s": 648.9,
"found": true,
"answer": "meadow-254<|im_end|>"
},
{
"tokens": 8379,
"prompt_ms": 9655.6,
"pp_tok_s": 867.8,
"found": true,
"answer": "juniper-766<|im_end|>"
},
{
"tokens": 16066,
"prompt_ms": 17573.8,
"pp_tok_s": 914.2,
"found": true,
"answer": "falcon-174<|im_end|>"
},
{
"tokens": 27743,
"prompt_ms": 30615.6,
"pp_tok_s": 906.2,
"found": true,
"answer": "willow-474<|im_end|>"
}
],
"chat": {
"turn1_tokens": 6584,
"turn1_ms": 7864.7,
"turn2_tokens": 6660,
"reused": 6632,
"turn2_ms": 366.9,
"answer": "lantern-417<|im_end|>",
"ok": true
},
"decode": [
{
"name": "story",
"tok_s": 45.5,
"reps": [
45.5
],
"hit_rate": 0.9703345151405784,
"text": "The wind didn\u2019t just blow; it screamed.\n\nElias pulled his wool coat tighter around his shoulders, the fabric stiff with salt and age. He was seventy years old, but he moved with the grace of a man who had spent decades on a narrow spiral staircase. His hands, rough as sandpaper, knew every bolt, every lever, and every creak in the iron bones of the tower.\n\nFor forty years, Elias had been the guardian of the Point. Not just of the light, but of the silence between the waves.\n\nIt was November. The sky was a bruised purple, heavy with rain that hadn\u2019t yet fallen. The sea was a churning cauldron of grey and white, throwing itself against the granite base with a violence that shook the floorboards beneath Elias\u2019s boots.\n\nHe climbed the stairs. One hundred and twelve steps. He counted them by feel, his left hand trailing along the cold iron railing. At the top, the air was"
},
{
"name": "code",
"tok_s": 58.0,
"reps": [
58.0
],
"hit_rate": 0.9700542735219787,
"text": "```python\nimport csv\nfrom typing import List, Dict, Any\n\n\ndef parse_csv_to_dicts(filepath: str) -> List[Dict[str, Any]]:\n \"\"\"\n Parse a CSV file into a list of dictionaries.\n \n Each dictionary represents a row, with keys from the header and values from the row.\n \n Args:\n filepath: Path to the CSV file.\n \n Returns:\n A list of dictionaries, one per row (excluding the header).\n \"\"\"\n result: List[Dict[str, Any]] = []\n with open(filepath, 'r', newline='', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n for row in reader:\n result.append(dict(row))\n return result\n\n\n# Tests\ndef test_parse_csv_to_dicts():\n import tempfile\n import os\n \n # Create a temporary CSV file\n with tempfile.NamedTemporaryFile"
}
],
"stage_timing": [
"strata serve: stage 0: 168 windows; per window: wait for the GPU 9.683 ms, pool + plan 0.509 ms, host staging 0.714 ms, commit 0.320 ms",
"strata serve: stage 1: 168 windows; per window: wait for the GPU 25.253 ms, pool + plan 0.881 ms, host staging 0.000 ms, commit 0.559 ms",
"strata serve: stage 0: 242 windows; per window: wait for the GPU 9.860 ms, pool + plan 0.550 ms, host staging 0.750 ms, commit 0.328 ms",
"strata serve: stage 1: 242 windows; per window: wait for the GPU 26.029 ms, pool + plan 0.980 ms, host staging 0.000 ms, commit 0.571 ms"
],
"engine_lines": [
"strata generate: layer split across 2 GPUs: CUDA0, then CUDA1 (split auto)",
"strata generate: layer split: CUDA1 PCIe probe 3.2 GB/s -> pcie_frac 0.00",
"strata generate: layer split: CUDA1 holds its weights, session and head; 16.18 GiB free",
"strata generate: layer split auto: K=20 - the caches hold 10761 of 12288 profiled pairs (fullest device 100%)",
"strata generate: expert cache auto: 10.46 GiB free, 700 MiB reserved -> 3345 slots",
"strata generate: expert cache 4603 slots, 8.29 GiB of VRAM; policy is",
"strata generate: layer split: CUDA1 runs layers 20-47, expert cache 6157 slots (12.19 GiB), 6157 of its 7168 profiled pairs; slot 0 verified",
"strata generate: layer split: CUDA0 runs layers 0-19",
"strata serve: the prompt path allocates its own buffers (too few cache slots to borrow)",
"strata serve: layer split: layers 0-19 (CUDA0), 20-47 (CUDA1), one hand-off per window",
"strata generate: layer split across 2 GPUs: CUDA0, then CUDA1 (split auto)",
"strata generate: layer split: CUDA1 PCIe probe 3.2 GB/s -> pcie_frac 0.00",
"strata generate: layer split: CUDA1 holds its weights, session and head; 16.18 GiB free",
"strata generate: layer split auto: K=20 - the caches hold 10761 of 12288 profiled pairs (fullest device 100%)",
"strata generate: expert cache auto: 10.46 GiB free, 700 MiB reserved -> 3345 slots",
"strata generate: expert cache 4603 slots, 8.29 GiB of VRAM; policy is",
"strata generate: layer split: CUDA1 runs layers 20-47, expert cache 6157 slots (12.19 GiB), 6157 of its 7168 profiled pairs; slot 0 verified",
"strata generate: layer split: CUDA0 runs layers 0-19",
"strata serve: the prompt path allocates its own buffers (too few cache slots to borrow)",
"strata serve: layer split: layers 0-19 (CUDA0), 20-47 (CUDA1), one hand-off per window",
"strata serve: decode expert cache hit rate: 96.6% (5559 hits / 5754 lookups)",
"strata serve: decode expert cache hit rate: 97.5% (4205 hits / 4315 lookups)",
"strata serve: decode expert cache hit rate: 97.8% (4217 hits / 4312 lookups)",
"strata serve: decode expert cache hit rate: 98.6% (4257 hits / 4318 lookups)"
]
}
@@ -0,0 +1,99 @@
{
"label": "x3-auto",
"extra": [
"--layer-split",
"auto"
],
"gpus": "0,2,1",
"needles": [
{
"tokens": 2726,
"prompt_ms": 3657.4,
"pp_tok_s": 745.3,
"found": true,
"answer": "meadow-254<|im_end|>"
},
{
"tokens": 8379,
"prompt_ms": 7594.1,
"pp_tok_s": 1103.4,
"found": true,
"answer": "juniper-766<|im_end|>"
},
{
"tokens": 16066,
"prompt_ms": 13996.4,
"pp_tok_s": 1147.9,
"found": true,
"answer": "falcon-174<|im_end|>"
},
{
"tokens": 27743,
"prompt_ms": 24255.2,
"pp_tok_s": 1143.8,
"found": true,
"answer": "willow-474<|im_end|>"
}
],
"chat": {
"turn1_tokens": 6584,
"turn1_ms": 6104.9,
"turn2_tokens": 6660,
"reused": 6632,
"turn2_ms": 393.4,
"answer": "lantern-417<|im_end|>",
"ok": true
},
"decode": [
{
"name": "story",
"tok_s": 51.9,
"reps": [
51.9
],
"hit_rate": 1.0,
"text": "The wind didn\u2019t just blow; it screamed. It tore through the iron gallery of the Blackwood Point Lighthouse, rattling the glass panes like a prisoner shaking its bars.\n\nElias Thorne was seventy years old, and he had been the keeper for forty of them. He knew every creak in the spiral staircase, every draft that slipped under the door, and every shift in the weather that the barometer couldn\u2019t quite predict. He knew that when the fog rolled in thick enough to swallow the horizon, the world ceased to exist. There was only the light, the stone, and him.\n\nIt was November, the month of storms. The sea was a churning cauldron of grey and white, throwing itself against the granite base with a violence that made the floorboards tremble. Elias stood by the lantern room, his hands wrapped around a mug of coffee that had gone cold hours ago. He wasn\u2019t drinking it. He was holding it for warmth."
},
{
"name": "code",
"tok_s": 69.7,
"reps": [
69.7
],
"hit_rate": 1.0,
"text": "```python\nimport csv\nfrom typing import List, Dict, Any\n\n\ndef parse_csv(file_path: str) -> List[Dict[str, Any]]:\n \"\"\"\n Parse a CSV file into a list of dictionaries.\n \n Each dictionary represents a row, with keys being the header values\n and values being the corresponding cell values.\n \n Args:\n file_path: Path to the CSV file.\n \n Returns:\n A list of dictionaries, one per row (excluding the header).\n \"\"\"\n result = []\n with open(file_path, 'r', newline='', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n for row in reader:\n result.append(dict(row))\n return result\n\n\n# Tests\ndef test_parse_csv_basic():\n \"\"\"Test basic CSV parsing with simple data.\"\"\"\n import tempfile\n import os\n \n # Create a temporary CSV"
}
],
"stage_timing": [
"strata serve: stage 2: 165 windows; per window: wait for the GPU 13.452 ms, pool + plan 0.162 ms, host staging 0.000 ms, commit 0.328 ms",
"strata serve: stage 0: 234 windows; per window: wait for the GPU 6.235 ms, pool + plan 0.145 ms, host staging 0.704 ms, commit 0.214 ms",
"strata serve: stage 1: 234 windows; per window: wait for the GPU 15.592 ms, pool + plan 0.246 ms, host staging 0.000 ms, commit 0.657 ms",
"strata serve: stage 2: 234 windows; per window: wait for the GPU 13.916 ms, pool + plan 0.177 ms, host staging 0.000 ms, commit 0.335 ms"
],
"engine_lines": [
"strata generate: layer split across 3 GPUs: CUDA0, then CUDA1,2 (split auto)",
"strata generate: layer split: CUDA1 PCIe probe 6.4 GB/s -> pcie_frac 0.14",
"strata generate: layer split: CUDA1 holds its weights, session; 18.62 GiB free",
"strata generate: layer split: CUDA2 PCIe probe 3.2 GB/s -> pcie_frac 0.00",
"strata generate: layer split: CUDA2 holds its weights, session and head; 16.18 GiB free",
"strata generate: layer split auto: K=12,33 - the caches hold 12288 of 12288 profiled pairs (fullest device 66%)",
"strata generate: expert cache auto: 10.46 GiB free, 700 MiB reserved -> 3072 slots",
"strata generate: expert cache 3072 slots, 5.41 GiB of VRAM; policy is",
"strata generate: layer split: CUDA1 runs layers 12-32, expert cache 5376 slots (10.22 GiB), 5376 of its 5376 profiled pairs; slot 0 verified",
"strata generate: layer split: CUDA2 runs layers 33-47, expert cache 3840 slots (7.79 GiB), 3840 of its 3840 profiled pairs; slot 0 verified",
"strata generate: layer split: CUDA0 runs layers 0-11",
"strata serve: the prompt path allocates its own buffers (too few cache slots to borrow)",
"strata serve: layer split: layers 0-11 (CUDA0), 12-32 (CUDA1), 33-47 (CUDA2), one hand-off per window",
"strata generate: layer split across 3 GPUs: CUDA0, then CUDA1,2 (split auto)",
"strata generate: layer split: CUDA1 PCIe probe 6.4 GB/s -> pcie_frac 0.14",
"strata generate: layer split: CUDA1 holds its weights, session; 18.62 GiB free",
"strata generate: layer split: CUDA2 PCIe probe 3.2 GB/s -> pcie_frac 0.00",
"strata generate: layer split: CUDA2 holds its weights, session and head; 16.18 GiB free",
"strata generate: layer split auto: K=12,33 - the caches hold 12288 of 12288 profiled pairs (fullest device 66%)",
"strata generate: expert cache auto: 10.46 GiB free, 700 MiB reserved -> 3072 slots",
"strata generate: expert cache 3072 slots, 5.41 GiB of VRAM; policy is",
"strata generate: layer split: CUDA1 runs layers 12-32, expert cache 5376 slots (10.22 GiB), 5376 of its 5376 profiled pairs; slot 0 verified",
"strata generate: layer split: CUDA2 runs layers 33-47, expert cache 3840 slots (7.79 GiB), 3840 of its 3840 profiled pairs; slot 0 verified",
"strata generate: layer split: CUDA0 runs layers 0-11"
]
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,26 @@
{
"mode": "project",
"dir": "per-layer",
"scale": 1.0,
"layer_range": [
4,
44
],
"refusal_harmful_thinkoff": 2.0,
"refusal_harmful_thinkon": 2.0,
"refusal_harmless_thinkoff": 4.0,
"refusal_harmless_thinkon": 6.0,
"broken": 0,
"kl_mean": 0.18598,
"kl_max": 18.26549,
"kl_p99": 3.143571,
"decode_tps": 51.19,
"vector_file": "Qwen3.8-Flash-Next-experimental-speed-projection.gguf",
"base_model_gguf": "unsloth/Qwen3.8-Flash-Next-GGUF UD-Q2_K_XL",
"base_model_sha256_shard1": "a4f3b21e77353999829f2f767e9ac21ce9c71d29a74f2cc9eda48c9bf23c8b86",
"worktree_commit": "37e026e3b",
"criterion_status": "PASS",
"dirty_direction": false,
"note": "projection ablation h -= s*(h.v)*v, v unit-normalized per layer. dir=per-layer",
"branch": "cvec-hooks (github.com/Cudila/llama.cpp)"
}
@@ -0,0 +1,21 @@
# experimental-speed-projection (EXPERIMENTAL, off by default)
`Qwen3.8-Flash-Next-experimental-speed-projection.gguf` is a 480 KB control vector for Qwen3.8-Flash-Next: one unit
direction of 2,560 values per layer (`direction.1` .. `direction.47`, GGUF `controlvector` format). The JSON beside it
is the configuration its authors selected and their scores.
**What it does.** Turned on, Strata removes each direction from the residual stream after layers 4-44
(`h -= (h . v) v` on every hyper-connection stream), the same operation as llama.cpp's `--cvec-mode project` in the
package's patches. The package's own documentation describes the vector as a **refusal-direction projection**: the
model declines far fewer requests (it reports 1 of 50 vs 50 of 50 on its test set), and removing refusals removes a
safety behaviour - you are responsible for what the model writes with it on. It also shifts the model's predictions
slightly on ordinary text (measured here: `bench/results/2026-09-27-esp/`). It is not an optimization in the engine:
on the same text it costs 0.2-0.4% per token; a chat's speed with it on depends on the text the model writes.
**Turning it on.** `START-HERE.bat --setup` asks (default: off), or pass `--experimental-speed-projection on`. With it
loaded, the web app's Sampling drawer and the API field `"experimental_speed_projection": false` switch it off per
request. See `docs/DETAILS.md`, "Experimental speed projection".
**Origin and license.** A local package of the vector (the published original uses filenames containing "refusal";
the bytes are the same), made from Qwen3.8-Flash-Next activations, so the Qwen Community License 1.0 of the model
applies to it. Made for the original Qwen3.8-Flash-Next (not Swift 1.5).
Binary file not shown.
Binary file not shown.
+253 -28
View File
@@ -4,42 +4,81 @@ The technical side of Strata: every measured number, the API, images, all settin
New here? Start with the [README](../README.md) - it has everything you need to install and use it.
> **On this page:** [Speed](#speed-measured) · [Other GPUs](#other-gpus-estimated) · [Which model?](#which-model) ·
> [Requirements](#before-you-start) · [Windows](#windows) · [Linux](#linux) · [API](#using-it) · [Images](#images-vision) ·
> [Requirements](#before-you-start) · [Windows](#windows) · [Linux](#linux) · [API](#using-it) ·
> [MCP tools](#tools-from-mcp-servers) · [Images](#images-vision) ·
> [Troubleshooting](#troubleshooting) · [How it works](#how-it-works)
---
## Speed (measured)
RTX 5070 **12 GB**, Ryzen 5 7600 (6 cores), 64 GB DDR5-5200, Windows. One code-agent prompt per length, 256 generated
tokens, MTP speculative decoding on. "262K" is the model's full context window (a 259,943-token prompt).
RTX 5070 **12 GB**, Ryzen 5 7600 (6 cores), 64 GB DDR5-5200, Windows, engine 0.1.14 with the settings setup writes
(`--prefill auto`, 8-bit KV above 4K, KV streaming from 64K). One code-agent prompt per length, 256 generated tokens,
MTP speculative decoding on. "262K" is the model's full context window (a 259,943-token prompt). The IQ2_XS row was
measured with Swift 1.5's IQ2_XS, which runs at the original's speed.
### Prompt processing (tokens/s)
| Model | 1K | 4K | 32K | 64K | 128K | 262K |
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
| **Q2_0** | 389 | 539 | 571 | 561 | 543 | 496 |
| **IQ2_XS** | 332 | 463 | 495 | 486 | 472 | 437 |
| **IQ3_XXS** | 285 | 410 | 435 | 427 | 414 | - |
| **Q2_0** | 494 | 1,007 | 1,308 | 1,294 | 1,208 | 967 |
| **IQ2_XS** | 461 | 811 | 1,238 | 1,136 | 1,071 | 886 |
| **IQ3_XXS** | 415 | 770 | 1,108 | 1,065 | 1,015 | - |
| **IQ3_S** | 396 | 737 | 1,070 | 1,070 | 931 | - |
| **Coder** | 599 | 1,152 | 1,298 | 1,350 | 1,266 | 1,034 |
### Output (tokens/s)
| Model | 1K | 4K | 32K | 64K | 128K | 262K |
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
| **Q2_0** | 88.7 | 94.6 | 87.5 | 76.4 | 65.1 | 56.3 |
| **IQ2_XS** | 82.0 | 78.0 | 65.3 | 63.7 | 52.0 | 48.0 |
| **IQ3_XXS** | 64.6 | 65.6 | 57.3 | 54.4 | 44.8 | - |
| **Q2_0** | 84.3 | 90.3 | 73.6 | 69.0 | 67.2 | 60.3 |
| **IQ2_XS** | 74.4 | 73.8 | 71.5 | 64.3 | 59.8 | 52.8 |
| **IQ3_XXS** | 60.3 | 62.1 | 51.4 | 50.0 | 45.8 | - |
| **IQ3_S** | 51.5 | 51.6 | 48.2 | 48.8 | 40.5 | - |
| **Coder** | 53.3 | 50.6 | 53.3 | 50.8 | 44.0 | 42.8 |
IQ3_XXS at 262K is not measured: with its 43 GB of experts, a 260K-token context brings a 64 GB PC to its memory
limit. Use up to 128K with IQ3_XXS on 64 GB.
Output speed depends on the text as well: speculative decoding runs faster when more of the drafted tokens are
accepted, so a different answer to the same prompt moves it by several percent. Run back to back on the 4K prompt,
0.1.14 writes 88.5 tokens/s and 0.1.12 85.7. The numbers before 0.1.13 (prompts about half as fast):
[`bench/results/2026-09-24-final`](../bench/results/2026-09-24-final/matrix.md); these:
[`bench/results/2026-09-28-speed-0114`](../bench/results/2026-09-28-speed-0114/README.md).
Time to first token is prompt length / prompt speed: about 7 s at 4K, 55 s at 32K, 4 minutes at 128K and 9 minutes at
262K. The raw numbers: [`bench/results/`](../bench/results/). The [paper](paper/Strata-Paper.pdf) explains every number.
IQ3_XXS and IQ3_S at 262K are not measured: with their 43 / 50 GB of experts, a 260K-token context brings a 64 GB PC
to its memory limit. Use up to 128K with them on 64 GB (setup caps it). IQ3_S (engine 0.1.4 or newer) is only published
for the original model, not for Swift 1.5.
**KV streaming (engine 0.1.5):** at 64K and more, setup keeps the context's KV cache in RAM and only the part the
attention reads in VRAM (`--kv-resident 32768`), so more experts fit on the GPU. Q2_0 at 262K: 50.9 -> 62.6 tokens/s
(1,589 -> 3,872 experts in VRAM); at 128K about +6%. The attention reads exactly the same values (only where the KV lives changes); it
costs ~13.7 KB of RAM per context token (1.7 GB at 128K). Existing installs: run `START-HERE.bat --setup` once to turn
it on.
**4-bit KV cache (engine 0.1.8, optional):** `START-HERE.bat --setup` asks above 8K context (or pass `--kv q4_0`). It
halves the KV cache's memory with a Hadamard rotation before 4-bit rounding (PR #21), about 4% faster at 128K, but it
is measurably less precise on long documents (perplexity +8-12%; needle tests still pass). 8-bit stays the default.
Details: [`bench/results/2026-09-27-kv-q4`](../bench/results/2026-09-27-kv-q4/README.md).
Time to first token is prompt length / prompt speed: with Q2_0 about 4 s at 4K, 25 s at 32K, under 2 minutes at 128K
and 4.5 minutes at 262K (engine 0.1.13 made long prompts about twice as fast, below).
**Faster prompts (engine 0.1.13):** the prompt is read in chunks of up to 8,192 tokens instead of 2,048 (`--prefill
auto`: the largest chunk whose buffers fit in the expert-cache slots it borrows, and a request borrows only what its
prompt needs); the experts are multiplied by llama.cpp's quantized MMQ kernels instead of being expanded to FP16
first; the next layer's experts stream over PCIe while the current layer's attention runs; the PLE block runs for the
whole chunk at once; unpinned experts are copied by helper threads. Measured on the RTX 5070 12 GB, 64 GB RAM,
32K-token prompt: Q2_0 572 -> 1,290 tokens/s, IQ3_S 383 -> 1,208. Through the server (Q2_0, 128K context): 999 tokens
353 -> 438 tokens/s, 6,927 tokens 529 -> 1,077, 28,584 tokens 584 -> 1,249. Output speed is unchanged. Needles 5/5
(1K-262K). Details and the quality check:
[`bench/results/2026-09-28-prefill-speed`](../bench/results/2026-09-28-prefill-speed/README.md). Existing installs
switch to `--prefill auto` the next time START-HERE / setup.sh starts them. The raw numbers:
[`bench/results/`](../bench/results/). The [paper](paper/Strata-Paper.pdf) explains every number.
## Other GPUs (estimated)
Not measured - estimated from the runs above (same CPU and 64 GB RAM): the GPU part scaled by memory bandwidth, the CPU
part by how many more experts the card's VRAM holds. Treat as **±20%**. Numbers are *prompt / output* tokens/s.
The prompt figures predate engine 0.1.13, which about doubled prompt speed on the measured card; how much of that a
card gains depends on its PCIe link (the experts stream over it), so they are still the older estimates.
| GPU | Model | 1K | 4K | 32K | 64K | 128K | 262K |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
@@ -51,7 +90,9 @@ part by how many more experts the card's VRAM holds. Treat as **±20%**. Numbers
| | IQ3_XXS | ~260 / ~106 | ~374 / ~103 | ~396 / ~89 | ~390 / ~85 | ~378 / ~71 | - |
More VRAM matters more than a faster GPU: every extra GB holds ~700 more experts, and every expert on the GPU is one the
CPU does not have to compute. A 3090's 24 GB takes most of the CPU work away.
CPU does not have to compute. A 3090's 24 GB takes most of the CPU work away. (Since 0.1.14 the expert profile ranks
all 24,576 experts; before, the cache stopped at 8,000, about 10-14 GB. `tools/make_profile.py` builds a profile from
your own prompts: run the engine once with `--dump-routing trace.bin`, see the tool's help.)
## Which model?
@@ -64,7 +105,24 @@ of [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next).
| **IQ2_XS** | 68 GB | ~36 GB experts + ~6 GB | close to Q2_0 | a bit better |
| **IQ3_XXS** | 76 GB | ~43 GB experts + ~6 GB | slower (more CPU work) | best |
With 64 GB of RAM all three fit (close the browser for IQ3_XXS, and keep its context at 128K or less). With 48 GB only Q2_0 / IQ2_XS may fit. 32 GB is not enough.
With 64 GB of RAM all three fit (close the browser for IQ3_XXS, and keep its context at 128K or less). With 48 GB only Q2_0 / IQ2_XS may fit. With 32 GB: the Coder (below).
### Or: the Coder (half the experts, for code)
**[Qwen3.8-Flash-Next GSQ-RCO Coder](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-Coder-GGUF)** is
ISTA-DASLab's expert-pruned release: 256 of each layer's 512 experts are kept (still 10 active per token), chosen with
RCO on code, agentic and vision calibration data; its authors report 91.3% of the full model's SWE-bench Verified and
98.7% of LiveCodeBench v6. One size, named IQ1_M for its 1.89 bits per *original* parameter; the kept experts are
stored like IQ3_S (IQ2_S-IQ4_XS gate/up, IQ4_NL/Q2_0 down). Shard 1 is 29.6 GB (experts: 23 GB of RAM), so it runs
on **32 GB of RAM**, and at 262K on 64 GB. Its shard 2 and its vision encoder are the original's files: with the
original installed, setup downloads only shard 1. Strata ships its expert profile (`data/expert-profile-coder.bin`,
the shipped ranking mapped onto the kept experts through the release's `rco-allocation.txt`: 72% of the expert
reads hit the GPU on a 12 GB card). Images work; the experimental speed projection loads and runs on it (it was made
for the full model).
```
START-HERE.bat --setup --family coder
```
### Or: Swift 1.5 (a fine-tune that thinks shorter)
@@ -96,7 +154,11 @@ You need **only an NVIDIA driver** (version 580 or newer; update it with the NVI
| Disk | ~70-80 GB free for the model, ~6 GB for the MTP layer (+1 GB with images). **Q2_0 on an AVX-512 CPU** also writes a one-time ~40 GB copy of its experts for the fast CPU kernel. An NVMe SSD is strongly recommended. |
| OS | Windows 10/11, or Linux (Ubuntu 22.04/24.04 get everything installed automatically). |
What the first start installs, all inside this folder (`.venv/`, `engine/`, `third_party/`, `models/`, `packs/`, `mtp/`):
What the first start installs: in this folder `.venv/`, `engine/` and `third_party/`; the model files (`models/`,
`packs/`, `mtp/`, 70-120 GB) in **`Strata-data` next to this folder**, so a new copy of Strata (an update unzipped
elsewhere) finds them and sets itself up the same way. The place is remembered per user (`%APPDATA%\Strata\settings.json`,
`~/.config/strata/settings.json`); `--data-dir` chooses another. Installs from before 0.1.16 are moved there by the next
start (a rename on the same drive; files on another drive are used where they are).
Python 3.12 if you have none (for your user account, no admin), a private Python environment, NVIDIA's CUDA libraries
(from pip, ~0.4 GB), the ready-made Strata engine for RTX 30/40/50, the model and the MTP draft layer. If no
ready-made engine fits your PC, it offers to install the build tools (Visual Studio Build Tools + CUDA Toolkit on
@@ -116,21 +178,44 @@ Windows, `build-essential` + CUDA on Ubuntu) and compiles the engine for your GP
4. **Images?** yes / no (see [Images](#images-vision)).
Then it downloads and prepares everything (the model is 66-76 GB, so the first start takes a while; an interrupted
download continues where it stopped) and **starts the model**: your browser opens `http://127.0.0.1:8080`, a small
page that shows it is running and lets you chat. The API is at `http://127.0.0.1:8080/v1` for your apps.
download continues where it stopped) and **starts the model**: your browser opens `http://127.0.0.1:8080`, the Strata
app. It has three tabs:
- **Chat:** streaming answers, the model's thinking (folded away once it answers), code with a copy button, pictures when
images are on, and sampling and thinking-level settings. Chats stay in your browser.
- **Monitor:** what the model is doing (reading the prompt, with progress, or writing, at how many tokens/s); GPU load,
VRAM, temperature, power and PCIe traffic; CPU, RAM and disk; the context in use; the last requests.
- **About:** the model and engine settings, and the addresses to connect other apps.
`http://127.0.0.1:8080/?q=your question` opens it with a new chat already asking. The API is at
`http://127.0.0.1:8080/v1` for your apps.
**Every time after that**, `START-HERE.bat` just starts the model (30-90 s to load 34-43 GB into RAM). Nothing is
downloaded again. Closing the window stops the model.
```
START-HERE.bat --setup install another model, or change context / images
SETUP.bat the same (double-click it)
START-HERE.bat --model IQ2_XS --context 32768 --vision yes --yes no questions
START-HERE.bat --gguf-dir D:\models\IQ2_XS use GGUF files you already have
START-HERE.bat --data-dir E:\Strata-data keep the model files somewhere else
START-HERE.bat --port 8081 another port
START-HERE.bat --gpu 1 another GPU (numbered as nvidia-smi; setup picks the one with the most VRAM)
START-HERE.bat --calibrate tune the engine for this PC (about 5-10 minutes), then start
```
With more than one model installed, it asks which one to start. `run-<model>.bat` starts a model directly.
**Tuning for your PC (`--calibrate`, engine 0.1.19).** Three engine settings depend on the PC more than on the model:
- the share of the experts missing from VRAM that are copied to the GPU instead of computed by the CPU
(`--pcie-frac`: a fast PCIe link and a slower CPU want more, a laptop's narrower link less);
- how sure the draft layer must be to add another guess to a check (`--spec-min-p`);
- how many CPU threads compute experts (`--pool-workers`: on CPUs with efficiency cores, fewer can be faster).
The defaults were measured on a Ryzen 5 7600 with an RTX 5070. Setup offers to measure them on your PC after an
install; `START-HERE.bat --calibrate` (Linux: `./setup.sh --calibrate`) does it any time. It measures the output
speed with each setting and keeps one only when it is more than 3% faster. The result is remembered per PC and model
(in the settings file next to the data folder's record), so updates keep it.
### Chat in the terminal (optional)
```
@@ -150,6 +235,15 @@ for the build tools), and the same start: `http://127.0.0.1:8080`. Later runs of
start the model directly. Options as on Windows (`./setup.sh --setup`, `--model Q2_0 --yes`, `--gguf-dir /data/Q2_0`).
Terminal chat: `.venv/bin/python chat.py`.
- **Updating:** `git pull`, then `./setup.sh`: it compiles the engine again when its source changed (a minute or
two for the changed files). If that compile fails, it says so and starts the engine you had.
- **Other distributions** (Arch, Fedora, ...): install the C++ compiler and the CUDA Toolkit 13 with your package
manager first (Arch: `sudo pacman -S base-devel cuda`); setup finds `nvcc` on PATH, in `/usr/local/cuda*` and in
`/opt/cuda*`, and does the rest.
- **WSL** works (Ubuntu 24.04 tested), with one limit: the NVIDIA driver pins only about 1 GB of RAM there, so KV
streaming (`--kv-resident`) is off and the KV cache stays in VRAM, and the experts are copied to the GPU from
unpinned RAM (slower prompts than native Linux).
---
## Using it
@@ -162,6 +256,8 @@ The server listens on `http://127.0.0.1:8080` (change with `--port` in setup, or
| Anthropic Messages (stream and non-stream, tools) | `POST /v1/messages` |
| Model list / health | `GET /v1/models`, `GET /health` |
| What the model is doing right now | `GET /status` |
| Everything the Monitor tab shows (engine, live state, last requests, hardware) | `GET /metrics` |
| The MCP servers, their state and tools ([below](#tools-from-mcp-servers)) | `GET /mcp` |
```bash
curl http://127.0.0.1:8080/v1/chat/completions -H "Content-Type: application/json" -d '{
@@ -193,16 +289,91 @@ print(r.choices[0].message.content)
15 s, and `GET /status` says what it is doing (`reading the prompt`, `answering`, tokens so far). Closing the
connection or pressing stop in your app really stops the model, so the next request starts at once.
- **Chat apps.** Any app with an "OpenAI-compatible" provider works: base URL `http://127.0.0.1:8080/v1`, any API key.
- **Context.** Chosen in setup (8K-262K). Requests longer than that are refused, never silently cut.
- **From other devices / the internet.** The server listens on your PC only (`127.0.0.1`). To reach it from elsewhere,
put a tunnel in front of it, for example [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/):
- **Claude Code** (Strata 0.1.17 or newer): set `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` and
`ANTHROPIC_MODEL` to a Claude model name it knows (it refuses names it doesn't; Strata ignores the name), plus any
`ANTHROPIC_AUTH_TOKEN` (or your `api_key`, if you set one).
- **Context.** Chosen in setup (8K-262K). Requests longer than that are refused, never silently cut. A request whose
`max_tokens` would run past the context is refused too (400); agents that always ask for their full output cap
can instead get it shortened to the room left: add `"fit_max_tokens": true` to `strata-<model>.json` (or pass
`--fit-max-tokens` to `serve/server.py`). A prompt that leaves no room at all is still refused.
- **From other devices on your network.** The server listens on your PC only (`127.0.0.1`) unless you say otherwise:
run setup with `START-HERE.bat --setup --host 0.0.0.0 --api-key some-long-secret` (or add `"host": "0.0.0.0"` and
`"api_key": "..."` to `strata-<model>.json`). The server window then prints this PC's addresses
(`from other devices: http://192.168.x.x:8080/`); open that on the other device, or use `.../v1` as an API base URL.
On Windows the firewall blocks it until you allow it: accept its prompt for Python (private networks), or run
`New-NetFirewallRule -DisplayName "Strata 8080" -Direction Inbound -Protocol TCP -LocalPort 8080 -Action Allow -Profile Private`
in an admin PowerShell, and make sure the network is set to Private.
- **From the internet.** Put a tunnel in front of it, for example [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/):
`cloudflared tunnel --url http://127.0.0.1:8080`. **Set a key first**, or anyone with the link can use your PC:
add `"api_key": "some-long-secret"` to `strata-<model>.json` (or set the `STRATA_API_KEY` environment variable);
clients then send it as their API key.
**Current limits (v1):** one request at a time; greedy decoding (temperature is ignored); every request processes its
whole prompt again (no conversation cache yet, so long chats have a long time-to-first-token); images only when set up
with them (below); no video.
**Conversation cache.** A request that continues a chat reads only the part after what the engine already holds: the
live session, or one of the checkpoints it keeps in RAM (up to 6, ~118 MB each, taken at the start of each new
assistant turn and every 16K prompt tokens). A checkpoint is used only when the prompt starts with exactly its tokens
and pictures. The oldest checkpoint - in practice the end of the system prompt, which every chat of the same client
shares - is kept for good while the rest rotates by least recent use, so a NEW chat that shares that prefix starts
reading after it instead of from token 0. A prompt read from the start is also checkpointed at the end of its system
prompt when that is 2,048 tokens or more (engine 0.1.20; PR #62 + #65), so that root exists for agent clients with long
system prompts and tool lists. Engine options: `--prompt-cache N` (0 = off), `--prompt-cache-every N`,
`--prompt-cache-root N` (0 = no system-prompt checkpoint), `--turn-token ID`.
**Current limits (v1):** one request at a time, and one conversation's history in the KV cache at a time (switching
between two chats re-reads the part where they diverge; the shared prefix, such as the system prompt, is reused); images
only when set up with them (below); no video. **Temperature / top_p / top_k / min_p /
seed** are honored per request (OpenAI and Anthropic fields); with the default adaptive expert tier a sampled result
is not reproducible run to run - for seed-reproducible output add `--adapt-every 100000` (static residency) to the
engine arguments. The run config's optional `sampling` block sets the defaults for requests that leave the fields out
(`"sampling": {"temperature": 1.0, "top_p": 0.95, "top_k": 20}`); a request's own fields always win, and with no
block at all a request without sampling keys decodes greedy. The penalties (`presence_penalty`, `frequency_penalty`,
`repetition_penalty`, with `penalty_last_n` capping how many recent tokens they count over, default 64 when any
penalty is set) ride the same path; they count the tokens the request has consumed, so a repetition penalty
suppresses what the model itself just said, not the prompt alone. Since engine 0.1.19 they apply to every token
the speculative decoding checks at once, exactly as if it decoded one token at a time (before, only the first of
each batch got them). That makes requests with penalties 1-11% slower than in 0.1.18: the draft layer guesses
without penalties, so more of its guesses are now rejected. Requests without penalties are unchanged. `top_k` keeps at most 64 candidates: `0` ("off") or anything above 64 uses all 64.
---
## Tools from MCP servers
The chat page can give the model tools from [MCP](https://modelcontextprotocol.io) servers, as LM Studio and Claude
Desktop do: reading your files, fetching web pages, searching, anything an MCP server offers. List the servers in
`strata-<model>.json` under `"mcp_servers"` - the same shape as Claude Desktop's `mcpServers` block, which you can
also paste as it is (key `"mcpServers"`):
```json
"mcp_servers": {
"files": {"command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "C:\\Users\\me\\Documents\\notes"]},
"search": {"url": "http://127.0.0.1:3000/mcp", "headers": {"Authorization": "Bearer ..."}}
},
"mcp": {"timeout_s": 60, "max_result_chars": 20000, "max_rounds": 8}
```
Or keep them in their own file and start the server with `--mcp-config path\to\claude_desktop_config.json` (a file
with an `mcpServers` block; add it to the `serve/server.py` line of your run script). Restart Strata after a change.
- **A program** (`command`, `args`, optional `env` and `cwd`) is started by Strata and spoken to over its
stdin/stdout; `npx`, `uvx`, `python` and friends are found on `PATH` as usual (Node.js is needed for `npx`
servers). **An address** (`url`, optional `headers`) uses MCP's Streamable HTTP transport (the older SSE-only
transport is not supported). `"disabled": true` leaves an entry out.
- The servers start with Strata, in the background; the server window says what each one offers
(`MCP server 'files': 14 tools (...)`), or why it did not start - its tools are then left out and the chat works
without them. The Monitor tab lists them, and the Sampling drawer has **Use tools from MCP servers** (on by
default). A server that stops later is started again at its next call.
- In the chat each call shows as a small block (tool, arguments, result); the model reads the result and goes on,
up to `max_rounds` calls in a row per answer. A tool that fails or takes longer than `timeout_s` (default 60 s)
gives the model an `error: ...` result instead of ending the chat. Results longer than `max_result_chars`
(default 20,000 characters) are cut, with a note, before the model reads them. Stop stops a running tool too.
- Only the chat page uses them. API clients (omp, Claude Code, OpenAI and Anthropic SDKs) see the API exactly as
before and keep their own tools; a request to `/v1/chat/completions` opts in with `"strata_mcp": true` (it then
gets `strata_mcp` tool events in the stream).
**Security.** MCP tools run on your PC with your user's rights, and **the model decides when to call them** - also
because of what it reads (a web page or a file can contain instructions). Give a filesystem server only the folders
it needs, prefer read-only tools, and don't add servers you don't trust. The tools can only be used from the chat
page itself (a request with another site's Origin or without a JSON content type is refused); if Strata is reachable
from other devices, set an API key.
---
@@ -280,6 +451,45 @@ test images.
---
## Experimental speed projection (EXPERIMENTAL, off by default)
**This is an experiment, not a finished feature.** It ships with Strata but stays off unless you turn it on.
A 480 KB control vector for Qwen3.8-Flash-Next (`data/experimental-speed-projection/`, see its README). After each
of layers 4-44 the engine removes one direction from every hyper-connection stream of the residual: `h -= (h . v) v`,
one unit vector `v` per layer, exactly as llama.cpp does with the package's `--cvec-mode project` patches.
**What it changes.** The vector's own package describes it as a **refusal-direction projection**: with it the model
declines far fewer requests (it reports 1 of 50 vs 50 of 50 on its test set), and removing refusals removes a safety
behaviour - you are responsible for what the model writes with it on. It also shifts ordinary answers a little
(measured below). It is not an optimization in the engine: on the same text it costs 0.2-0.4% per token. What a
chat's tokens/s does with it on depends on the text the model writes (length, repetition, how well the drafts land),
so measure it on your own prompts; the Monitor marks every request ESP or stock.
**Turning it on (at setup).** `START-HERE.bat --setup` asks "Turn on the experimental speed projection?" (default:
no), or pass `--experimental-speed-projection on` (`off`, or a path to another vector GGUF). Only for the original
Qwen3.8-Flash-Next, not Swift 1.5. It writes these engine flags (llama.cpp's) into `strata-<model>.json`:
```
--control-vector-scaled <Strata>\data\experimental-speed-projection\Qwen3.8-Flash-Next-experimental-speed-projection.gguf:1.0
--control-vector-layer-range 4 44 --cvec-mode project --cvec-dir per-layer
```
The engine log then says `control vector mode = project, dir = per-layer, layers 4..44 (41 steered)`, and the web
app's About tab lists it. (`--cvec-mode add` is llama.cpp's stock additive mode, for additive vectors.)
**Per request.** A loaded vector is on for every request unless it says otherwise: the web app's Sampling drawer has
a switch, and the API takes `"experimental_speed_projection": false` in the request body (OpenAI and Anthropic; a
config default goes in `"sampling": {"experimental_speed_projection": false}`). Switching drops the conversation
cache once, since the model state was computed the other way. Switched off, the output is token-for-token the stock
model's.
**Measured here** (Q2_0, fixed experts, 2,557 teacher-forced tokens of code, a document and a chat): the top-1 token
changes at 10% of positions, mean KL from the stock model 0.063 nats (max 4.1), perplexity +15% on code, +2.3% on
the document, +0.4% on the chat. Details: `bench/results/2026-09-27-esp/`.
---
## Troubleshooting
| Symptom | What to do |
@@ -288,14 +498,19 @@ test images.
| Python or the build tools could not be installed | Install what it names (links are printed), then run it again. Everything already done is kept. |
| `port 8080 is already in use` | Strata is already running (look for its window), or another program uses the port: `START-HERE.bat --port 8081`. |
| `cudaHostRegister ... out of memory` in the log | Normal on Windows: the engine pins the experts in per-layer slices instead. Only a problem if the load then fails. |
| The first start takes minutes | It is reading 34-43 GB into RAM; the second start is faster while the files are in the OS cache. |
| `ExpertCache: cudaMalloc(...) failed: out of memory` although VRAM is free | Windows' page file is off or tiny: every allocation on the graphics card is also charged to Windows' commit (RAM + page file). Set the page file to "System managed" (System > About > Advanced system settings > Performance > Advanced > Virtual memory) and restart. Since 0.1.19 the engine retries with a smaller cache instead of stopping, and setup warns about a page file under 4 GB (issue #60). |
| The first start takes minutes | It is reading 34-55 GB into RAM; the second start is faster while the files are in the OS cache. |
| The PC freezes for a few minutes at the start | Normal, most of all the first time (the server window says when it happens): the engine loads the experts into RAM, pins part of it for the GPU and sizes the expert cache. Wait; don't close the window. Still frozen after 10 minutes: restart the PC, close other programs, try again, or pick a smaller size. |
| `the engine stopped unexpectedly (exit code ...)` | The engine process ended mid-answer - usually out of RAM (Linux ends the biggest program: `sudo dmesg \| grep -i -E 'killed process\|out of memory'`). The next request starts it again by itself. If it repeats: close other programs or pick a smaller size. The server also warns at start when the model's experts leave less than ~6 GB of RAM for everything else. |
| Slow output, disk light busy | Not enough free RAM: close other programs, or choose Q2_0 / IQ2_XS. |
| `prompt ... exceeds the context` | The request is longer than the context you chose: run setup again with a bigger `--context`. |
| Slower than the tables | The monitor plugged into the GPU and other GPU programs take VRAM from the expert cache; RAM running below its rated speed (enable EXPO/XMP in the BIOS) slows the CPU half. |
| `this server was started without the vision encoder` | The model was set up for text only: run setup again with `--vision gpu`. |
| A picture is refused or `cannot read the image` | The file is not a picture Pillow can open (JPEG, PNG, WebP, GIF, BMP, TIFF, AVIF work). |
| Pictures are slow (10-30 s) | The encoder runs on the CPU: run setup again with `--vision gpu` (needs ~1.4 GB of VRAM). |
| A request never finishes (older engines, mostly with pictures) | Run `START-HERE.bat` once to get engine 0.1.2 or newer. It keeps a real margin of VRAM free: the log says `... MiB of VRAM free with everything loaded` and warns when it is close to 0. |
| A request never finishes: "reading the prompt", GPU "100%" at low power | The GPU ran out of VRAM (engines before 0.1.9 could end with ~30 MiB free at large contexts). Run `START-HERE.bat` once to get engine 0.1.9 or newer; the log then says `... MiB of VRAM free with everything loaded` (a few hundred) and names the `--vram-reserve-mib` to add if it is low. |
| Generation stops mid-answer, GPU "100%", one CPU core busy | Fixed in engine 0.1.12 (issue #29, a race in the CPU expert pool on big-VRAM cards). Since then a request that stops moving ends with an error instead of hanging (after 2 minutes; 1 minute from 0.1.13): the log says `no progress for ... s ... (issue #29)` with where it stopped, and the next request starts the engine again. If you see that line, please open an issue with it. Engine 0.1.13 adds a stall report under it (what every expert-pool thread and the GPU handshake were doing, memory and page faults) and, on Windows, a `strata-stall-<pid>.dmp` file with every thread's stack: attach both. (`STRATA_WATCHDOG_S` sets the time in seconds; 0 turns it off.) Engine 0.1.14 fixes the stall those reports found (issue #31: with the IQ packs the host could wait forever inside the NVIDIA driver while copying experts in a verify window; the experts are now copied by a GPU kernel, `--pcie-mode dma` restores the old way). |
| `out of memory: cudaFuncSetAttribute` in the log (IQ3_XXS, long prompt) | Fixed in engine 0.1.15: CUDA loaded a kernel's code when it was first needed, and mid-prompt there was no VRAM left for it. Run `START-HERE.bat` (Windows) or `./setup.sh` (Linux) once to update. |
| Anything else | The engine log is `strata-<model>.log` in this folder. |
---
@@ -305,13 +520,15 @@ test images.
<p align="center"><img src="paper/tiers.svg" width="760" alt="memory tiers"></p>
- **GPU (VRAM):** attention and DeltaNet mixers, the gated-residual weights, routers, shared experts, output head, the MTP
draft layer, the KV cache, and an **expert cache** that fills the rest of VRAM with the most-used experts (it adapts to
draft layer, the KV cache (from 64K: only its most-read part, the rest streams from RAM), and an **expert cache** that fills the rest of VRAM with the most-used experts (it adapts to
the conversation while you chat).
- **RAM:** all 24,576 experts, pinned. The CPU computes the experts that are not on the GPU **in place**, at the same time
as the GPU works on the cached ones (AVX-512 / AVX2 kernels, ggml's for the i-quants).
- **SSD:** the 28.8 GB n-gram table, read a few rows per token through the OS cache.
- **Speculation:** the model's own MTP layer drafts up to 3 tokens; one pass over all 48 layers checks them. 2.4-3.2
tokens per pass on average.
tokens per pass on average. When the reply repeats the context (code edits, quoted text), **prompt lookup** (engine
0.1.7) drafts up to 5 tokens from the earlier copy, but only where its measured acceptance and cost say it pays:
code edits 6-11% faster, other text unchanged. The drafts are checked like the MTP's, so the output is the same.
- **Prompts** are processed in 2,048-token chunks with the experts streamed to the GPU over PCIe.
The full story, with measurements, bottlenecks and what comes next: **[docs/paper/Strata-Paper.pdf](paper/Strata-Paper.pdf)**.
@@ -320,8 +537,12 @@ The full story, with measurements, bottlenecks and what comes next: **[docs/pape
## Credits and licenses
Strata itself: [MIT](../LICENSE). The model files are not part of it; their licenses apply to them (below).
- Model: [Qwen/Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) by the Qwen team; quantizations:
[ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF).
The Coder: [ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-Coder-GGUF](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-Coder-GGUF)
(Apache-2.0 per its card); its support in Strata came from @pjgmobile's PR #54.
Swift 1.5: [ukisai/Swift-1.5-Qwen3.8-Flash-Next-GSQ-RCO-GGUF](https://huggingface.co/ukisai/Swift-1.5-Qwen3.8-Flash-Next-GSQ-RCO-GGUF)
by UkisAI. Their licenses apply to the weights.
- [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp) (MIT): the i-quant formats, the GPU dot products and
@@ -330,3 +551,7 @@ The full story, with measurements, bottlenecks and what comes next: **[docs/pape
`third_party/ggml/LICENSE`.
- Ideas from [Splash](https://github.com/incoai/splash), [ninfer](https://github.com/Neroued/ninfer) and
[HyperQwen](https://github.com/syv-ai/HyperQwen); references in the paper.
- The web app's font: [Outfit](https://github.com/Outfitio/Outfit-Fonts) (SIL Open Font License 1.1, see
`serve/web/fonts/OFL.txt`). Its Monitor tab started from @code-martin's dashboard idea (PR #22).
- The experimental speed projection's vector (`data/experimental-speed-projection/`): Qwen Community License 1.0,
made from the model's activations (see its README).
+85
View File
@@ -0,0 +1,85 @@
# Strata on two or three GPUs (layer split)
One model can run across several NVIDIA cards in one PC. The layers are split into contiguous ranges, one per GPU:
the first card runs layers 0 to K-1, the next card runs K onward, and so on; the last card also runs the output head
and the draft (MTP) layer. Each card keeps an expert cache for **its own layers only**, so two cards hold about twice
the experts one card holds - for the Coder model on a 16 GB + 24 GB pair, nearly all of them, which is where the
speed comes from (decode then barely touches the CPU pool).
This is pipeline (layer) parallelism, not tensor parallelism: a token crosses from one card to the next once per
verify window (a few hundred KB through pinned RAM), not twice per layer. No NVLink or peer-to-peer access is
needed; cards on x4 or x1 slots work, and the PCIe share of each card is probed on its own link.
## Using it
At setup time (Windows: `START-HERE.bat`, Linux: `./setup.sh`):
```
--gpus 0,2 the cards, as nvidia-smi numbers them; the first is the main one
--layer-split auto (default) or the first layer of each later card, e.g. 18 or 16,32
```
or in an existing config (`strata-*.json`), then restart:
```json
"gpu": [0, 2],
"layer_split": "auto"
```
The engine flags behind it: `--layer-split K1[,K2..]|auto` and `--split-device D1[,D2..]` (the later stages'
devices; default the next visible ones). `--layer-split K --split-device 0` runs both stages on one card sharing
everything - the bit-exact check of the hand-off, not a speed mode.
**auto** tries every placement (all of them for two or three cards; proportional to the free VRAM beyond that) and
keeps the one whose caches would hold the most of the expert profile, hottest pairs weighted most; ties go to the
placement that leaves the fullest card the most room. The startup log prints the choice:
```
strata generate: layer split auto: K=19 - the caches hold 11767 of 12288 profiled pairs (fullest device 100%)
strata serve: layer split: layers 0-18 (CUDA0), 19-47 (CUDA1), one hand-off per window
```
## What each card holds
- **every card**: a copy of the dense weights (~3.4 GB for the Coder), its own session state (the KV cache of the full
context), its verify window and its prompt-path buffers, and an expert cache for its layers filled from the profile;
- **the last card**: also the output head and the draft layer (~0.8 GB);
- **host RAM**: the expert arena once, shared by all cards (the CPU pool computes whatever no card holds).
Prompts are read in chunks that flow through the cards in turn; while a later card reads chunk c, the first card
already reads chunk c+1. Conversation checkpoints save and restore every card's state; the adaptive expert swaps copy
into the card that owns the layer.
## Limits (for now)
- Not with images (`--vision`), control vectors / the speed projection, KV streaming (`--kv-resident`), the older
helper-GPU experiment (`--expert-cache-remote`, docs/SECOND_GPU.md) or `--mmap-experts`.
- The prompt path has its own buffers on every card (1.5 GB each at the default 2048-token chunk; `--prefill 1024`
halves that) instead of borrowing cache slots as one card does.
- Mid-prompt checkpoints (`--prompt-cache-every`) are off; the ones at turn boundaries - what a chat reuses - stay.
- On Windows only 8 GiB of the expert arena is pinned (more, mapped into two GPU contexts, leaves WDDM refusing
allocations); the PCIe share covers those layers.
- Every card needs compute capability 8.0 (RTX 30 or newer). A Turing card (RTX 20, sm_75) builds only with the
experimental `-DSTRATA_EXPERIMENTAL_SM75=ON`.
## Measured
The Coder on an RTX 5080 + RTX 3090 (Ryzen 9 9950X3D), 32K context; details in
`bench/results/2026-09-29-layer-split/`:
| | Prompt 16K / 28K tok/s | Decode story / code tok/s |
|---|---|---|
| 5080 alone | 1,726-2,017 / 1,970 | 83-87 / 88-105 |
| 5080 + 3090, best split (K=26) | 2,039 / 2,357 | 84 / 110 |
| 5080 + 3090, auto (K=22) | 2,037 / 2,073 | 80 / 109 |
- **Prompts gain the most** (+18-20%): each card reads its own layers of the chunk while the other reads the next.
- **Decode is on par with the faster card alone**, and ahead on code. Once both caches hold nearly every routed
expert, the per-layer GPU time decides.
- **Correctness:** one GPU is byte-identical to 0.1.20, and the hand-off itself is bit-exact.
**Which cards and in what order:**
- Put the fastest card first; auto gives it as many layers as its cache allows.
- Leave out a much slower card when two already hold the model. An RTX 2080 Ti as a third card made the 5080 +
3090 pair slower (68 / 90 tok/s decode): every extra card costs its own round per window.
- More cards pay off when the model's routed experts do not fit the faster ones.
+95
View File
@@ -0,0 +1,95 @@
# OrcaRouter IQ3_XXS compatibility
Validated on Linux with an RTX 5090 (32 GB), Ryzen 9 9950X3D and 128 GB RAM on 2026-09-27.
The target is `orcarouter/Qwen3.8-Flash-Next-Uncensored-GGUF`, **IQ3_XXS** (two shards, 85.20 GB total).
It has Strata's Qwen4Exp geometry, but ordinary quantization also compresses the hyperconnection,
small attention and PLE projections which Strata's kernels read as BF16.
`tools/iq_pack.py --compat-bf16` expands only those projections, rounding to BF16 using nearest-even.
This adds rounding relative to the GGUF's dequantized weights; it does not recover the original BF16
checkpoint or its quality. Expert weights, native attention weights, embeddings and the 28.8 GB PLE
lookup table remain unchanged. The latter stays on disk. The pack records its conversions in
`compat-bf16.json`.
The native loader retains this pack's converted PLE key. Its existing native Q2_0 key path remains
available to the original model. Hugging Face snapshot symlinks are supported; all split shards must
finish downloading before packing. Pass the snapshot filename, not its hash-named blob target.
## Preparation
Build Strata normally using the pinned llama.cpp dependency. Python needs numpy, regex and gguf-py
from that dependency (`STRATA_GGUF_PY` can point to its `gguf-py` directory).
```sh
.venv/bin/python tools/iq_pack.py \
--gguf /path/to/Qwen3.8-Flash-Next-Uncensored-IQ3_XXS-00001-of-00002.gguf \
--out packs/orca-iq3_xxs --compat-bf16
```
Use this model's own tokenizer exported into the pack. Do not share another model's `dense.bin`,
and do not rename the Orca files to impersonate one of setup's GSQ-RCO models.
Strata's persistent server also requires the MTP runtime, prepared by the existing tools:
```sh
.venv/bin/python tools/mtp_fetch.py fetch --out mtp
.venv/bin/python tools/mtp_pack.py --src mtp --experts q2_0 --out mtp/mtp-q2_0.gguf
.venv/bin/python tools/mtp_rt.py --gguf mtp/mtp-q2_0.gguf --out mtp/rt
cp data/draft_vocab.bin mtp/rt/draft_vocab.bin
```
This uses the original model's draft head, as setup does for Swift. The target model verifies its
proposals; draft acceptance and performance must be measured for this fine-tune.
## Local server
Save the following as `strata-orca-iq3_xxs.json` at the repository root, replacing both `/path/to/`
entries with the **same first shard**. Orca's PLE table is in shard 1. Start with a 32K context and
512-token prefill chunks; the expert cache sizes itself to available VRAM. The expert arena alone
needs about 49.8 GiB of available system RAM, plus draft/runtime buffers and other applications.
```json
{
"exe": "build/strata",
"args": [
"--pack", "packs/orca-iq3_xxs",
"--native", "/path/to/Qwen3.8-Flash-Next-Uncensored-IQ3_XXS-00001-of-00002.gguf",
"--ple-gguf", "/path/to/Qwen3.8-Flash-Next-Uncensored-IQ3_XXS-00001-of-00002.gguf",
"--expert-profile", "data/expert-profile.bin", "--expert-cache", "auto",
"--prefill", "512", "--spec", "4", "--spec-min-p", "0.5",
"--mtp", "mtp/rt", "--max-context", "32768", "--kv", "int8"
],
"cwd": ".",
"tokenizer": "packs/orca-iq3_xxs/tokenizer",
"model_name": "orcarouter-qwen3.8-flash-next-uncensored-iq3_xxs",
"log": "strata-orca-iq3_xxs.log",
"host": "127.0.0.1",
"port": 8080
}
```
Run from the repository root:
```sh
.venv/bin/python -m serve.server --engine strata --config strata-orca-iq3_xxs.json --port 8080
```
The web interface is at `http://127.0.0.1:8080`; API clients use `http://127.0.0.1:8080/v1`.
Vision is not configured in this text-only example. Performance of the original GSQ-RCO model
does not establish this fine-tune's speed or accuracy.
## Scope and validation
- IQ3_XXS is the target of this change; other Orca quantizations are not validated.
- IQ3_M additionally uses Q5_0 expert down matrices, which the native GPU expert path does not support.
- The installer model menu is unchanged. This is an explicit local packing workflow.
- Focused conversion and split-file tests: `.venv/bin/python -m unittest discover -s tools -p test_iq_pack.py`.
- All eight packing tests, 17 server tests and the GPU gated-residual parity check passed.
- The complete model packed successfully: 460 tensors converted, 1.39 GiB of converted BF16 weights;
1.43 GiB total packed dense weights. The 48 expert layers remain IQ3_XXS gate/up and IQ4_NL down.
- Real server checks passed: arithmetic, Python code, translation, a longer explanation, multi-turn context
reuse (25 prompt tokens reused), streaming cancellation and a subsequent correct response. The engine
PID stayed unchanged. These are smoke tests, not a quality benchmark or a full-context stress test.
- With the configuration above, a 111-token explanation generated at 77.7 tokens/s (engine decode timing);
total request time was 1.77 seconds. This single short measurement is not a general throughput claim.
+84
View File
@@ -0,0 +1,84 @@
# CUDA1–3 expert experiment
CUDA0 keeps the dense layers, KV/state, MTP and its existing expert cache. Up to
three independent expert caches fill CUDA1, CUDA2 and CUDA3 in that order. Each
expert has one owner: CUDA0, one secondary GPU, or the CPU pool. Each secondary
GPU computes its rows while the CPU pool works; the results return through
private pinned host buffers before CUDA0 continues the layer. The GPU launches
can overlap, but each layer waits for all three.
The shipped profile ranks 8,000 `(layer, expert)` pairs. CUDA0 takes its usual
`--expert-cache auto` allocation. With only CUDA1, it takes the first remaining
pairs. When CUDA2 or CUDA3 is enabled, the remaining profiled pairs are assigned
round robin to the secondary GPUs. The ranked list is then extended with all
other pairs in expert-then-layer order and distributed in the same way. This
fills the additional VRAM and lets all secondary GPUs see frequent experts, but
unranked experts may hardly ever be routed. The startup
log reports slots and GiB per card; each request reports the number of expert
entries actually computed on each secondary GPU. Remote outputs are packed
before returning over PCIe/USB4: the request log compares MiB transferred
with the previous full-row transfer. The input and routing metadata use pinned
host staging. This reduces transfer traffic and launch overhead, but does not
release the expert arena in system RAM or significantly change VRAM use.
The optional `--gpu-placement layer` gives each layer to exactly one secondary
GPU (layer number modulo the number of enabled cards). It fills that card with
the most frequently routed experts of its layers first, then the unranked ones.
This can reduce USB4 device switches and synchronization when several secondary
cards serve the same layer in the default `stripe` mode. All three cards still
keep their requested expert slots; the primary CUDA0 cache and CPU fallback
continue to work. The log counts active layer launches on each card, so the
tradeoff can be measured alongside tokens per second. This placement is
experimental and may be slower if a given layer needs more than one card's
compute capacity.
The experiment requires the corresponding visible CUDA devices and a CUDA
enabled build. Each card must keep at least 512 MiB free. Tiers must be enabled
in order. On Windows, CUDA registration of the host expert arena is capped at
8 GiB to leave room for the contexts and MTP on CUDA0. The rest remains
available to the CPU pool; the PCIe expert path is available only for the
registered layers. Without secondary GPUs, the original uncapped registration
behavior applies.
On an existing Linux installation, apply the patch to its source tree, then run:
```sh
./setup.sh --setup --gpu1-experts 5000 --gpu2-experts 5000 --gpu3-experts 5000
```
On Windows, from PowerShell in the installation folder:
```powershell
.\START-HERE.bat --setup --gpu1-experts 5000 --gpu2-experts 5000 --gpu3-experts 5000
```
To test the layer placement instead, add `--gpu-placement layer` to the setup
command. To go back, run the same command with `--gpu-placement stripe`. After
the new engine is built, the mode can also be changed without a rebuild: edit
`--expert-cache-remote-placement` in `strata-iq3_xxs.json` from `layer` to
`stripe` or vice versa and restart the server. If the argument is absent, the
default is `stripe`.
Choose the same model, size, context and image settings as your current
installation when prompted. Setup recompiles the engine, updates its server
configuration, and starts it. Later starts keep those settings. To tune the
slots without recompiling, edit the values after `--expert-cache-device1`,
`--expert-cache-device2` and `--expert-cache-device3` in the stored
`strata-iq3_xxs.json`, then restart with `.\run-iq3_xxs.bat`.
Compare identical requests at one, two and four GPUs, preferably with several
repeats. VRAM use alone does not show useful offload: compare the per-request
CUDA1–3 counts and tokens per second. More GPU contexts and synchronization
may lower the speed. Prompt prefill still uses CUDA0; secondary GPUs serve
decode, including MTP verification. No peer-to-peer access is required.
Longer contexts reserve more KV/state memory on CUDA0, which reduces its
automatic expert cache. On a 64 GB PC `setup.py` limits IQ3_XXS to 128K even
if 262K was selected; the secondary caches still require the host expert arena.
To try 262K on 64 GB, choose Q2_0 or IQ2_XS instead and check that CUDA0 still
has enough free VRAM after loading. Benchmark long prompts separately from
short decode requests.
The existing warning about the CUDA0 expert-cache GPU hit path still applies:
its outputs diverge from cache-off runs. Treat performance as experimental
until the generated tokens have been validated.
+23
View File
@@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 330" width="960" height="330" role="img" aria-label="Speculative decoding: a small helper guesses words, the big model checks them at once"><title>Speculative decoding: a small helper guesses words, the big model checks them at once</title><style>
@font-face{font-family:"Outfit";src:url(data:font/woff2;base64,d09GMgABAAAAAH4kABQAAAABEvgAAH2tAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGokKG4GFEhyEfD9IVkFShwg/TVZBUlAGYD9TVEFUgR4AhQwvbBEICoGIUOtMC4N0ADCB3i4BNgIkA4dkBCAFhVoHhwAMB1smAVHBNga67bYBAKcffRoLZLBt+OnNYgAVeRfORkSwcSDmgX9Ls//PSU5kjM0Ctl/TNA3MJGWu6mqOhpMJjl69fYTurl4HxsC+V1F1NLEfzQZVm1CuUAwiPmZYiXF0GFOYTsmghiQV7CXfcKZM+TZcm2TjssMkfgwGl7QUhM0ORt82irww786YFU4we4TpaLjbOFnR2Gx+/0aBd6Inf+6DilwyuZCRVyLoNcsA2/VBFBnW4y15iP8/+37VPvFVukdSrqQMlQH9RH7BqiPS7X/2LglJCAFCKC2EEEIMLUREBPSHUAzViIhYGgFEQERARGwgIUIohioC8hBDLZaODRuWhqV+nhQ7NiyvQR/s97+Zs/vu/U00WWiqKYuFJAaESKZmEqVb4v+k0743oBlppJGBNw478JG5uqswG9/9orzqKqoJcAGTOGQ7toCtoX/6e9ie+4Ik0TzkJKiZpmn8Y2CBQMDNH57m9F/il5zJu/PkJCchCgl4kHYdUtG1Ezcy2HfrN7VOpT6jSilWLPvB/aGP5t452D1KCgTGfS1fQBhTozr/ufnfDRoKNaeKv1Ko0QeFaJN8IvDM0CQtZagi2vaLWhnxHkY71tGWt+uZ7T+zeqvx1fhqgOcfu7f3Y05gz0KelU6/5T1pipVbqAvMRQD+wM9pr3WowiiEOBAh/n++ROxb1D7EsYQAKVAKlBpdx5ROrbczLz1vdyp0JnZ3Vf9nZsKcz3M59rSvu2c3RrOZjBHBqKkI6sf8BpEKYx7CiOCuQ7hZ/wNUDKlTcbqu3b12T1XYu/dNT8RWtCqYFU2wEOKEBIKY79vm5SNvZCxNQ7AiE5HbeTqcaGBAmga6iV1dXdj7/+XW917SWpf62i6RP+JeElI0wUPDmVmZsoXtqAtB7vjAHqElbS2wFmJbgEURP2jEZq6wR3wmhoFNdjNLyDQJloAzSBwKhRKfbIJ4nJjHGIxEuC7OKYw5Wz1APhAlHc9/v7d+dt4sQncTDynLT7X8wiAMzt+5b4YJG7KCRw5T9RQ5WxwaFEJSvyGrkKREacH/3+ustu/r6YII7pbsCt+unpmnjo+J6rwcG6tz3k3Iq149/Y/gSx/hD9guCZniA6bORxTVH6jwwRWwJHxADolTx3ZnUyGFC9hTH1NVI7D7DNXR1XFCiK7JcReXIZ59Covl7GZWq5R2vZ/VfvbL2Y///Vqfdk6f3x/4LnLX1uwsTa0wINQ4OfPe0uvzid8CJ71AHSK1pCJRhmWEAUSZio5RiQpUm5KzC6ZJCgkSHpFD5LCpH57m+376zJTc9p2liBCEEcYYMQghhBCuMSKY7POr6/drGp9qqUzyJlmkWNDDZTOMTV2+wyhAcsKQ0gaTUCfZWW0ZB1/mCBxj7t8zsTfGtrEIEJGIEO8bjm9tjvTgOWoQGiQaQn52aFiM2A/8+t+FgEcHYBiUHiiLMJFDZWbEx7JgUM8JCwbJsAsIBHg4QIJ0DUBLrGVCSbgRYGHAR5yFBIcNgKozHwkODYCqdQsSjPkFUDWXIMEwL4Cq/goSDG0AVLU4JBjiAKjKFCRIzwBUkYEEKUl21sgYkMfR2yZ3tYNh4Oy2zlmNmbQ0beNnVaY6FQ3pv1bnRYjLBA3etNsas6wdxxCI6pkBSUPNbt1oBRdqm1fXXtpMfaXK77yFCYQM0WYAMWQIMWYOscSCsPEhjlwhbtwg7iQQX34QqWBIqFCIzGLIEkshy62FyEVD4qyHJEiGpEiBpNnS+BTmentXkTETcO+8h0MQ0IIQjRk3sfSBRaAcCgGUXQEPA8cDe8JhMCDsjj8oCBV1cT5rgYQ+mv74brKPVHiS2pMcOUvLOdTnujXWxYk6JuuY4u1Ur2YHYaEr8+xe4p+lKFcYWxWzL8PsWuUG1VsU9iPhYkcuCavHCLvSmaucv84poADxu1/TRkLcJmeekZ+tnGPSo8iJooieY1OyVLmczrI8yfO8EZIYY4oZsUgj0ormZTHKqE6tP0EGitN4ElOcx3zJVQVHESHTlNI8naWckppVNrnIbXH50qpqj3zCcBB4KAoYKjQI6NBjwLimWViLdSYRU6g0VmwY2LHnlpdtO+x+75XnIpWpUq1GQb1GLbTSoYve9X6UK244cM8jL3jFL7cWkUaC9DLIKAtZyca3fulPGrTXUVe60a3udK8HPelN7/rQX3G3nR3sZkKDAoWgV6AXkAK7INZpEWqFVpFJLBbrxaTUKCWldmlampEhspAs3aXpqncHFbx6WkMbDhobyF58iuj3LYQOZQ6NTNZnYk7IQEC//UfJNDpjFSAYOJAYm5gYBNDBJ4NNmDRjAfqOtZf10REAmQoIGHz7xkv6OP/Fmcw+trsJ8y91TS8fNTZfG2stKvpJNCoSPQgViObkOAlqbkdD5EGIxwIN7kLhjQqoMKYXQOVSkTYVLYbLXNWoaiW4ql9A3cEH6i2uY22HIYLdKDfV0FiTi5kPPNFW2+2w0y7ECPaRmggNxggJHucchtqMhBwegErlvCEhxZaHtIXz5cqjzE8Rb9SuX+CC0dveU+cG5pyQe6oOEBKsu7hu0/gG0tLp1ItZ8W+8Nk5+dTWGqK1EcEQE9Ao0CcFFqGRtZehdDhziSkhCzTstynR0eC1oy8UkGIolMawinWUIJYm2mMduHZ3DCRGHajdYkGNddS77pABMbBiptWOh/vfEuaZ8naUP4z3SQ32km7qylZ1ei20/d0UtuObVouY3q5lNxX/XVI3Xs7pb5+tYdYW/Q1PVpS5lpVeJ1YoquPIqUfGLVcyiYr8zlfGMZChH0pTKKJNeYssKHh5cJMU9TuGu/ynMUDFAf/e9zxz2lH02WGWxCjMjOdaELMQhlKeFDMnw06QX7rtoAK6+lEC94B0yo0Qbj2GwFTy/96/iCiv656YqmI269a7a34loMjYp3nNWgFQUlaeXT4VfcWisygUWleqeAAwkpiu0Fk1X1ZMUTBpZQzk9dG0ORbKyHFaR8urpk8GIVrCgFTi0Ap9WkEgnGjrGdEY6i+Angkmr83D7QADXgMzSfE2yTppua6a1hYtYehwzjsR0Vbe1F1mixKdDKTiUgSPfiQdVpFbrO7zfguddFEYq/gCpGPr54iqWdRgzMNVd7IoHLdwaBCeQE9Cs7ahPChbx3KYKpWIGaMqNPlXyHir0+1lVNMsDDPLJ9ZqYBjGpBJ058wWq5DLN/SeqDY9dQw0sGkTHqorm8D3ojCTRGCBRFnNomEgWyhl4fVGxt/wQUypcRGZqcDH5JL2QGZtpLgNiZ8kn/o149gV5bbu0Ym+G4W5V0Wz8E7lLaAOnhNYp9QyRm1ymAd/98BjLExYxTbmYLtNDOemrotm8F8Y1N1qF6lApDlXhyLvwYIAMBc2haW9dmCLD3pFiESoSLi6q2GS1dthIo4Bkho/TrFqzlupHGsiElg6Jt6YnYzqhsN8J2W3HVhcNeth3NEQemC0iGQyMJFPfGFg0pgmtMWOxNwFd6WaeFsIrLCEsbZKMjfBpokAYxBAuUvjAqMhzMix6MQmn4/B891PX9XOXwKEO/Bwe7GUmBDzAKpgxBL0HyqUSR5RmGUQHYMiwVo1/vNZ78AXNh4dSmBjunffaVnmqfYxuwgvWIrtr/eYE03a/6K22K7RIV99vjza3f0Z6/HWTgfaXnO/vQ16j3SJNMP0bJXSjAdDybvLPz7Nt9xz/sPL5/W9teM7L7UyZNfcIepbqdaoa3jiu0fnonMemvb99yw9/6h7BZfq/GsgeLdu2fBnVr2ut71WjjWN0UdNofdee8+pgtHT21FQurRX5p913/7dKAz1z0ouDVjG6bOUSCxiNtdsqXBw34WUnvze3502V1m7aerHVta+mW916XLb6kM5m+MMAlr7HLko6U+RltyLQU7HXajv554cXvnER3XejmdNLu76/Ff9yFqjYNr2etMTnvtK7tT0zpdFMlXPO1042b1Lyuk/w7pvy8v2u922RjpO/v992uRPUVaXQUN1PcM69E9jPt/IJ5yY8c8dvwe0nzj+MTJwo9eOW1itAEL9VhqjLC3W/qkR7Kvtai3w9yb050yNIrWtTXi05i9bt+f69133KOfS4un/raPI9y59Q6P8rWY6RbOFeR3fcvlVqujXu6tE0Ul56CyhlqjlGV2P6guh+nZ6BTqfe9IwZ62o6R925W2r4xMLERl+TkZguQlm70j7O2f3Wmy3TXiuVixHtG9njX2+1o+pR/aJfxXRWM47ypkseclXSdYFj0+gsUE5Ng0JPkKZh9kTdxx/GtjtEj62xEBRXJqGiwkosCHQwbYYMzYj5BUvWsHJbwQTsg0MzAkfDOJephFyR60Ba1OF58RN8dFWLL//rnxDCMOUSqHqBBbRE7qllmRVwK6+PEQu3DkPiDJdQT5EoGdGmQlua9D6fYdvKttvpKvlwPQ0O0XXKRnemnu6Ci/RdMuSGYaYLOgz73kCGozwdocUkgFjwaZkhSAgl0kFEjXZxAzb5oZUVekFBpQzoQjksWP7ISDHoBnRk5LwlPXHpDEnpOt10RhHL6Ubl2OlCQQZf4MaXaGG80gtCjfhq+RwLTzheczxy2rOCT/Hz/E78k0HLjtNrX+DW7o8Zgd49iVpCtG5FE1vhtycRxPUXWn7Pf8//RymdGHJRezffyufZxY1Q53Nif6XzDScbnmv4IXWc/JnYgz/ZPlTn/pzMx9s4lkPpF7F/wW7Y+rru1+hjW03Sv9ehv5ral/h/hRT7Wuzr2DUbxBuYH0b017i0fDXc2fLLcFPYIhn64mA87DDBsCA0Line truncated
text{font-family:"Outfit",system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;fill:#221e1f}
.bg{fill:#f9fafb}.card{fill:#ffffff;stroke:#dee5ee;stroke-width:1}.soft{fill:#f7f7f7}
.muted{fill:#6f6f6f}.ink{fill:#221e1f}.icon{color:#2F384C}
.acc{fill:#10b981}.acc-t{fill:#0c8a60}.acc-tint{fill:#ecfdf5}.acc-line{stroke:#10b981}
.info{fill:#0f97ff}.info-t{fill:#0668c2}.info-tint{fill:#e8f4ff}
.warn{fill:#e39b0b}.warn-t{fill:#8a5a00}.warn-tint{fill:#fff6e0}
.dan{fill:#e0283f}.dan-tint{fill:#fdecee}.dan-t{fill:#b01a2e}
.dot{fill:#dee5ee}.line{stroke:#dee5ee}.arrow{stroke:#6f6f6f;fill:none}.arrowhead{fill:#6f6f6f}
.dash{stroke:#6f6f6f;fill:none;stroke-dasharray:4 4}
.on-acc{fill:#ffffff}.icon-acc{color:#0c8a60}.icon-dan{color:#b01a2e}
@media (prefers-color-scheme:dark){
text,.ink{fill:#eef1f3}.bg{fill:#0e1113}.card{fill:#161a1d;stroke:#2a3036}.soft{fill:#1d2226}
.muted{fill:#8e979f}.icon{color:#c7cdd3}
.acc{fill:#34d399}.acc-t{fill:#4ade80}.acc-tint{fill:rgba(52,211,153,.12)}.acc-line{stroke:#34d399}
.info{fill:#3aa9ff}.info-t{fill:#7cc4ff}.info-tint{fill:rgba(58,169,255,.12)}
.warn{fill:#f5b53d}.warn-t{fill:#f8c96a}.warn-tint{fill:rgba(245,181,61,.12)}
.dan{fill:#ff5a6e}.dan-tint{fill:rgba(255,90,110,.12)}.dan-t{fill:#ff8a98}
.dot{fill:#2a3036}.line{stroke:#2a3036}.arrow{stroke:#8e979f}.arrowhead{fill:#8e979f}.dash{stroke:#8e979f}
.on-acc{fill:#052e20}.icon-acc{color:#4ade80}.icon-dan{color:#ff8a98}
}
</style><rect x="0" y="0" width="960" height="330" rx="20" class="bg"/><rect x="24" y="24" width="912" height="282" rx="14" class="card"/><g class="icon" transform="translate(48,48) scale(1.2500)"><path opacity="0.12" d="M12 2.5C8.41 2.5 5.5 5.41 5.5 9C5.5 11.3 6.7 13.3 8.5 14.5V16.5C8.5 17.05 8.95 17.5 9.5 17.5H14.5C15.05 17.5 15.5 17.05 15.5 16.5V14.5C17.3 13.3 18.5 11.3 18.5 9C18.5 5.41 15.59 2.5 12 2.5Z" fill="currentColor"></path><path d="M12 2.5C8.41 2.5 5.5 5.41 5.5 9C5.5 11.3 6.7 13.3 8.5 14.5V16.5C8.5 17.05 8.95 17.5 9.5 17.5H14.5C15.05 17.5 15.5 17.05 15.5 16.5V14.5C17.3 13.3 18.5 11.3 18.5 9C18.5 5.41 15.59 2.5 12 2.5Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M9.5 20.5H14.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></g><text x="92" y="70" font-size="19" font-weight="700" class="ink" text-anchor="start">Guess, then check - many words per step</text><text x="92" y="94" font-size="15" font-weight="400" class="muted" text-anchor="start">A small, fast helper inside the model guesses the next words. The big model checks all the guesses in one go</text><text x="92" y="113" font-size="15" font-weight="400" class="muted" text-anchor="start">and keeps the ones it agrees with. The big model decides every word - it just arrives 1.6-1.8x sooner.</text><text x="48" y="180" font-size="14" font-weight="600" class="muted" text-anchor="start">Helper guesses</text><text x="48" y="262" font-size="14" font-weight="600" class="muted" text-anchor="start">Big model checks</text><rect x="176" y="156" width="61" height="38" rx="10" class="dash" stroke-width="1.4"/><text x="206.5" y="181" font-size="16" font-weight="500" class="ink" text-anchor="middle">def</text><rect x="251" y="156" width="97" height="38" rx="10" class="dash" stroke-width="1.4"/><text x="299.5" y="181" font-size="16" font-weight="500" class="ink" text-anchor="middle">reverse</text><rect x="362" y="156" width="88" height="38" rx="10" class="dash" stroke-width="1.4"/><text x="406.0" y="181" font-size="16" font-weight="500" class="ink" text-anchor="middle">(items</text><rect x="464" y="156" width="52" height="38" rx="10" class="dash" stroke-width="1.4"/><text x="490.0" y="181" font-size="16" font-weight="500" class="ink" text-anchor="middle">):</text><rect x="176" y="238" width="61" height="38" rx="10" class="acc-tint"/><text x="198.5" y="263" font-size="16" font-weight="600" class="acc-t" text-anchor="middle">def</text><g class="icon-acc" transform="translate(215,249) scale(0.6667)"><path d="M4.5 12.5L9.5 17.5L19.5 6.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></g><line x1="206.5" y1="196" x2="206.5" y2="234" class="line" stroke-width="1.4"/><rect x="251" y="238" width="97" height="38" rx="10" class="acc-tint"/><text x="291.5" y="263" font-size="16" font-weight="600" class="acc-t" text-anchor="middle">reverse</text><g class="icon-acc" transform="translate(326,249) scale(0.6667)"><path d="M4.5 12.5L9.5 17.5L19.5 6.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></g><line x1="299.5" y1="196" x2="299.5" y2="234" class="line" stroke-width="1.4"/><rect x="362" y="238" width="88" height="38" rx="10" class="acc-tint"/><text x="398.0" y="263" font-size="16" font-weight="600" class="acc-t" text-anchor="middle">(items</text><g class="icon-acc" transform="translate(428,249) scale(0.6667)"><path d="M4.5 12.5L9.5 17.5L19.5 6.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></g><line x1="406.0" y1="196" x2="406.0" y2="234" class="line" stroke-width="1.4"/><rect x="464" y="238" width="52" height="38" rx="10" class="dan-tint"/><text x="482.0" y="263" font-size="16" font-weight="600" class="dan-t" text-anchor="middle">):</text><g class="icon-dan" transform="translate(494,249) scale(0.6667)"><path opacity="0.12" d="M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z" fill="currentColor"></path><path d="M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M15 9L9 15M9 9L15 15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></g><rect x="526" y="238" width="89" height="38" rx="10" class="acc-tint"/><text x="570.5" y="263" font-size="16" font-weight="600" class="acc-t" text-anchor="middle">, key):</text><line x1="490.0" y1="196" x2="490.0" y2="234" class="line" stroke-width="1.4"/><line x1="655" y1="257" x2="678" y2="257" class="arrow" stroke-width="1.6" stroke-linecap="round"/><path d="M685 257 l-8 -5 v10 z" class="arrowhead"/><rect x="697" y="232" width="215" height="50" rx="12" class="acc"/><g class="icon" style="color:#ffffff" transformLine truncated

After

Width:  |  Height:  |  Size: 49 KiB

+23
View File
@@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 470" width="960" height="470" role="img" aria-label="How Strata splits the model between your graphics card, RAM and SSD"><title>How Strata splits the model between your graphics card, RAM and SSD</title><style>
@font-face{font-family:"Outfit";src:url(data:font/woff2;base64,d09GMgABAAAAAH4kABQAAAABEvgAAH2tAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGokKG4GFEhyEfD9IVkFShwg/TVZBUlAGYD9TVEFUgR4AhQwvbBEICoGIUOtMC4N0ADCB3i4BNgIkA4dkBCAFhVoHhwAMB1smAVHBNga67bYBAKcffRoLZLBt+OnNYgAVeRfORkSwcSDmgX9Ls//PSU5kjM0Ctl/TNA3MJGWu6mqOhpMJjl69fYTurl4HxsC+V1F1NLEfzQZVm1CuUAwiPmZYiXF0GFOYTsmghiQV7CXfcKZM+TZcm2TjssMkfgwGl7QUhM0ORt82irww786YFU4we4TpaLjbOFnR2Gx+/0aBd6Inf+6DilwyuZCRVyLoNcsA2/VBFBnW4y15iP8/+37VPvFVukdSrqQMlQH9RH7BqiPS7X/2LglJCAFCKC2EEEIMLUREBPSHUAzViIhYGgFEQERARGwgIUIohioC8hBDLZaODRuWhqV+nhQ7NiyvQR/s97+Zs/vu/U00WWiqKYuFJAaESKZmEqVb4v+k0743oBlppJGBNw478JG5uqswG9/9orzqKqoJcAGTOGQ7toCtoX/6e9ie+4Ik0TzkJKiZpmn8Y2CBQMDNH57m9F/il5zJu/PkJCchCgl4kHYdUtG1Ezcy2HfrN7VOpT6jSilWLPvB/aGP5t452D1KCgTGfS1fQBhTozr/ufnfDRoKNaeKv1Ko0QeFaJN8IvDM0CQtZagi2vaLWhnxHkY71tGWt+uZ7T+zeqvx1fhqgOcfu7f3Y05gz0KelU6/5T1pipVbqAvMRQD+wM9pr3WowiiEOBAh/n++ROxb1D7EsYQAKVAKlBpdx5ROrbczLz1vdyp0JnZ3Vf9nZsKcz3M59rSvu2c3RrOZjBHBqKkI6sf8BpEKYx7CiOCuQ7hZ/wNUDKlTcbqu3b12T1XYu/dNT8RWtCqYFU2wEOKEBIKY79vm5SNvZCxNQ7AiE5HbeTqcaGBAmga6iV1dXdj7/+XW917SWpf62i6RP+JeElI0wUPDmVmZsoXtqAtB7vjAHqElbS2wFmJbgEURP2jEZq6wR3wmhoFNdjNLyDQJloAzSBwKhRKfbIJ4nJjHGIxEuC7OKYw5Wz1APhAlHc9/v7d+dt4sQncTDynLT7X8wiAMzt+5b4YJG7KCRw5T9RQ5WxwaFEJSvyGrkKREacH/3+ustu/r6YII7pbsCt+unpmnjo+J6rwcG6tz3k3Iq149/Y/gSx/hD9guCZniA6bORxTVH6jwwRWwJHxADolTx3ZnUyGFC9hTH1NVI7D7DNXR1XFCiK7JcReXIZ59Covl7GZWq5R2vZ/VfvbL2Y///Vqfdk6f3x/4LnLX1uwsTa0wINQ4OfPe0uvzid8CJ71AHSK1pCJRhmWEAUSZio5RiQpUm5KzC6ZJCgkSHpFD5LCpH57m+376zJTc9p2liBCEEcYYMQghhBCuMSKY7POr6/drGp9qqUzyJlmkWNDDZTOMTV2+wyhAcsKQ0gaTUCfZWW0ZB1/mCBxj7t8zsTfGtrEIEJGIEO8bjm9tjvTgOWoQGiQaQn52aFiM2A/8+t+FgEcHYBiUHiiLMJFDZWbEx7JgUM8JCwbJsAsIBHg4QIJ0DUBLrGVCSbgRYGHAR5yFBIcNgKozHwkODYCqdQsSjPkFUDWXIMEwL4Cq/goSDG0AVLU4JBjiAKjKFCRIzwBUkYEEKUl21sgYkMfR2yZ3tYNh4Oy2zlmNmbQ0beNnVaY6FQ3pv1bnRYjLBA3etNsas6wdxxCI6pkBSUPNbt1oBRdqm1fXXtpMfaXK77yFCYQM0WYAMWQIMWYOscSCsPEhjlwhbtwg7iQQX34QqWBIqFCIzGLIEkshy62FyEVD4qyHJEiGpEiBpNnS+BTmentXkTETcO+8h0MQ0IIQjRk3sfSBRaAcCgGUXQEPA8cDe8JhMCDsjj8oCBV1cT5rgYQ+mv74brKPVHiS2pMcOUvLOdTnujXWxYk6JuuY4u1Ur2YHYaEr8+xe4p+lKFcYWxWzL8PsWuUG1VsU9iPhYkcuCavHCLvSmaucv84poADxu1/TRkLcJmeekZ+tnGPSo8iJooieY1OyVLmczrI8yfO8EZIYY4oZsUgj0ormZTHKqE6tP0EGitN4ElOcx3zJVQVHESHTlNI8naWckppVNrnIbXH50qpqj3zCcBB4KAoYKjQI6NBjwLimWViLdSYRU6g0VmwY2LHnlpdtO+x+75XnIpWpUq1GQb1GLbTSoYve9X6UK244cM8jL3jFL7cWkUaC9DLIKAtZyca3fulPGrTXUVe60a3udK8HPelN7/rQX3G3nR3sZkKDAoWgV6AXkAK7INZpEWqFVpFJLBbrxaTUKCWldmlampEhspAs3aXpqncHFbx6WkMbDhobyF58iuj3LYQOZQ6NTNZnYk7IQEC//UfJNDpjFSAYOJAYm5gYBNDBJ4NNmDRjAfqOtZf10REAmQoIGHz7xkv6OP/Fmcw+trsJ8y91TS8fNTZfG2stKvpJNCoSPQgViObkOAlqbkdD5EGIxwIN7kLhjQqoMKYXQOVSkTYVLYbLXNWoaiW4ql9A3cEH6i2uY22HIYLdKDfV0FiTi5kPPNFW2+2w0y7ECPaRmggNxggJHucchtqMhBwegErlvCEhxZaHtIXz5cqjzE8Rb9SuX+CC0dveU+cG5pyQe6oOEBKsu7hu0/gG0tLp1ItZ8W+8Nk5+dTWGqK1EcEQE9Ao0CcFFqGRtZehdDhziSkhCzTstynR0eC1oy8UkGIolMawinWUIJYm2mMduHZ3DCRGHajdYkGNddS77pABMbBiptWOh/vfEuaZ8naUP4z3SQ32km7qylZ1ei20/d0UtuObVouY3q5lNxX/XVI3Xs7pb5+tYdYW/Q1PVpS5lpVeJ1YoquPIqUfGLVcyiYr8zlfGMZChH0pTKKJNeYssKHh5cJMU9TuGu/ynMUDFAf/e9zxz2lH02WGWxCjMjOdaELMQhlKeFDMnw06QX7rtoAK6+lEC94B0yo0Qbj2GwFTy/96/iCiv656YqmI269a7a34loMjYp3nNWgFQUlaeXT4VfcWisygUWleqeAAwkpiu0Fk1X1ZMUTBpZQzk9dG0ORbKyHFaR8urpk8GIVrCgFTi0Ap9WkEgnGjrGdEY6i+Angkmr83D7QADXgMzSfE2yTppua6a1hYtYehwzjsR0Vbe1F1mixKdDKTiUgSPfiQdVpFbrO7zfguddFEYq/gCpGPr54iqWdRgzMNVd7IoHLdwaBCeQE9Cs7ahPChbx3KYKpWIGaMqNPlXyHir0+1lVNMsDDPLJ9ZqYBjGpBJ058wWq5DLN/SeqDY9dQw0sGkTHqorm8D3ojCTRGCBRFnNomEgWyhl4fVGxt/wQUypcRGZqcDH5JL2QGZtpLgNiZ8kn/o149gV5bbu0Ym+G4W5V0Wz8E7lLaAOnhNYp9QyRm1ymAd/98BjLExYxTbmYLtNDOemrotm8F8Y1N1qF6lApDlXhyLvwYIAMBc2haW9dmCLD3pFiESoSLi6q2GS1dthIo4Bkho/TrFqzlupHGsiElg6Jt6YnYzqhsN8J2W3HVhcNeth3NEQemC0iGQyMJFPfGFg0pgmtMWOxNwFd6WaeFsIrLCEsbZKMjfBpokAYxBAuUvjAqMhzMix6MQmn4/B891PX9XOXwKEO/Bwe7GUmBDzAKpgxBL0HyqUSR5RmGUQHYMiwVo1/vNZ78AXNh4dSmBjunffaVnmqfYxuwgvWIrtr/eYE03a/6K22K7RIV99vjza3f0Z6/HWTgfaXnO/vQ16j3SJNMP0bJXSjAdDybvLPz7Nt9xz/sPL5/W9teM7L7UyZNfcIepbqdaoa3jiu0fnonMemvb99yw9/6h7BZfq/GsgeLdu2fBnVr2ut71WjjWN0UdNofdee8+pgtHT21FQurRX5p913/7dKAz1z0ouDVjG6bOUSCxiNtdsqXBw34WUnvze3502V1m7aerHVta+mW916XLb6kM5m+MMAlr7HLko6U+RltyLQU7HXajv554cXvnER3XejmdNLu76/Ff9yFqjYNr2etMTnvtK7tT0zpdFMlXPO1042b1Lyuk/w7pvy8v2u922RjpO/v992uRPUVaXQUN1PcM69E9jPt/IJ5yY8c8dvwe0nzj+MTJwo9eOW1itAEL9VhqjLC3W/qkR7Kvtai3w9yb050yNIrWtTXi05i9bt+f69133KOfS4un/raPI9y59Q6P8rWY6RbOFeR3fcvlVqujXu6tE0Ul56CyhlqjlGV2P6guh+nZ6BTqfe9IwZ62o6R925W2r4xMLERl+TkZguQlm70j7O2f3Wmy3TXiuVixHtG9njX2+1o+pR/aJfxXRWM47ypkseclXSdYFj0+gsUE5Ng0JPkKZh9kTdxx/GtjtEj62xEBRXJqGiwkosCHQwbYYMzYj5BUvWsHJbwQTsg0MzAkfDOJephFyR60Ba1OF58RN8dFWLL//rnxDCMOUSqHqBBbRE7qllmRVwK6+PEQu3DkPiDJdQT5EoGdGmQlua9D6fYdvKttvpKvlwPQ0O0XXKRnemnu6Ci/RdMuSGYaYLOgz73kCGozwdocUkgFjwaZkhSAgl0kFEjXZxAzb5oZUVekFBpQzoQjksWP7ISDHoBnRk5LwlPXHpDEnpOt10RhHL6Ubl2OlCQQZf4MaXaGG80gtCjfhq+RwLTzheczxy2rOCT/Hz/E78k0HLjtNrX+DW7o8Zgd49iVpCtG5FE1vhtycRxPUXWn7Pf8//RymdGHJRezffyufZxY1Q53Nif6XzDScbnmv4IXWc/JnYgz/ZPlTn/pzMx9s4lkPpF7F/wW7Y+rru1+hjW03Sv9ehv5ral/h/hRT7Wuzr2DUbxBuYH0b017i0fDXc2fLLcFPYIhn64mA87DDBsCA0Line truncated
text{font-family:"Outfit",system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;fill:#221e1f}
.bg{fill:#f9fafb}.card{fill:#ffffff;stroke:#dee5ee;stroke-width:1}.soft{fill:#f7f7f7}
.muted{fill:#6f6f6f}.ink{fill:#221e1f}.icon{color:#2F384C}
.acc{fill:#10b981}.acc-t{fill:#0c8a60}.acc-tint{fill:#ecfdf5}.acc-line{stroke:#10b981}
.info{fill:#0f97ff}.info-t{fill:#0668c2}.info-tint{fill:#e8f4ff}
.warn{fill:#e39b0b}.warn-t{fill:#8a5a00}.warn-tint{fill:#fff6e0}
.dan{fill:#e0283f}.dan-tint{fill:#fdecee}.dan-t{fill:#b01a2e}
.dot{fill:#dee5ee}.line{stroke:#dee5ee}.arrow{stroke:#6f6f6f;fill:none}.arrowhead{fill:#6f6f6f}
.dash{stroke:#6f6f6f;fill:none;stroke-dasharray:4 4}
.on-acc{fill:#ffffff}.icon-acc{color:#0c8a60}.icon-dan{color:#b01a2e}
@media (prefers-color-scheme:dark){
text,.ink{fill:#eef1f3}.bg{fill:#0e1113}.card{fill:#161a1d;stroke:#2a3036}.soft{fill:#1d2226}
.muted{fill:#8e979f}.icon{color:#c7cdd3}
.acc{fill:#34d399}.acc-t{fill:#4ade80}.acc-tint{fill:rgba(52,211,153,.12)}.acc-line{stroke:#34d399}
.info{fill:#3aa9ff}.info-t{fill:#7cc4ff}.info-tint{fill:rgba(58,169,255,.12)}
.warn{fill:#f5b53d}.warn-t{fill:#f8c96a}.warn-tint{fill:rgba(245,181,61,.12)}
.dan{fill:#ff5a6e}.dan-tint{fill:rgba(255,90,110,.12)}.dan-t{fill:#ff8a98}
.dot{fill:#2a3036}.line{stroke:#2a3036}.arrow{stroke:#8e979f}.arrowhead{fill:#8e979f}.dash{stroke:#8e979f}
.on-acc{fill:#052e20}.icon-acc{color:#4ade80}.icon-dan{color:#ff8a98}
}
</style><rect x="0" y="0" width="960" height="470" rx="20" class="bg"/><rect x="24" y="24" width="912" height="92" rx="14" class="card"/><g class="icon" transform="translate(48,50) scale(1.5000)"><path opacity="0.12" d="M21 11.5C21 16.194 16.971 20 12 20C10.8 20 9.65 19.78 8.6 19.37L4 20.5L5.2 16.8C3.83 15.34 3 13.5 3 11.5C3 6.806 7.029 3 12 3C16.971 3 21 6.806 21 11.5Z" fill="currentColor"></path><path d="M21 11.5C21 16.194 16.971 20 12 20C10.8 20 9.65 19.78 8.6 19.37L4 20.5L5.2 16.8C3.83 15.34 3 13.5 3 11.5C3 6.806 7.029 3 12 3C16.971 3 21 6.806 21 11.5Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M8 11.5H8.01M12 11.5H12.01M16 11.5H16.01" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></g><text x="100" y="62" font-size="19" font-weight="600" class="ink" text-anchor="start">The model is made of 24,576 small specialists - “experts”.</text><text x="100" y="88" font-size="15" font-weight="400" class="muted" text-anchor="start">For each word it writes, it asks only 10 of them. Strata keeps the busy ones</text><text x="100" y="106" font-size="15" font-weight="400" class="muted" text-anchor="start">on your graphics card and the rest in RAM - so it runs on a normal PC.</text><circle cx="698.0" cy="46.0" r="2.6" class="dot"/><circle cx="705.6" cy="46.0" r="2.6" class="dot"/><circle cx="713.2" cy="46.0" r="2.6" class="dot"/><circle cx="720.8" cy="46.0" r="2.6" class="dot"/><circle cx="728.4" cy="46.0" r="2.6" class="dot"/><circle cx="736.0" cy="46.0" r="2.6" class="dot"/><circle cx="743.6" cy="46.0" r="2.6" class="dot"/><circle cx="751.2" cy="46.0" r="2.6" class="dot"/><circle cx="758.8" cy="46.0" r="2.6" class="acc"/><circle cx="766.4" cy="46.0" r="2.6" class="dot"/><circle cx="774.0" cy="46.0" r="2.6" class="dot"/><circle cx="781.6" cy="46.0" r="2.6" class="dot"/><circle cx="789.2" cy="46.0" r="2.6" class="dot"/><circle cx="796.8" cy="46.0" r="2.6" class="dot"/><circle cx="804.4" cy="46.0" r="2.6" class="dot"/><circle cx="812.0" cy="46.0" r="2.6" class="dot"/><circle cx="819.6" cy="46.0" r="2.6" class="acc"/><circle cx="827.2" cy="46.0" r="2.6" class="dot"/><circle cx="834.8" cy="46.0" r="2.6" class="dot"/><circle cx="842.4" cy="46.0" r="2.6" class="dot"/><circle cx="850.0" cy="46.0" r="2.6" class="dot"/><circle cx="857.6" cy="46.0" r="2.6" class="dot"/><circle cx="865.2" cy="46.0" r="2.6" class="dot"/><circle cx="872.8" cy="46.0" r="2.6" class="dot"/><circle cx="880.4" cy="46.0" r="2.6" class="dot"/><circle cx="888.0" cy="46.0" r="2.6" class="dot"/><circle cx="895.6" cy="46.0" r="2.6" class="dot"/><circle cx="903.2" cy="46.0" r="2.6" class="dot"/><circle cx="698.0" cy="53.6" r="2.6" class="dot"/><circle cx="705.6" cy="53.6" r="2.6" class="dot"/><circle cx="713.2" cy="53.6" r="2.6" class="acc"/><circle cx="720.8" cy="53.6" r="2.6" class="dot"/><circle cx="728.4" cy="53.6" r="2.6" class="dot"/><circle cx="736.0" cy="53.6" r="2.6" class="dot"/><circle cx="743.6" cy="53.6" r="2.6" class="dot"/><circle cx="751.2" cy="53.6" r="2.6" class="dot"/><circle cx="758.8" cy="53.6" r="2.6" class="dot"/><circle cx="766.4" cy="53.6" r="2.6" class="dot"/><circle cx="774.0" cy="53.6" r="2.6" class="dot"/><circle cx="781.6" cy="53.6" r="2.6" class="dot"/><circle cx="789.2" cy="53.6" r="2.6" class="dot"/><circle cx="796.8" cy="53.6" r="2.6" class="dot"/><circle cx="804.4" cy="53.6" r="2.6" class="dot"/><circle cx="812.0" cy="53.6" r="2.6" class="dot"/><circle cx="819.6" cy="53.6" r="2.6" class="dot"/><circle cx="827.2" cy="53.6" r="2.6" class="dot"/><circle cx="834.8" cy="53.6" r="2.6" class="dot"/><circle cx="842.4" cy="53.6" r="2.6" class="acc"/><circle cx="850.0" cy="53.6" r="2.6" class="dot"/><circle cx="857.6" cy="53.6" r="2.6" class="dot"/><circle cx="865.2" cy="53.6" r="2.6" class="dot"/><circle cx="872.8" cy="53.6" r="2.6" class="dot"/><circle cx="880.4" cy="53.6" r="2.6" class="dot"/><circle cx="888.0" cy="53.6" r="2.6" class="dot"/><circle cx="895.6" cy="53.6" r="2.6" class="dot"/><circle cx="903.2" cy="53.6" r="2.6" class="dot"/><circle cx="698.0" cy="61.2" r="2.6" class="dot"/><circle cx="705.6" cy="61.2" r="2.6" class="dot"/><circle cx="713.2" cy="61.2" r="2.6" class="dot"/><circle cx="720.8" cy="61.2" r="2.6" class="dot"/><circle cx="728.4" cy="61.2" r="2.6" class="acc"/><circle cx="736.0" cy="61.2" r="2.6" class="dot"/><circle cx="743.6" cy="61.2" r="2.6" class="dot"/><circle cx="751.2" cy="61.2" r="2.6" class="dot"/><circle cx="758.8" cy="61.2" r="2.6" class="dot"/><circle cx="766.4" cy="61.2" r="2.6" class="dot"/><circle cx="774.0" cy="61.2" r="2.6" class="dot"/><circle cx="781.6" cy="61.2" r="2.6" class="dot"/><circle cx="789.2" cy="61.2" r="2.6" class="dot"/><circle cx="796.8" cy="61.2" r="2.6" class="acc"/><circle cx="804.4" cy="61.2" r="2.6" class="dot"/><circle cx="812.0" cy="61.2" r="2.6" class="dot"/><circle cx="819.6" cy="61.2" r="2.6" class="dot"/><circle cx="827.2" cy="61.2" r="2.6" class="dot"Line truncated

After

Width:  |  Height:  |  Size: 75 KiB

+178
View File
@@ -0,0 +1,178 @@
"""docs/media/make_figures.py - the README's "How does it work?" illustrations, in the Strata app's style: its colors
(serve/web/tokens.css), its icons (serve/web/sprite.svg) and its font (Outfit, embedded, SIL OFL 1.1). Light and dark
follow the reader's system setting.
python docs/media/make_figures.py # writes docs/media/how-it-works.svg and docs/media/guess-and-check.svg
"""
import base64
import random
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
OUT = ROOT / "docs" / "media"
SPRITE = (ROOT / "serve" / "web" / "sprite.svg").read_text(encoding="utf-8")
FONT = base64.b64encode((ROOT / "serve" / "web" / "fonts" / "outfit-latin-wght.woff2").read_bytes()).decode()
# the app's tokens (light, then dark)
CSS = """
@font-face{font-family:"Outfit";src:url(data:font/woff2;base64,%s) format("woff2");font-weight:100 900}
text{font-family:"Outfit",system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;fill:#221e1f}
.bg{fill:#f9fafb}.card{fill:#ffffff;stroke:#dee5ee;stroke-width:1}.soft{fill:#f7f7f7}
.muted{fill:#6f6f6f}.ink{fill:#221e1f}.icon{color:#2F384C}
.acc{fill:#10b981}.acc-t{fill:#0c8a60}.acc-tint{fill:#ecfdf5}.acc-line{stroke:#10b981}
.info{fill:#0f97ff}.info-t{fill:#0668c2}.info-tint{fill:#e8f4ff}
.warn{fill:#e39b0b}.warn-t{fill:#8a5a00}.warn-tint{fill:#fff6e0}
.dan{fill:#e0283f}.dan-tint{fill:#fdecee}.dan-t{fill:#b01a2e}
.dot{fill:#dee5ee}.line{stroke:#dee5ee}.arrow{stroke:#6f6f6f;fill:none}.arrowhead{fill:#6f6f6f}
.dash{stroke:#6f6f6f;fill:none;stroke-dasharray:4 4}
.on-acc{fill:#ffffff}.icon-acc{color:#0c8a60}.icon-dan{color:#b01a2e}
@media (prefers-color-scheme:dark){
text,.ink{fill:#eef1f3}.bg{fill:#0e1113}.card{fill:#161a1d;stroke:#2a3036}.soft{fill:#1d2226}
.muted{fill:#8e979f}.icon{color:#c7cdd3}
.acc{fill:#34d399}.acc-t{fill:#4ade80}.acc-tint{fill:rgba(52,211,153,.12)}.acc-line{stroke:#34d399}
.info{fill:#3aa9ff}.info-t{fill:#7cc4ff}.info-tint{fill:rgba(58,169,255,.12)}
.warn{fill:#f5b53d}.warn-t{fill:#f8c96a}.warn-tint{fill:rgba(245,181,61,.12)}
.dan{fill:#ff5a6e}.dan-tint{fill:rgba(255,90,110,.12)}.dan-t{fill:#ff8a98}
.dot{fill:#2a3036}.line{stroke:#2a3036}.arrow{stroke:#8e979f}.arrowhead{fill:#8e979f}.dash{stroke:#8e979f}
.on-acc{fill:#052e20}.icon-acc{color:#4ade80}.icon-dan{color:#ff8a98}
}
""" % FONT
def icon(name, x, y, size=28, cls="icon"):
"""A sprite icon at (x, y), `size` px, drawn in the class's color (the icons use currentColor)."""
m = re.search(r'<symbol id="%s"[^>]*>(.*?)</symbol>' % name, SPRITE, re.S)
return (f'<g class="{cls}" transform="translate({x},{y}) scale({size / 24:.4f})">{m.group(1)}</g>')
def text(x, y, s, size=14, weight=400, cls="ink", anchor="start"):
return (f'<text x="{x}" y="{y}" font-size="{size}" font-weight="{weight}" class="{cls}" '
f'text-anchor="{anchor}">{s}</text>')
def rect(x, y, w, h, r=14, cls="card"):
return f'<rect x="{x}" y="{y}" width="{w}" height="{h}" rx="{r}" class="{cls}"/>'
def svg(w, h, body, title):
return (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}" width="{w}" height="{h}" role="img" '
f'aria-label="{title}"><title>{title}</title><style>{CSS}</style>'
f'{rect(0, 0, w, h, 20, "bg")}{body}</svg>')
def dots(x, y, cols, rows, step, lit, lit_cls="acc", r=2.6, seed=1):
"""A grid of expert dots; `lit` of them (spread at random) in the accent color."""
rnd = random.Random(seed)
n = cols * rows
on = set(rnd.sample(range(n), min(lit, n)))
out = []
for i in range(n):
cx, cy = x + (i % cols) * step, y + (i // cols) * step
out.append(f'<circle cx="{cx:.1f}" cy="{cy:.1f}" r="{r}" class="{lit_cls if i in on else "dot"}"/>')
return "".join(out)
def arrow(x1, y1, x2, y2, cls="arrow"):
return (f'<line x1="{x1}" y1="{y1}" x2="{x2 - 7}" y2="{y2}" class="{cls}" stroke-width="1.6" stroke-linecap="round"/>'
f'<path d="M{x2} {y2} l-8 -5 v10 z" class="arrowhead"/>')
def how_it_works():
W, H = 960, 470
b = []
# the headline strip: one word, 10 experts
b.append(rect(24, 24, W - 48, 92))
b.append(icon("i-chat", 48, 50, 36))
b.append(text(100, 62, "The model is made of 24,576 small specialists - “experts”.", 19, 600))
b.append(text(100, 88, "For each word it writes, it asks only 10 of them. Strata keeps the busy ones",
15, 400, "muted"))
b.append(text(100, 106, "on your graphics card and the rest in RAM - so it runs on a normal PC.", 15, 400, "muted"))
b.append(dots(W - 262, 46, 28, 4, 7.6, 10, seed=3))
b.append(text(W - 262, 94, "10 at work for one word", 12, 500, "acc-t"))
# three cards
cw, ch, cy = 288, 316, 136
xs = [24, 24 + cw + 24, 24 + 2 * (cw + 24)]
cards = [
("i-gpu", "Graphics card", "Small but very fast: 12-24 GB", "acc",
["Runs the core of the model", "for every word, and keeps the", "~4,000 experts used most"],
"the most-used experts", 5, 26, 26 * 5, "acc"),
("i-memory", "RAM + processor", "Big: 32-64 GB of memory", "info",
["Holds all 24,576 experts. When a", "word needs one the card doesn't", "have, the CPU does it - at once"],
"all 24,576 experts", 10, 26, 18, "info"),
("i-disk", "SSD", "Holds a 29 GB lookup table", "warn",
["The model looks up a few", "small rows of it per word -", "no need to keep it in RAM"],
"", 0, 0, 0, "warn"),
]
for (ic, title, sub, tone, lines, dlabel, rows, cols, lit, lit_cls), x in zip(cards, xs):
b.append(rect(x, cy, cw, ch))
b.append(rect(x + 20, cy + 20, 48, 48, 12, tone + "-tint"))
b.append(icon(ic, x + 30, cy + 30, 28, "icon"))
b.append(text(x + 82, cy + 42, title, 19, 700))
b.append(text(x + 82, cy + 62, sub, 13, 500, tone + "-t"))
for i, ln in enumerate(lines):
b.append(text(x + 20, cy + 104 + i * 21, ln, 15, 400, "ink"))
if rows:
b.append(dots(x + 26, cy + 196, cols, rows, 9.4, lit, lit_cls, seed=len(title)))
b.append(text(x + 20, cy + 196 + rows * 9.4 + 14, dlabel, 12, 500, "muted"))
else: # the SSD: a table of rows, a few being read
for r in range(6):
yy = cy + 190 + r * 17
b.append(rect(x + 20, yy, cw - 40, 11, 4, "warn" if r in (1, 4) else "soft"))
b.append(text(x + 20, cy + 306, "a few rows per word", 12, 500, "muted"))
return svg(W, H, "".join(b), "How Strata splits the model between your graphics card, RAM and SSD")
def guess_and_check():
W, H = 960, 330
b = [rect(24, 24, W - 48, H - 48)]
b.append(icon("i-thinking", 48, 48, 30))
b.append(text(92, 70, "Guess, then check - many words per step", 19, 700))
b.append(text(92, 94, "A small, fast helper inside the model guesses the next words. The big model checks all the "
"guesses in one go", 15, 400, "muted"))
b.append(text(92, 113, "and keeps the ones it agrees with. The big model decides every word - it just arrives 1.6-1.8x sooner.",
15, 400, "muted"))
words = ["def", "reverse", "(items", "):"]
fixed = ", key):" # the model's own word where the guess was wrong
ok = [True, True, True, False]
# row 1: the helper's guesses (dashed chips)
y1, y2 = 156, 238
b.append(text(48, y1 + 24, "Helper guesses", 14, 600, "muted"))
b.append(text(48, y2 + 24, "Big model checks", 14, 600, "muted"))
x = 176
xs = []
for i, w in enumerate(words[:4]):
wd = 34 + len(w) * 9
xs.append((x, wd))
b.append(f'<rect x="{x}" y="{y1}" width="{wd}" height="38" rx="10" class="dash" stroke-width="1.4"/>')
b.append(text(x + wd / 2, y1 + 25, w, 16, 500, "ink", "middle"))
x += wd + 14
# row 2: the verdicts
for i, ((x, wd), good) in enumerate(zip(xs, ok)):
tone = "acc" if good else "dan"
b.append(rect(x, y2, wd, 38, 10, tone + "-tint"))
b.append(text(x + wd / 2 - 8, y2 + 25, words[i], 16, 600, tone + "-t", "middle"))
b.append(icon("i-check" if good else "i-error", x + wd - 22, y2 + 11, 16, "icon-" + tone))
if not good: # the model's own word beside the rejected guess
fx = x + wd + 10
fw = 26 + len(fixed) * 9
b.append(rect(fx, y2, fw, 38, 10, "acc-tint"))
b.append(text(fx + fw / 2, y2 + 25, fixed, 16, 600, "acc-t", "middle"))
xs[i] = (fx, fw)
b.append(f'<line x1="{x + wd / 2}" y1="{y1 + 40}" x2="{x + wd / 2}" y2="{y2 - 4}" class="line" stroke-width="1.4"/>')
# the result
rx = xs[-1][0] + xs[-1][1] + 40
b.append(arrow(rx, y2 + 19, rx + 30, y2 + 19))
b.append(rect(rx + 42, y2 - 6, W - 48 - (rx + 42), 50, 12, "acc"))
b.append(icon("i-bolt", rx + 54, y2 + 6, 26, "on-acc-icon"))
b.append(text(rx + 90, y2 + 16, "4 words in one step", 15, 700, "on-acc"))
b.append(text(rx + 90, y2 + 34, "3 guesses kept + its own word", 12, 500, "on-acc"))
return svg(W, H, "".join(b).replace('class="on-acc-icon"', 'class="icon" style="color:#ffffff"'),
"Speculative decoding: a small helper guesses words, the big model checks them at once")
if __name__ == "__main__":
(OUT / "how-it-works.svg").write_text(how_it_works(), encoding="utf-8")
(OUT / "guess-and-check.svg").write_text(guess_and_check(), encoding="utf-8")
for f in ("how-it-works.svg", "guess-and-check.svg"):
print(f, (OUT / f).stat().st_size, "bytes")
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 KiB

+4 -3
View File
@@ -454,8 +454,9 @@ private:
// ---- architecture guard (P1.S2). The engine is specialised to ONE model; anything else must be
// refused with a precise error rather than silently mis-run.
struct Qwen4ExpGuard {
uint32_t block_count = 48, hidden = 2560, experts = 512, experts_used = 10, head_count = 24,
head_count_kv = 2;
uint32_t block_count = 48, hidden = 2560, experts = 0, experts_used = 0, head_count = 24,
head_count_kv = 2; // 0 = presence-only: pruned variants (GSQ-RCO Coder) legitimately ship
// fewer experts than the canonical 512; the graph reads the true value
};
inline std::string check_architecture(const GgufFile& g, const Qwen4ExpGuard& want = {}) {
@@ -477,7 +478,7 @@ inline std::string check_architecture(const GgufFile& g, const Qwen4ExpGuard& wa
for (const auto& r : reqs) {
const MetaValue* v = g.get(r.key);
if (!v) return std::string("missing ") + r.key;
if (v->u != r.want)
if (r.want && v->u != r.want)
return std::string(r.key) + " = " + std::to_string(v->u) + ", expected " + std::to_string(r.want);
}
return {}; // empty == ok
+6 -1
View File
@@ -31,6 +31,8 @@
namespace strata::core {
class RemoteExperts;
/// Where one routed expert's bytes come from.
///
/// Phase 2 has NO cache (`phase-2-correct-engine.md`: hit rate `h = 0`), so the only implementation is a
@@ -92,6 +94,8 @@ struct GpuPlanSink {
struct ExpertDispatch {
strata::kernels::cpu::ExpertPool* pool = nullptr;
ExpertSource* src = nullptr;
RemoteExperts* remote[3] = {}; ///< optional CUDA1..3 tiers for otherwise CPU-served rows
int remote_count = 0;
int64_t n_expert = strata::kernels::cpu::NE;
/// Counters, for the driver to report rather than for control flow.
@@ -320,7 +324,8 @@ public:
/// Allocates and loads `<pack_dir>/experts.bin`. Prints nothing; the caller reports `note()` and the load
/// rate, because those are the two numbers that say whether the arena is the one that was asked for.
bool open(const std::string& pack_dir, int64_t n_layers, int64_t n_expert, int threads, std::string& err);
bool open(const std::string& pack_dir, int64_t n_layers, int64_t n_expert, int threads, std::string& err,
uint64_t max_pinned_bytes = 0);
/// Plan v0.3 P6: a native pack without experts.bin takes its experts from the model's shard 1.
void set_gguf(const std::string& shard1) { gguf_ = shard1; }
void close();
+40 -3
View File
@@ -36,6 +36,7 @@
#include "strata/core/weights.hpp"
#include "strata/kernels/gr.hpp"
#include "strata/kernels/kv_stream.hpp"
#include "strata/kernels/ngram.hpp"
#include "strata/kernels/ple.hpp"
@@ -206,10 +207,24 @@ struct QsaState {
int8_t* v_q = nullptr;
uint16_t* k_scale = nullptr;
uint16_t* v_scale = nullptr;
int32_t* page_table = nullptr; ///< (n_pages,) logical page -> physical page
/// PR #21: Q4_0 KV with Walsh-Hadamard rotation (qsa_set_kv_q4, kv_q4.hpp): 144 B per cell and head
bool kv_q4 = false;
uint8_t* k_q4 = nullptr;
uint8_t* v_q4 = nullptr;
int32_t* page_table = nullptr; ///< (n_pages,) logical page -> physical page (-1: not resident, streamed)
int64_t n_pages = 0;
int64_t max_cells = 0;
/// KV STREAMING (docs/kv-streaming-design.md, `kv_stream.hpp`). `kv_mode` 0: every page in VRAM, identity
/// table (n_slots == n_pages, no host copy). 1: streamed - the authoritative K/V in `host`, `n_slots` pages
/// in VRAM, `map` resolves the selection's blocks on device. 2: a ring (the MTP drafter's window) - `n_slots`
/// pages at `block % n_slots`, with the host copy for a resume. The pool pointers above point at the slots.
int kv_mode = 0;
int64_t n_slots = 0;
strata::kernels::KvHostPools host;
strata::kernels::KvStreamMap map;
int64_t idx_pooled_rows = 0; ///< rows of `idx_pooled` (a ring, which has no indexer, keeps 2)
float* idx_tail = nullptr; ///< (idx_block - 1, idx_dim): the raw tail of the block being filled
float* idx_dead = nullptr; ///< (idx_dim,): the spare slot's key, CONSTANT for the sequence
float* idx_pooled = nullptr; ///< (max_cells/idx_block + 2, idx_dim)
@@ -242,13 +257,35 @@ struct QsaState {
/// Plan v0.3 P7: the RoPE cos/sin table (max_cells x n_rot/2 x 2 floats, 64 MiB at 262K) is identical in every
/// QSA layer. `with_rope = false` sizes a state that borrows it; `share_rope` points `st` at another state's table
/// instead of building a copy (the session builds it once, in the first QSA layer).
uint64_t qsa_state_bytes(const ModelGeometry& g, int64_t max_cells, bool with_rope = true);
uint64_t qsa_state_bytes(const ModelGeometry& g, int64_t max_cells, bool with_rope = true, int64_t ring_cells = 0);
/// KV streaming: keep `cells` cells of each QSA layer in VRAM and the rest in pinned host memory (0: all in VRAM,
/// the default). Set before sizing and initializing the session; a context that fits in `cells` is not streamed.
/// Also puts the MTP drafter's K/V in a ring of its window (`ring_cells` of qsa_state_bytes/init; -1 forces a fully
/// resident state).
void qsa_set_kv_resident(int64_t cells);
int64_t qsa_kv_resident();
/// The fewest resident cells a streamed layer may have: one verify window's selections (8 queries x 2,051 cells
/// in whole blocks) must fit at once, with room to spare.
int64_t qsa_kv_resident_min();
/// Pinned host bytes the streamed states hold (their host copies).
uint64_t qsa_kv_host_bytes();
/// Plan v0.3 P7: store K/V as INT8 with FP16 scales per 64 values (half the VRAM of FP16). Set before sizing and
/// initializing the session; default off until gate G-C accepts it.
void qsa_set_kv_int8(bool enabled);
bool qsa_kv_int8();
/// PR #21: store K/V as Q4_0 after a Hadamard rotation (`--kv q4_0`): 576 B per cell, vs 1,056 in INT8.
void qsa_set_kv_q4(bool enabled);
bool qsa_kv_q4();
/// The state's KV format for the block-moving functions of kv_stream.hpp (kKvF16 / kKvInt8 / kKvQ4).
inline int qsa_kv_format(const QsaState& st) {
return st.kv_q4 ? strata::kernels::kKvQ4 : st.kv_int8 ? strata::kernels::kKvInt8 : strata::kernels::kKvF16;
}
uint64_t qsa_state_init(const ModelGeometry& g, int64_t max_cells, void* base, QsaState& st,
const QsaState* share_rope = nullptr);
const QsaState* share_rope = nullptr, int64_t ring_cells = 0);
/// KV streaming: the pools a reader sees (the VRAM slots) and, when streamed, make the selection's blocks resident.
strata::kernels::QsaAttnPools qsa_attn_pools(const QsaState& st);
void qsa_kv_resolve(const QsaState& st, const ModelGeometry& g, const int32_t* ids, const int32_t* steps, int64_t n_q,
int64_t cap, void* stream);
/// Zeroes the pools AND the indexer, so a fresh sequence matches the reference's own `zeros()`. The KV pool
/// matters even for cells that are never attended, because `kv_gather` reads whatever the selection names.
void qsa_state_zero(const QsaState& st, const ModelGeometry& g, void* stream);
+9
View File
@@ -45,7 +45,14 @@ public:
int64_t window = 32768);
/// The prompt's length: prefill() skips the cells the attention window can never reach again.
void set_prompt_len(int64_t n) { prompt_len_ = n; }
/// At most this many drafts per round (below max_t - 1): a window longer than the MTP's comes from elsewhere.
void set_max_drafts(int k) { max_drafts_ = k; }
uint64_t vram_bytes() const { return vram_; }
/// The draft layer's K/V state (read-only: --serve's STRATA_STATE_HASH check hashes it)
const QsaState& kv_state() const { return st_; }
/// KV streaming: refill the ring of the drafter's window from its host copy for a sequence that continues at
/// `upto` (a conversation-cache resume). No-op unless the drafter's K/V is a ring.
void kv_restore(int64_t upto);
/// The main model's embedding and head, and the verify window's final residuals (T rows, hc*n_embd each).
bool bind(const WeightTable& wt, const NativeHead* head, const float* window_R, std::string& err);
@@ -82,6 +89,8 @@ private:
const NativeHead* head_ = nullptr;
const float* window_R_ = nullptr;
int max_t_ = 0;
int device_ = -1; ///< the device `load` ran on: the public calls switch to it (layer split)
int max_drafts_ = 1 << 30;
int64_t n_vocab_ = 0;
uint64_t vram_ = 0;
cudaStream_t cs_ = nullptr;
+22
View File
@@ -0,0 +1,22 @@
#pragma once
// Multi-GPU: make `device` current for a scope and restore the caller's device after it. An object that owns CUDA
// streams, graphs and buffers on one device (a verify stage, the drafter) wraps its public calls in this, so the
// caller's thread may be on any device. A negative device, or the device already current, does nothing.
#include <cuda_runtime.h>
namespace strata::core {
struct OnDevice {
int previous = -1;
explicit OnDevice(int device) {
int cur = 0;
if (device >= 0 && cudaGetDevice(&cur) == cudaSuccess && cur != device && cudaSetDevice(device) == cudaSuccess)
previous = cur;
}
~OnDevice() { if (previous >= 0) cudaSetDevice(previous); }
OnDevice(const OnDevice&) = delete;
OnDevice& operator=(const OnDevice&) = delete;
};
} // namespace strata::core
+2 -1
View File
@@ -39,7 +39,8 @@ struct PinnedArena {
explicit PinnedArena(uint64_t bytes, uint64_t slice = 0);
/// Plan v0.3 P6: slices of different sizes (one per layer of a native pack), given as their start offsets
/// followed by the end of the last one. `slice_starts` holds the registered ones.
PinnedArena(uint64_t bytes, const std::vector<uint64_t>& bounds);
/// `max_pinned_bytes`: optional cap on CUDA registration. 0 preserves the normal unrestricted path.
PinnedArena(uint64_t bytes, const std::vector<uint64_t>& bounds, uint64_t max_pinned_bytes = 0);
std::vector<uint64_t> slice_starts;
~PinnedArena();
PinnedArena(const PinnedArena&) = delete;
+51
View File
@@ -0,0 +1,51 @@
// include/strata/core/progress.hpp - issue #29: whether a request is still moving, and where it is.
//
// The engine's host loop and the GPU wait on each other through flags; a protocol bug there does not crash, it
// spins forever (the GPU "100%", one CPU core busy, no tokens). `--serve` runs a watchdog thread over this: a
// request whose heartbeat stops for `STRATA_WATCHDOG_S` seconds (default 60; 0 = off) ends the engine with the
// stage it was stuck in, and the server starts it again instead of hanging. Tokens, prompt chunks and verify
// windows beat; the stage is two relaxed stores per layer, which the token path does not notice.
#pragma once
#include <atomic>
#include <chrono>
#include <cstdint>
#include <cstdio>
namespace strata::core {
struct Progress {
std::atomic<uint64_t> beats{0};
std::atomic<bool> busy{false};
std::atomic<const char*> where{"idle"};
std::atomic<int64_t> detail{-1};
std::atomic<int64_t> since_ms{0}; ///< when `where` was set (steady clock): how long a stage has lasted
std::atomic<uint64_t> ticks{0}; ///< layers served: a window that still moves, slowly, against one that stopped
};
inline int64_t progress_now_ms() {
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch())
.count();
}
/// What the watchdog prints about a part of the engine when it fires (the expert pool, the verify window).
using DiagFn = void (*)(std::FILE*);
inline std::atomic<DiagFn>& diag_pool_fn() { static std::atomic<DiagFn> f{nullptr}; return f; }
inline std::atomic<DiagFn>& diag_verify_fn() { static std::atomic<DiagFn> f{nullptr}; return f; }
inline Progress& progress() {
static Progress p;
return p;
}
inline void progress_at(const char* where, int64_t detail = -1) {
Progress& p = progress();
p.where.store(where, std::memory_order_relaxed);
p.detail.store(detail, std::memory_order_relaxed);
p.since_ms.store(progress_now_ms(), std::memory_order_relaxed);
}
inline void progress_beat() { progress().beats.fetch_add(1, std::memory_order_relaxed); }
inline void progress_tick() { progress().ticks.fetch_add(1, std::memory_order_relaxed); }
} // namespace strata::core
+85
View File
@@ -0,0 +1,85 @@
#pragma once
#include "strata/core/expert_cache.hpp"
#include "strata/core/expert_source.hpp"
#include <cuda_runtime.h>
#include <cstdint>
#include <string>
#include <utility>
#include <vector>
namespace strata::core {
/// A static, profile-filled expert tier on another CUDA device. CUDA0 keeps all
/// dense weights and state; results return through the existing pinned CPU rows.
class RemoteExperts {
public:
RemoteExperts() = default;
~RemoteExperts();
RemoteExperts(const RemoteExperts&) = delete;
RemoteExperts& operator=(const RemoteExperts&) = delete;
/// Initialise the device before the host expert arena registers
/// tens of GiB of portable mapped memory with CUDA.
static bool preflight(int device, double& free_gib, std::string& err);
bool open(int device, int slots, int64_t layers, int64_t experts,
const std::vector<std::pair<int32_t, int32_t>>& ranked,
const ExpertCache& primary, ExpertSource& source,
std::vector<uint8_t>& claimed, std::string& err);
void close();
/// `kind` is the primary verifier's classification (-1 = CPU candidate),
/// or null on the one-token path. Entries already served on CUDA0 are excluded.
bool begin(int64_t layer, const float* x, const int32_t* ids, int64_t n_tok,
int64_t k, const int32_t* kind, const int32_t* primary_res,
std::string& err);
bool owns(int64_t index) const { return owned_[(size_t) index] != 0; }
bool finish(float* out, std::string& err);
int64_t resident() const { return cache_.resident(); }
int64_t computed() const { return computed_; }
int64_t launched_layers() const { return launched_layers_; }
double gib() const { return cache_.gib(); }
uint64_t returned_bytes() const { return returned_bytes_; }
uint64_t full_row_bytes() const { return full_row_bytes_; }
/// host time spent in begin() (staging + launches) and in finish() (waiting for this GPU), cumulative
double ms_begin() const { return ms_begin_; }
double ms_wait() const { return ms_wait_; }
private:
int device_ = -1;
int64_t n_expert_ = 0;
int32_t groups_ = 0;
int64_t computed_ = 0;
int64_t launched_layers_ = 0;
uint64_t returned_bytes_ = 0;
uint64_t full_row_bytes_ = 0;
double ms_begin_ = 0, ms_wait_ = 0;
ExpertCache cache_;
cudaStream_t stream_ = nullptr;
float* h_x_ = nullptr;
float* h_out_ = nullptr;
void* h_meta_ = nullptr;
float* d_x_ = nullptr;
float* z_x_ = nullptr; ///< h_x_ as the helper GPU sees it (zero-copy: no input copy per layer)
float* z_out_ = nullptr; ///< h_out_ as the helper GPU sees it (zero-copy: no result copy)
bool zero_copy_ = false;
float* d_out_ = nullptr;
uint8_t* d_q8_ = nullptr;
float* d_scales_ = nullptr;
void* d_scratch_ = nullptr;
void* d_meta_ = nullptr; ///< one contiguous upload of grouped indices, instead of five small copies
int32_t* d_start_ = nullptr;
int32_t* d_dst_ = nullptr;
int32_t* d_tok_ = nullptr;
int32_t* d_count_ = nullptr;
unsigned long long* d_ptr_ = nullptr;
std::vector<uint8_t> owned_;
std::vector<uint8_t> layers_present_;
std::vector<int32_t> group_of_, group_id_;
std::vector<int32_t> start_, dst_, tok_, original_row_;
std::vector<unsigned long long> ptr_;
};
} // namespace strata::core
+62 -1
View File
@@ -21,9 +21,12 @@
// selection, native indexer) and a profile-filled VRAM expert tier with its residency table on the device.
#pragma once
#include <cstdio>
#include "strata/core/expert_source.hpp"
#include "strata/core/layer.hpp"
#include "strata/core/session.hpp"
#include "strata/kernels/sampler.hpp"
#include <cuda_runtime.h>
@@ -51,6 +54,9 @@ public:
Verifier(const Verifier&) = delete;
Verifier& operator=(const Verifier&) = delete;
/// The watchdog's view of the window in flight (issue #31): the layer, the GPU's sequence, the flags.
void diag(std::FILE* f) const;
/// `max_t` <= kVerifyMaxT. `head` may be null (the canonical head is then run per token).
bool init(const WeightTable& wt, const ModelGeometry& g, SessionState& ss, const VerifyHits& hits,
const NativeHead* head, int max_t, std::string& err);
@@ -58,13 +64,52 @@ public:
/// One window: `tokens[0..T)` at positions pos0.., the pool served per layer; `out[t]` = argmax after token t.
/// The PLE rows are gathered here from `ss.ple_prev` and the tokens. Captures the T-token graph on first use.
bool run(int T, const int32_t* tokens, int64_t pos0, PoolMultiFn pool, void* user, int32_t* out, std::string& err);
/// The sampling the verify window's head applies (temperature / top_p / top_k / seed). Set per
/// request; greedy by default. The sampling itself runs OUTSIDE the captured graph - its
/// parameters would otherwise be baked forever - so this can change between requests freely.
void set_sampling(const strata::kernels::SamplerParams& sp) {
sampling_ = sp; // row t of a window at pos0 draws Philox(seed, pos0 + t): see run()
if (next_) next_->set_sampling(sp);
}
/// The penalty histories for `sampling_.penalty_last_n`: ONE ROW PER WINDOW ROW, T rows of `history_len`
/// int32 slots at that stride (`strata::kernels::penalty_rows` builds them), most recent token LAST, unused
/// front slots -1 (the kernel reads only the tail window). Row t follows the window's drafts 1..t - staging
/// row 0 alone (before 0.1.19) left the drafted rows with unwritten histories. Null disables the penalties
/// entirely - the neutral run's sampling call is byte-for-byte what it was. The engine re-uploads the rows
/// before every window; the buffer must hold kVerifyMaxT rows and stay alive across the request.
void set_history(const int32_t* history, int history_len) {
hist_d_ = history;
hist_len_ = history_len;
if (next_) next_->set_history(history, history_len);
}
/// Off: `run` skips the request's head sampling and `out` is the recorded greedy pick. For windows whose
/// picks are discarded - a prompt read through windows commits every token - so they cost no sampler launch
/// or sync and never read a history staged for another position.
void set_head_sampling(bool on) { head_sampling_ = on; if (next_) next_->set_head_sampling(on); }
/// LAYER SPLIT (multi-GPU): this verifier runs layers [layer_begin, layer_end) of every window. A stage that
/// does not start at layer 0 takes its residual from `handoff_in` instead of embedding the tokens; a stage that
/// does not end at the last layer writes its residual to `handoff_out` and has no head. The hand-off holds,
/// per token, the residual R (hc x n_embd), the last layer's pending write bo (n_embd) and inject (hc): the next
/// stage folds that write into its first read exactly as the unsplit window does, so the split is bit-exact.
/// Both pointers must be device-visible (mapped pinned memory, portable when the stages are on two devices).
/// Set before `init`. Default: the whole model, no hand-off.
void set_stage(int64_t layer_begin, int64_t layer_end, const float* handoff_in, float* handoff_out) {
lb_ = layer_begin; le_ = layer_end; hand_in_ = handoff_in; hand_out_ = handoff_out;
}
/// The next stage: `run` and `commit` continue into it (its pool calls get `next_user`); sampling settings
/// and `final_R` are the last stage's.
void set_next(Verifier* next, void* next_user) { next_ = next; next_user_ = next_user; }
/// floats per token in a hand-off buffer
static int64_t handoff_floats(const ModelGeometry& g) { return (int64_t) g.hc * g.n_embd + g.n_embd + g.hc; }
/// Keep the first `n_keep` (1..T) tokens of the last window; advances `ss.ple_prev` by them.
bool commit(int n_keep, std::string& err);
/// Token t's residual after the last layer, (hc, n_embd) on the device, valid until the next `run`.
const float* final_R(int t) const;
const float* final_R_all() const { return R_; }
const float* final_R_all() const { return next_ ? next_->final_R_all() : R_; }
/// The GPU plan the pool writes each layer (VRAM hits + the PCIe share of the misses); give it to the
/// dispatch (`ExpertDispatch::plan`) before the first `run`.
@@ -83,6 +128,22 @@ public:
private:
bool capture(int T, std::string& err);
strata::kernels::SamplerParams sampling_ = [] {
strata::kernels::SamplerParams s;
s.greedy = true;
s.temperature = 0.0f;
return s;
}(); ///< greedy by default; per-request via set_sampling
const int32_t* hist_d_ = nullptr; ///< penalty-history row (set_history); null = no penalties apply
int hist_len_ = 0;
bool head_sampling_ = true; ///< set_head_sampling
int device_ = -1; ///< the device `init` ran on: run/commit switch to it (layer split)
int64_t lb_ = 0, le_ = -1; ///< set_stage: the layers this verifier runs (-1: to the last)
const float* hand_in_ = nullptr;
float* hand_out_ = nullptr;
Verifier* next_ = nullptr;
void* next_user_ = nullptr;
bool ple_stage() const { return lb_ <= 1 && 1 < le_; } ///< holds layer 1, where the PLE block runs
bool capture_commit(std::string& err);
bool record_window(int T, cudaStream_t cs, std::string& err);
+26
View File
@@ -0,0 +1,26 @@
// include/strata/kernels/cpu/iq_avx2.hpp - AVX-2 multi-token dot products for the i-quant expert
// formats (IQ2_XXS, IQ2_XS, IQ2_S, IQ3_XXS, IQ3_S) against Q8_K activations (ggml's block_q8_K).
//
// The AVX-512 kernel's scheme on the CPUs without it (AMD Zen 2/3, Intel Core 12th-14th gen): the weights of a
// 32-value chunk are decoded once per verify window and every token applies them with five instructions.
#pragma once
#include <cstddef>
#include <cstdint>
namespace strata::kernels::cpu {
bool iq256_supported(int ggml_type) noexcept;
/// ff[t][r] = silu(gate_r . a[t]) * (up_r . a[t]), rows [r0, r1); gate rows at blob, up rows at blob + up_off.
void iq256_gu_rows(int ggml_type, const uint8_t* blob, size_t gu_row, size_t up_off, int n, const void* const* act,
int nt, float* const* ff, int r0, int r1);
/// out[t][r] = w_r . a[t], rows [r0, r1).
void iq256_rows(int ggml_type, const uint8_t* w, size_t row_bytes, int n, const void* const* act, int nt,
float* const* out, int r0, int r1);
/// out[t][r] = w_r . h[t] for IQ4_NL (type 20) rows against Q8_0 activations (ggml's block_q8_0).
/// IQ4_NL is a 32-value-block format, so this does not go through iq256_rows (QK_K blocks, Q8_K acts).
void iq4nl256_down_rows(const uint8_t* w, size_t row_bytes, int n, const void* const* hq, int nt,
float* const* out, int r0, int r1);
} // namespace strata::kernels::cpu
+54 -5
View File
@@ -14,16 +14,29 @@
// THE COMPLETION PROTOCOL, because this is where a pool usually goes wrong. `run()` waits for `done == n`
// AND for every worker to PARK. Waiting only for `done` is not enough: a worker can still be inside the
// drain loop after its last `done` increment, and the host resetting `head` underneath it would let that
// worker claim a job from the NEXT batch before the next batch has been published. The
// `done`-then-`parked` pair makes the handover unambiguous, and the second wait costs a few hundred cycles
// against a layer that takes milliseconds.
// worker claim a job from the NEXT batch before the next batch has been published.
//
// **AND `parked` IS NOT ENOUGH EITHER (issue #29).** A worker that went to sleep (after `kSpinBeforeSleep`) is
// still counted as parked when it wakes, so for a moment after it has seen a new epoch the host believes it is
// idle. On a card with most experts in VRAM the workers sleep in the middle of a request, tiny batches finish
// before a sleeper is awake, and that moment comes round constantly: a late worker could claim from the NEXT
// batch while the host was still writing it, run a job twice, or add to `done` after the host had reset it -
// and `done != n` then never ended, with the GPU waiting on the pool forever. So every claim carries its
// batch: `head` is one 64-bit word `epoch | njobs | index`, a claim is a CAS that only succeeds for the epoch
// the worker woke for, and a late worker's claim simply fails. A claim that succeeds belongs to the current
// batch, whose description the host cannot change until that job's `done` has landed.
#pragma once
#include "strata/kernels/cpu/expert.hpp"
#include "strata/kernels/cpu/native_expert.hpp"
#include <atomic>
#include <cstdio>
#include <memory>
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <mutex>
#include <thread>
#include <vector>
@@ -89,6 +102,8 @@ public:
/// threads on six cores. `false` is the A/B arm and exists so the change is measurable rather than
/// asserted - the counter it moves is `pool phases ... drain`, which is host-side and needs no profiler.
explicit ExpertPool(int n_workers = 0, bool pin = true, bool host_works = true);
/// The watchdog's view of the pool (issue #31): the batch, the counters, every thread's state.
void diag(std::FILE* f) const;
~ExpertPool();
ExpertPool(const ExpertPool&) = delete;
ExpertPool& operator=(const ExpertPool&) = delete;
@@ -136,10 +151,31 @@ public:
repark = ms_repark_;
}
/// **A PARKED WORKER SPINS FOR THIS LONG, THEN SLEEPS.** The park is a `_mm_pause` spin because a layer's
/// batches are microseconds apart and a wake-up from the OS costs more than that. But a spin that never ends
/// keeps every worker's core at 100% while the engine waits for a request - issue #4, "CPU 50% even when
/// doing nothing" (7 of the 5700X's 16 threads). Between requests the workers block on `sleep_cv_`. NOT only
/// between requests: with most experts in VRAM (a 24 GB card) many layers have no CPU work, so the workers
/// also sleep mid-request - which the claim protocol above must survive (issue #29).
/// `STRATA_POOL_SPIN_US` overrides it (a test knob: a short spin makes the workers sleep constantly).
static constexpr std::chrono::milliseconds kSpinBeforeSleep{20};
/// A pool wait that sees no completion for this long is a bug; the engine stops with a message instead of
/// spinning forever, and the server starts it again (issue #29).
static constexpr std::chrono::seconds kStall{60};
private:
void worker(int id);
void drain(int id, ExpertScratch& scratch);
void drain(int id, ExpertScratch& scratch, uint32_t epoch);
void run_phase(int mode, int n_tasks);
/// Claim the next job of batch `epoch`, or -1 (that batch is exhausted, or it is not the current one).
int claim(uint32_t epoch);
/// Publish the batch whose description the caller has just written: reset `done`, then `head`, then the epoch.
uint32_t begin_batch(int n);
/// The host's waits, bounded by `kStall`.
void wait_parked(const char* what);
void wait_done(int n);
/// Bump `epoch_`, and wake the workers that went to sleep. Every publish goes through here.
void publish();
int n_ = 0;
bool host_works_ = true;
@@ -164,11 +200,24 @@ private:
//
// The counter was diagnostic only - `pauses()` was read in one place, to print a number nothing branched on
// - so it is deleted rather than amortised. `alignas(64)` then stops the remaining four sharing.
alignas(64) std::atomic<uint32_t> head_{0};
// issue #31 diagnostics: each worker's state (kParked, kSleeping, kBetween, or the job it runs) and the host's
// (kIdle, kWaitParked, kWaitDone, or its job), printed by the serve watchdog through `diag`
static constexpr int32_t kParked = -1, kSleeping = -2, kBetween = -3, kIdle = -10, kWaitParked = -11,
kWaitDone = -12;
std::unique_ptr<std::atomic<int32_t>[]> wstate_;
std::atomic<int32_t> hstate_{kIdle};
std::atomic<int64_t> hstate_ms_{0};
alignas(64) std::atomic<uint64_t> head_{0}; // epoch << 32 | njobs << 16 | next index (issue #29)
alignas(64) std::atomic<uint32_t> done_{0};
alignas(64) std::atomic<uint32_t> parked_{0};
alignas(64) std::atomic<uint32_t> epoch_{0};
alignas(64) std::atomic<bool> stop_{false};
// The sleep after `kSpinBeforeSleep`. `sleepers_` is how `publish` knows whether anyone needs waking, so the
// token path pays one uncontended load per publish and never takes the mutex while the workers spin.
alignas(64) std::atomic<uint32_t> sleepers_{0};
std::mutex sleep_mu_;
std::condition_variable sleep_cv_;
std::chrono::microseconds spin_before_sleep_{kSpinBeforeSleep};
std::vector<std::thread> threads_;
std::vector<ExpertScratch> scratch_; // one per worker: no allocation, no false sharing of the hot data
// run_split state: mode 0 = whole experts, 1 = gate/up row parts, 2 = down row parts
+56
View File
@@ -0,0 +1,56 @@
// include/strata/kernels/cvec.hpp - a control vector on the residual stream: the engine's side of llama.cpp's
// `llama_adapter_cvec` with the `--cvec-mode` / `--cvec-dir` extension of the `experimental-speed-projection`
// package (its `02-cvec-projection-mode.patch`, and `01-qwen4exp-cvec-hooks.patch` for WHERE it applies).
//
// After layer l's FFN write - llama.cpp's `build_cvec(res_hc, il)` right after the layer's second
// `build_hc_combine` - every one of the hc residual streams h of every token becomes
//
// project: h <- h - s_l (h . v_l) v_l v_l = d / |d|, s_l = |d|, d = the scaled direction
// add: h <- h + d llama.cpp's stock additive control vector
//
// for l in [first, last]. `d` is the GGUF's `direction.<src>` times the file's scale (summed over files), with
// src = l (per-layer) or one fixed layer (single:L, project mode only), exactly as llama.cpp's loader places it;
// layer 0 has no direction. Off unless a vector is loaded (--control-vector-scaled); a loaded one is switched
// per request through a device flag, so the captured graphs are the same either way. With the flag off the
// result is bit-identical to the engine without a vector: the kernel then only applies the pending residual
// write, with the fused read's own arithmetic.
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace strata::kernels {
struct Cvec {
const float* dir = nullptr; ///< device, n_layers * n_embd: project = the unit v_l, add = d_l; zero rows elsewhere
const float* s = nullptr; ///< device, n_layers: project = s_l, add = 1; 0 = the layer is not steered
const int* on = nullptr; ///< device int: 0 = this request runs the stock model
int mode = 0; ///< 0 = project, 1 = add
int first = 0, last = -1; ///< the steered layers, inclusive
int64_t n_embd = 0, hc = 0;
bool loaded() const { return dir != nullptr; }
/// Layer l's FFN write is followed by the vector (with a real direction there). Decided at load time, never
/// per request: it changes where the residual write happens, so the graphs depend on it.
bool covers(int64_t l) const { return loaded() && l >= first && l <= last && l < (int64_t) steered.size() && steered[(size_t) l]; }
std::vector<bool> steered; ///< host copy of s_l != 0
};
/// The loaded vector (empty until `cvec_upload`).
const Cvec& cvec();
/// Upload a vector built by the loader: `dir` is n_layers * n_embd, `s` n_layers (see `Cvec`). Starts ON.
bool cvec_upload(const std::vector<float>& dir, const std::vector<float>& s, int mode, int first, int last,
int64_t n_embd, int64_t hc, std::string& err);
/// The per-request switch. Synchronizes the device when it changes, so call it between requests.
void cvec_set_enabled(bool on);
bool cvec_enabled();
/// Layer `layer`'s vector on T tokens' residual stacks (`R + t * r_ld`, hc streams of n_embd). With `write`, the
/// pending FFN write `R += bo * 2 sigmoid(inj / hc)` (the fused read's arithmetic) is applied first, for callers
/// whose writes are folded into the next layer's read; without it, R must already hold the layer's output.
void cvec_apply(float* R, int64_t layer, int64_t T, int64_t r_ld, const float* bo, int64_t bo_ld, const float* inj,
int64_t inj_ld, bool write, void* stream);
} // namespace strata::kernels
+61
View File
@@ -0,0 +1,61 @@
// include/strata/kernels/kv_q4.hpp - Q4_0 KV storage with Walsh-Hadamard rotation for the QSA layers (`--kv q4_0`).
//
// From PR #21 (code-martin). K and V are rotated by the orthonormal 256-point Hadamard matrix H before they are
// quantized to ggml's q4_0 (32 values per block: an fp16 scale + 16 bytes of 4-bit codes, 144 B per head per cell,
// 576 B per cell for K and V of both heads, vs 1,056 B in INT8). The rotation spreads a head's outlier channels
// over all 256 dimensions, which is what makes 4 bits usable. The query is rotated the same way, so
// <Hq, Hk> = <q, k> and the scores are unchanged, and the attention output (a mix of rotated values) is rotated
// back with H (self-inverse). What is lossy is the 4-bit rounding itself: measured, not assumed, in
// bench/results/2026-09-27-kv-q4.
//
// With KV streaming (--kv-resident) the codes live in the host copy and the resident slots like INT8's (KvHostPools
// k_q4/v_q4, the same block granule), so both options combine.
#pragma once
#include "strata/kernels/qsa.hpp"
#include <cstdint>
namespace strata::kernels {
inline constexpr int QK4_0 = 32;
#pragma pack(push, 1)
struct block_q4_0 {
uint16_t d; // scale (fp16 bits)
uint8_t qs[QK4_0 / 2]; // 32 4-bit codes: element j in the low nibble of qs[j], j + 16 in the high one
};
#pragma pack(pop)
static_assert(sizeof(block_q4_0) == 18, "block_q4_0 must be 18 bytes");
/// Bytes per cell and KV head: 8 blocks of 32 for head_dim 256 -> 144.
inline uint64_t kv_q4_bytes_per_head(int head_dim) { return (uint64_t) (head_dim / QK4_0) * sizeof(block_q4_0); }
/// Bytes per cell (one token, one layer): K and V of every KV head.
inline uint64_t kv_q4_bytes_per_cell(const QsaShapes& s) {
return (uint64_t) s.n_head_kv * kv_q4_bytes_per_head((int) s.head_dim) * 2;
}
/// Orthonormal Fast Walsh-Hadamard Transform of rows of 256 floats (scale 1/16): its own inverse.
void fwht256_cuda(const float* src, float* dst, int64_t n_rows, void* stream);
inline void fwht256_inplace_cuda(float* data, int64_t n_rows, void* stream) { fwht256_cuda(data, data, n_rows, stream); }
/// Append the (already rotated) cell at step[kStepPos]. With a host copy (KV streaming) it is written there too,
/// and to VRAM only if its block is resident.
void kv_append_q4_step(uint8_t* k_q4, uint8_t* v_q4, const int32_t* page_table, const int32_t* step,
const float* kcur, const float* vcur, const QsaShapes& s, void* stream,
const KvHostPools* host = nullptr);
/// The prompt path: T consecutive (rotated) cells from pos0, K/V [T, n_head_kv, 256]; also into `stage` (identity
/// layout, the one-layer staging pool of a streamed session) when given.
void kv_append_q4(uint8_t* k_q4, uint8_t* v_q4, const int32_t* page_table, int64_t pos0, int64_t T, const float* K,
const float* V, const QsaShapes& s, void* stream, const KvHostPools* host = nullptr,
const KvHostPools* stage = nullptr);
/// Gather step[kStepWidth] cells named by `ids` into FP16 scratch [id][kv_head][head_dim] (still rotated).
void kv_gather_q4_step(const uint8_t* k_q4, const uint8_t* v_q4, const int32_t* page_table, const int32_t* ids,
const int32_t* step, int64_t max_ids, const QsaShapes& s, uint16_t* k_scratch,
uint16_t* v_scratch, void* stream);
} // namespace strata::kernels
+5 -2
View File
@@ -12,6 +12,7 @@
// the FP16 pools: codes `[page][kv_head][page_size][head_dim]`, scales `[page][kv_head][page_size][head_dim/64]`.
#pragma once
#include "strata/kernels/kv_stream.hpp"
#include "strata/kernels/qsa.hpp"
#include <cstdint>
@@ -25,9 +26,11 @@ inline uint64_t kv_q8_bytes_per_cell(const QsaShapes& s) {
return (uint64_t) s.n_head_kv * s.head_dim * 2 + (uint64_t) s.n_head_kv * (s.head_dim / KV_Q8_GROUP) * 2 * 2;
}
/// Append the cell at step[kStepPos] (graph-capturable: position and page come from device memory).
/// Append the cell at step[kStepPos] (graph-capturable: position and page come from device memory). With a host
/// copy (KV streaming, `kv_stream.hpp`) the cell is written there too, and to VRAM only if its block is resident.
void kv_append_q8_step(int8_t* k_q, int8_t* v_q, uint16_t* k_scale, uint16_t* v_scale, const int32_t* page_table,
const int32_t* step, const float* kcur, const float* vcur, const QsaShapes& s, void* stream);
const int32_t* step, const float* kcur, const float* vcur, const QsaShapes& s, void* stream,
const KvHostPools* host = nullptr);
/// Gather step[kStepWidth] cells named by `ids` into FP16 scratch `[id][kv_head][head_dim]`; the grid is sized by
/// `max_ids` (capacity), the kernel reads the real count from `step`.
+98
View File
@@ -0,0 +1,98 @@
// include/strata/kernels/kv_stream.hpp - KV streaming for the QSA layers (docs/kv-streaming-design.md).
//
// A streamed QSA layer keeps its AUTHORITATIVE K/V in pinned, device-mapped host memory, laid out exactly as a
// fully resident pool with the identity page table would be. VRAM holds a pool of `n_slots` pages (one page =
// `page_size` = 4 cells = one indexer block, all KV heads, K and V: 4,224 B in int8), and the page table becomes a
// RESIDENCY MAP: `page_table[block]` is the slot holding that block, or -1.
//
// Every reader (`qsa_decode_attn_*`, `kv_gather_*`) is unchanged: it goes through the page table as before. What
// changes is that after the selection and before the attention, `kv_stream_resolve` makes every block the
// selection names resident - on device, inside the captured graphs:
// 1. one block of 1,024 threads walks the selected ids: a resident block gets this call's epoch and its clock
// reference bit, a missing block is claimed once (page_table -1 -> -2) into a miss list;
// 2. the same block picks one victim slot per miss with a CLOCK sweep (second chance) that never takes a slot
// this call uses, re-points the page table (old block -> -1, new block -> slot);
// 3. a copy kernel reads the missed blocks from the host copy (zero-copy, over PCIe) into their slots.
// Writers (`kv_append_*`) write the host copy always, and the slot only if the block is resident, so the slots
// never go stale and nothing needs invalidating - not after a rejected draft, not on a conversation-cache resume.
//
// A RING (the MTP drafter, which only ever reads its last `window` cells) uses the same host copy with a static
// table `block -> block % n_slots` and no resolve: the ring is refilled from the host copy on a resume.
#pragma once
#include "strata/kernels/qsa.hpp"
#include "strata/kernels/qsa_decode_attn.hpp"
#include <cstdint>
namespace strata::kernels {
/// The host copy of one layer's K/V: device-mapped pointers (UVA) into pinned memory, identity layout
/// `[block][kv_head][page_size][head_dim]`. All null when the layer is fully resident.
struct KvHostPools {
uint16_t* k_pool = nullptr; ///< fp16 mode
uint16_t* v_pool = nullptr;
int8_t* k_q = nullptr; ///< int8 mode: codes
int8_t* v_q = nullptr;
uint16_t* k_scale = nullptr; ///< int8 mode: fp16 scale per 64 values
uint16_t* v_scale = nullptr;
uint8_t* k_q4 = nullptr; ///< q4_0 mode (kv_q4.hpp): block_q4_0 codes, 144 B per cell and head
uint8_t* v_q4 = nullptr;
bool present() const { return k_pool != nullptr || k_q != nullptr || k_q4 != nullptr; }
};
/// The KV storage format, for the functions below that move whole blocks (`fmt`): fp16, int8 (+ scales), q4_0.
/// (A bool `int8` argument still reads as kKvF16 / kKvInt8.)
enum KvFormat : int { kKvF16 = 0, kKvInt8 = 1, kKvQ4 = 2 };
/// The residency map of a streamed layer, all device memory at fixed addresses (the graphs bake them in).
struct KvStreamMap {
int32_t* page_table = nullptr; ///< (n_blocks,) block -> slot, or -1
int32_t* slot_block = nullptr; ///< (n_slots,) slot -> block, or -1
int32_t* slot_stamp = nullptr; ///< (n_slots,) the resolve epoch that last used the slot
int32_t* slot_ref = nullptr; ///< (n_slots,) clock reference bit
int32_t* ctl = nullptr; ///< kKvCtlInts: epoch, hand, misses of the last call, overflow, u64 counters
int32_t* miss_block = nullptr; ///< (n_slots,)
int32_t* miss_slot = nullptr; ///< (n_slots,)
int64_t n_blocks = 0;
int64_t n_slots = 0;
};
inline constexpr int kKvCtlInts = 16; ///< [0] epoch [1] hand [2] misses [3] overflow; u64 at [4] misses, [6] lookups, [8] calls
/// Bytes of the map's arrays besides the page table (which the state already has): 5 per-slot ints + ctl.
inline uint64_t kv_stream_map_bytes(int64_t n_slots) { return (uint64_t) n_slots * 4 * 5 + kKvCtlInts * 4; }
/// Every block evicted: page table all -1, slots free, counters zero.
void kv_stream_reset(const KvStreamMap& m, void* stream);
/// Make every block named by the selections of `n_q` queries resident (ids [n_q][cap], width from
/// steps[q * kStepCount + kStepWidth]). Capturable. `n_slots` must hold the distinct blocks of one call
/// (n_q x (cap / page_size + 2)); a call that cannot sets ctl[3] (see `kv_stream_counters`).
void kv_stream_resolve(const KvStreamMap& m, const QsaAttnPools& slots, const KvHostPools& host, int fmt,
const int32_t* ids, const int32_t* steps, int64_t n_q, int64_t cap, const QsaShapes& s,
void* stream);
/// The static ring table `block -> block % n_slots`.
void kv_ring_table(int32_t* page_table, int64_t n_blocks, int64_t n_slots, void* stream);
/// Copy blocks [b0, b1) of the host copy into the pool pages `page_table` names (host-side table `phys(b)`
/// computed as `b % n_slots`: the ring restore). Not capturable.
void kv_ring_restore(const QsaAttnPools& slots, const KvHostPools& host, int fmt, int64_t b0, int64_t b1,
int64_t n_slots, const QsaShapes& s, void* stream);
/// Copy the first `n_blocks` blocks of the host copy into a fully resident (identity-layout) pool: the prompt
/// path's staging of one layer. Not capturable (DMA).
void kv_stage_from_host(const QsaAttnPools& stage, const KvHostPools& host, int fmt, int64_t n_blocks,
const QsaShapes& s, void* stream);
struct KvStreamCounters {
uint64_t misses = 0, lookups = 0, calls = 0;
bool overflow = false;
};
/// Synchronous read of the counters (debug and the end-of-request summary).
KvStreamCounters kv_stream_counters(const KvStreamMap& m);
/// Bytes of one block (page) of K and V together.
uint64_t kv_block_bytes(const QsaShapes& s, int fmt);
} // namespace strata::kernels
@@ -27,4 +27,12 @@ void native_ple_postops(const float* projected_key, const float* hidden,
const PleWeights& weights, const NativePlePostopsBuffers& buffers,
void* stream);
/// The prompt path: T tokens at once, element for element the arithmetic of T calls of native_ple_postops with
/// the history advanced after each. `key` (T x 10240) holds the projected keys and is normalized in place;
/// `hidden` (T x 10240) becomes the result; `value` T x 2560; `query_norm` (T x 10240, the normalized hidden, then
/// the normalized gated values), `gated` (T x 10240) and `gate` (T x 4) are scratch; `history` (9 rows per
/// channel) is advanced past the T tokens.
void native_ple_postops_batch(float* key, float* hidden, const float* value, float* history, const PleWeights& w,
float* query_norm, float* gated, float* gate, int T, void* stream);
} // namespace strata::kernels
+9 -3
View File
@@ -110,7 +110,9 @@ inline QsaShapes qsa_real_shapes() {
QsaShapes s;
s.n_head = 24; s.n_head_kv = 2; s.head_dim = 256; s.n_rot = 64;
s.idx_n_head = 4; s.idx_dim = 128; s.idx_block = 4; s.idx_top_k = 2048;
s.page_size = 512;
// one page = one indexer block (4 cells): the granule KV streaming keeps resident (kv_stream.hpp). The readers
// resolve a row per cell anyway, so the page size costs them nothing (measured: identical output and speed).
s.page_size = 4;
return s;
}
@@ -353,8 +355,12 @@ void qsa_attend_step(const float* q, const uint16_t* k_scratch, const uint16_t*
const int32_t* step, int64_t max_ids, const QsaShapes& s, float* attn, float* weights,
void* stream);
/// The cell's position comes from `step`, so this is the form a graph may contain.
struct KvHostPools; // kv_stream.hpp
/// The cell's position comes from `step`, so this is the form a graph may contain. With a host copy (KV streaming)
/// the cell is written there too, and to VRAM only if its block is resident.
void kv_append_step(uint16_t* k_pool, uint16_t* v_pool, const int32_t* page_table, const int32_t* step,
const float* kcur, const float* vcur, const QsaShapes& s, void* stream);
const float* kcur, const float* vcur, const QsaShapes& s, void* stream,
const KvHostPools* host = nullptr);
} // namespace strata::kernels
@@ -27,6 +27,8 @@ struct QsaAttnPools {
const int8_t* v_q = nullptr;
const uint16_t* k_scale = nullptr; ///< fp16 [page][kv_head][page_size][head_dim / 64]
const uint16_t* v_scale = nullptr;
const uint8_t* k_q4 = nullptr; ///< q4_0 block_q4_0 [page][kv_head][page_size][head_dim / 32 * 18]
const uint8_t* v_q4 = nullptr;
const int32_t* page_table = nullptr;
};
+29 -9
View File
@@ -1,21 +1,21 @@
// include/strata/kernels/sampler.hpp - the sampler chain, host-callable (P2.S2).
//
// penalties -> top_k -> top_p -> temperature -> pick
// penalties -> top_k -> top_p -> min_p -> temperature -> pick
//
// The ORDER is specified in docs/sampling.md, transcribed from llama.cpp's own chain. Two facts there are
// easy to get backwards: temperature comes AFTER the truncation filters, and penalties come AFTER temperature.
//
// PENALTIES ARE NOT IMPLEMENTED YET: they need the token history, which is not in this signature, so a caller
// passing a non-zero `penalty_last_n` gets a loud failure rather than a silently unpenalised sample.
// llama.cpp's default chain (issue #53): ONE penalties stage, first, and temperature AFTER the truncation filters;
// top_p cuts before min_p. `src/kernels/cuda/sampler.cu` has the details and `sampler_parity` pins them.
#pragma once
#include <algorithm>
#include <cstddef>
#include <cstdint>
namespace strata::kernels {
struct SamplerParams {
int top_k = 20; // 0 disables the filter
int top_k = 20; // sampled path: 1..64 as given; 0 (off) or more than 64 keep the widest list, 64
float top_p = 0.95f; // 1.0 disables the filter
float min_p = 0.0f; // 0 disables; keeps tokens with p >= min_p * p_max (a prefix of the top_k list)
float temperature = 1.0f; // <= 0 means greedy
int min_keep = 1; // top_p keeps at least this many
int penalty_last_n = 0; // 0 disables; the window over `history` to count occurrences in
@@ -35,9 +35,29 @@ struct SamplerParams {
// reported by whatever synchronising call happens NEXT, which will be somewhere else entirely and will name a
// buffer that has nothing to do with it. A sticky async fault does not know where it came from.
//
// `history` is (n_tokens, history_len) int32, the most recent tokens for each row with any unused slots set to
// -1; only the last `p.penalty_last_n` of each row are counted. Pass nullptr and 0 when no penalties apply.
// `history` is (n_tokens, history_len) int32 - ONE ROW PER TOKEN ROW, at a stride of `history_len` - the most
// recent tokens that row's pick follows, with any unused slots set to -1; only the last `p.penalty_last_n` of
// each row are counted, and ids outside [0, n_vocab) are ignored. Pass nullptr and 0 when no penalties apply.
// A verify window's rows need DIFFERENT histories: row t follows the window's drafts 1..t (`penalty_rows`).
void sample_tokens(const float* logits, int n_tokens, int n_vocab, const int* history, int history_len,
const SamplerParams& p, int* out, void* stream);
// The penalty-history rows of a verify window, on the host: row t of `out` (T rows of `h` slots) is the last `h`
// tokens of `tail[0..n_tail)` followed by `window[0..t]`, most recent LAST, -1 in the unused front slots.
// `tail` is what the state consumed before the window, `window[0]` the fed-back token and `window[1..]` the
// drafts: row t is exactly the history plain decode counts when it picks the token after `window[t]`, so drafting
// cannot change which tokens are penalised. Row 0 alone is the single row the engine staged before 0.1.19.
inline void penalty_rows(const int32_t* tail, int64_t n_tail, const int32_t* window, int T, int h, int32_t* out) {
for (int t = 0; t < T; ++t) {
int32_t* row = out + (size_t) t * (size_t) h;
const int64_t avail = n_tail + t + 1; // tail + window[0..t]
const int take = (int) std::min<int64_t>(h, avail);
std::fill(row, row + (h - take), -1);
for (int j = 0; j < take; ++j) {
const int64_t i = avail - take + j; // index into (tail, window)
row[h - take + j] = i < n_tail ? tail[i] : window[i - n_tail];
}
}
}
} // namespace strata::kernels
+3
View File
@@ -36,6 +36,9 @@ public:
void native(const uint16_t* X, int ggml_type, const void* W_blocks, float* Y, int64_t T, int64_t N, int64_t K,
int64_t ldy = 0, float beta = 0.0f);
/// Caller-owned buffers only: the scratch and workspace moved (the prompt path laid its buffers out again).
void rebind(uint16_t* scratch, int64_t scratch_elems, void* workspace, size_t ws_bytes);
uint16_t* scratch() const { return scratch_; }
int64_t scratch_elems() const { return scratch_elems_; }
void* stream() const { return stream_; }
+7 -2
View File
@@ -5,6 +5,8 @@
// the kernel walks the chunk's tokens in order inside one launch.
#pragma once
#include "strata/kernels/kv_stream.hpp"
#include <cstdint>
namespace strata::prefill {
@@ -35,7 +37,7 @@ void gdn_recurrence(float* state, const float* h, const float* gate, const float
// ---- MoE
/// softmax over 512, top-10 (ties to the lower id), weights renormalised over the ten (the native router).
void route(const float* logits, int32_t* ids, float* weights, int64_t T, void* stream);
void route(const float* logits, int32_t* ids, float* weights, int64_t T, int64_t n_expert, void* stream);
/// Expert blob (Strata pack layout, Q2_0) -> BF16 matrices: gate/up interleaved [1280, 2560], down [2560, 640].
void blob_dequant(const uint8_t* blob, uint16_t* gu16, uint16_t* down16, void* stream);
/// h16[n, r] = fp16(silu(gu[n, 2r]) * gu[n, 2r + 1]) (the interleaved expert gate/up)
@@ -63,11 +65,14 @@ void gate_attn(const float* attn, const float* q_full, uint16_t* out16, int64_t
/// K, V: [T, 2, 256].
void kv_append(const float* K, const float* V, int64_t T, int64_t pos0, const int32_t* page_table, int64_t page_size,
uint16_t* k_pool, uint16_t* v_pool, int8_t* k_q, int8_t* v_q, uint16_t* k_scale, uint16_t* v_scale,
void* stream);
void* stream, const strata::kernels::KvHostPools* host = nullptr,
const strata::kernels::KvHostPools* stage = nullptr);
/// fp32 -> fp16 bits and fp32 -> bf16, n elements (the two activation images of the prompt GEMMs).
void to_f16(const float* x, uint16_t* y, int64_t n, void* stream);
void to_bf16(const float* x, uint16_t* y, int64_t n, void* stream);
/// y = fp32(fp16(x)): what an FP16 store of x would read back (the FP16 indexer-key experiment)
void round_f16(const float* x, float* y, int64_t n, void* stream);
/// Expert blob -> FP16 (Q2_0 values are exact in FP16).
void blob_dequant_f16(const uint8_t* blob, uint16_t* gu16, uint16_t* down16, void* stream);
+73
View File
@@ -0,0 +1,73 @@
// include/strata/prefill/moe_mmq.hpp - prompt-speed plan step 2b: the prompt path's experts through llama.cpp's MMQ
// kernels (ggml-cuda mmq.cuh, MIT): the weights stay quantized and the activations are rounded to q8_1, the
// products run on int8 tensor cores. The dequantize-to-FP16 + cuBLAS path wrote ~10 MB of FP16 per expert and
// multiplied in FP16; this reads the ~1.4-2 MB expert once. A group of experts is gathered into one buffer
// (`gather_*`, one launch per expert as its blob arrives) and multiplied in one launch per product.
#pragma once
#include <cstddef>
#include <cstdint>
namespace strata::prefill::mmq {
/// This build has the MMQ path (the ggml sources were available to the build).
bool built();
/// MMQ covers this ggml type (the i-quants and Q2_0 the packs use; IQ1_M is not covered).
bool supported(int ggml_type);
/// Bytes of one expert's gate+up ([2*n_ff, n_embd]) or down ([n_embd, n_ff]) weights in `ggml_type`.
size_t matrix_bytes(int ggml_type, int64_t rows, int64_t cols);
/// Bytes of `rows` activation rows of `cols` values quantized for MMQ (the row padded to 512 values).
size_t q8_bytes(int64_t rows, int64_t cols);
/// q8_1 activations for MMQ against weights of `ggml_type`: row i of the output is row ids[i] of x (or row i when
/// ids is null); `x` has `ld` floats per row.
void quantize(const float* x, const int32_t* ids, void* xq, int ggml_type, int64_t cols, int64_t ld, int64_t rows,
void* stream);
/// One launch over n experts whose weights lie `expert_bytes` apart from `w`: for expert e, the activation rows
/// [bounds[e], bounds[e+1]) of `xq` (bounds on the device, n+1 entries) times its [w_rows, w_cols] matrix into
/// dst rows of the same indices (`ld_dst` floats apart, via `ids`: dst row = ids[row], an identity table works).
/// `total_rows`: the rows of xq; `max_rows`: the most rows one expert has (the launch grid).
struct Product {
const void* w = nullptr;
int type = -1;
int64_t w_rows = 0, w_cols = 0;
size_t expert_bytes = 0;
int n = 0;
const void* xq = nullptr;
const int32_t* bounds = nullptr;
const int32_t* ids = nullptr;
int64_t total_rows = 0, max_rows = 0;
float* dst = nullptr;
int64_t ld_dst = 0;
};
/// The launch context (llama.cpp's MMQ keeps a small scratch pool for its stream-k fixup). One per prompt path.
class Context {
public:
Context();
~Context();
Context(const Context&) = delete;
Context& operator=(const Context&) = delete;
void run(const Product& p, void* stream);
private:
void* ctx_ = nullptr;
};
/// A GGUF-native expert (gate at `gate`, up at `up`, down at `down`, each its GGUF rows) into a group buffer's
/// slot: gate rows then up rows at `gu_dst`, down at `d_dst`.
void gather_native(const void* gate, const void* up, size_t gu_half_bytes, const void* down, size_t d_bytes,
void* gu_dst, void* d_dst, void* stream);
/// A Strata-pack Q2_0 expert blob (codes and fp16 scales in separate planes, gate/up rows interleaved) into GGUF
/// Q2_0 blocks: gate/up [1280, 2560] at `gu_dst` (rows stay interleaved), down [2560, 640] at `d_dst`. Same values.
void gather_strata_q2(const uint8_t* blob, void* gu_dst, void* d_dst, void* stream);
/// h[r, k] = silu(gate) * up of GU rows [2 n_ff wide]: interleaved (gate 2k, up 2k+1: the Strata pack) or split
/// (gate k, up n_ff + k: GGUF). FP32 out (the down product's quantizer reads floats).
void swiglu(const float* gu, float* h, int64_t rows, int64_t n_ff, bool interleaved, void* stream);
/// dst[i] = i for i < n (the identity row map MMQ's MoE mode writes through).
void iota(int32_t* dst, int64_t n, void* stream);
} // namespace strata::prefill::mmq
+23
View File
@@ -48,6 +48,16 @@ public:
core::ExpertSource* src, const core::ExpertCache* cache, const int32_t* host_res, int64_t chunk,
void* stream, std::string& err, void* borrow = nullptr, uint64_t borrow_bytes = 0);
/// With borrowed buffers: lay them out again for chunks of `chunk` tokens (at most `init`'s) in `borrow` - a
/// request lends only the slots its prompt needs. The stream must be idle (between prompts).
bool relayout(int64_t chunk, void* borrow, uint64_t borrow_bytes, std::string& err);
int64_t chunk() const;
/// The share of the streamed experts' bytes DMA-able straight from pinned RAM (1 = all). Sizes the streamed
/// ring (a big one only pays when the copy engine, not the host copies, is the limit); set before bytes_needed.
static void set_pinned_share(double share);
static double pinned_share();
/// Device bytes `init` needs for a chunk of `chunk` tokens (what a borrowed region must hold).
static uint64_t bytes_needed(const core::ModelGeometry& g, const core::SessionState& ss, int64_t chunk);
@@ -69,7 +79,20 @@ public:
/// embedding where non-null (an image's <|image_pad|> cells). Null (default): every position embeds its token.
const float* const* embd_rows = nullptr;
/// LAYER SPLIT (multi-GPU): this prompt path runs layers [layer_begin, layer_end) (-1: to the last) on the
/// device `init` runs on. A stage that does not start at layer 0 reads each chunk's residual rows from the
/// previous stage instead of embedding the tokens; a stage that does not end at the last layer copies its rows
/// to pinned host buffers (two, allocated by `init`) and runs `next` on them - on a thread, so the next stage
/// reads chunk c while this one reads chunk c + 1. `on_chunk` belongs on the last stage. Set before `init`.
void set_stage(int64_t layer_begin, int64_t layer_end, Prefill* next) {
stage_lb_ = layer_begin; stage_le_ = layer_end; next_ = next;
}
private:
int64_t stage_lb_ = 0, stage_le_ = -1;
Prefill* next_ = nullptr;
const float* hand_in_ = nullptr; ///< the previous stage's rows of the chunk being read (host, pinned)
bool carve(std::size_t T, void* alloc); // the device buffers of a chunk (prefill.cpp's Alloc)
struct Impl;
std::unique_ptr<Impl> impl_;
PrefillStats stats_;
+43
View File
@@ -0,0 +1,43 @@
// include/strata/program/conv_cache.hpp - the conversation cache's retention policy: which checkpoint
// leaves when the cache is over its slot budget.
//
// The cache holds conversation checkpoints: snapshots of the running state (the GDN recurrence, the PLE
// history, the QSA indexer tails - `ConvCheckpoint` in generate.cpp) that a request resumes its prompt
// from instead of reading those tokens again. The request path keeps only the checkpoints whose tokens
// are a prefix of the current prompt - any other checkpoint's positional cells have been overwritten,
// because the KV cache is one arena that holds one branch of history at a time - so at any moment the
// retained checkpoints are a CHAIN: sorted by length, each a prefix of the next. That is a radix cache's
// tree collapsed onto the one branch of history the session can hold.
//
// The chain's root is therefore the deepest point every request so far has shared - in practice the end
// of the system prompt, which every new chat of the same client mounts through - and it is exactly the
// node a radix cache keeps alive while its leaves rotate. First-in-first-out kept it only by accident of
// being first: after `prompt_cache` newer checkpoints it was gone, and the next chat read the whole
// prefix again at prefill speed. So:
//
// * the root is PINNED: dropping it frees one slot's ~118 MB and costs every future conversation that
// shares the prefix a full re-read of it (a 30K system prompt is ~30 s at ~1,000 tok/s);
// * every other slot rotates by least recent use. A checkpoint's stamp advances when it is created,
// when a new checkpoint lands on its length, and when a request mounts through it.
//
// With a budget below two slots there is no room to keep root and leaf apart, so the pin switches off and
// the oldest checkpoint leaves - the previous behaviour, one slot = the newest point only.
#pragma once
#include <cstddef>
#include <cstdint>
namespace strata::program::conv_cache {
/// The index in `stamps` of the chain item to drop once the chain holds more than `cap` items. `stamps`
/// are the items' last-use stamps; the caller owns the chain and erases the returned index. Pure and
/// deterministic so conv_cache_test.cpp can walk the scenarios by hand.
inline size_t eviction_victim(const uint64_t* stamps, size_t n, int64_t cap) {
if (cap < 2 || n < 2) return 0; // no room for root and leaf: the pin is off, the oldest leaves
size_t v = 1; // the root (0) is pinned; least recent use among the rest
for (size_t i = 2; i < n; ++i)
if (stamps[i] < stamps[v]) v = i;
return v;
}
} // namespace strata::program::conv_cache
+52
View File
@@ -0,0 +1,52 @@
// include/strata/spec/draft_policy.hpp - per verify round: the MTP's window, or a lookup (suffix) window?
//
// The suffix drafter (prompt lookup) proposes the tokens that followed an earlier repeat of the context. Taken
// whenever it proposes more than the MTP, it lost 2-8% on ordinary text: a long lookup window costs much more to
// verify than the MTP's usual 3-4 tokens, and it is only worth that when enough of it is accepted. llama.cpp's
// lookup decoding answers the same problem with confidence thresholds on its n-gram statistics; here the policy
// learns both sides online and compares expected committed tokens per millisecond:
//
// MTP E = the mean tokens a window of that size has committed (EMA), at the measured cost of that size
// lookup E(k) = 1 + q + q^2 + ... + q^k for k <= the proposal, q = the acceptance rate of lookup drafts whose
// match was about as long (4 buckets of match length, decayed counts), at the measured cost of k + 1
//
// and takes the lookup window only when its best E/cost beats the MTP's by `margin`. Costs are the measured round
// times per window size (EMA; sizes not seen yet are scaled from seen ones by a prior shape), so the policy adapts
// to the machine and the context length. It only chooses which drafts to verify: the output is unchanged.
#pragma once
#include <array>
namespace strata::spec {
class DraftPolicy {
public:
static constexpr int kMaxT = 8;
static constexpr int kBuckets = 4;
explicit DraftPolicy(int max_t, double margin = 0.03);
struct Pick {
bool lookup = false;
int t = 1; // window size (1 + drafts)
};
/// `t_mtp`: the MTP's window; `lookup_k`: the lookup proposal's length (0 = none); `match`: its match length.
Pick choose(int t_mtp, int lookup_k, int match) const;
/// After the round: the window it used, the drafts accepted, and the round's time (verify + commit + draft).
void observe(bool lookup, int t, int accepted, int match, double round_ms);
double lookup_rate(int match) const; // current q for a match length
double cost_ms(int t) const; // measured or scaled round time of a window of t tokens
private:
static int bucket(int match);
double mtp_tokens(int t) const;
int max_t_;
double margin_;
std::array<double, kMaxT + 1> cost_{}, cost_n_{}; // round ms by window size
std::array<double, kMaxT + 1> mtp_tok_{}, mtp_n_{}; // tokens committed by MTP windows of that size
std::array<double, kBuckets> ok_{}, bad_{}; // lookup drafts accepted / windows cut short, decayed
};
} // namespace strata::spec
+10 -2
View File
@@ -129,6 +129,14 @@ def images_of(messages: list[dict]) -> list[str]:
for item in m["content"] if item.get("type") == "image"]
def _late_system_to_user(messages: list[dict]) -> list[dict]:
"""The chat template takes a system message only at the start ("System message must be at the beginning").
Clients also send them mid-conversation - Claude Code's hook context as {"role": "system"} after the first user
turn, some OpenAI clients a late "developer" message (issue #56) - so those become user messages, in place:
merging them into the first one would change the prompt's start and cost the conversation cache every turn."""
return [dict(m, role="user") if m.get("role") == "system" and i > 0 else m for i, m in enumerate(messages)]
def openai_to_messages(req: dict) -> tuple[list[dict], list[dict] | None, dict]:
"""OpenAI Chat Completions -> (template messages, template tools, template kwargs)."""
messages = []
@@ -161,7 +169,7 @@ def openai_to_messages(req: dict) -> tuple[list[dict], list[dict] | None, dict]:
kwargs = {"enable_thinking": False}
elif k == "reasoning_effort" and "enable_thinking" not in kwargs:
kwargs.update(effort_kwargs(v))
return messages, tools, kwargs
return _late_system_to_user(messages), tools, kwargs
def anthropic_to_messages(req: dict) -> tuple[list[dict], list[dict] | None, dict]:
@@ -210,7 +218,7 @@ def anthropic_to_messages(req: dict) -> tuple[list[dict], list[dict] | None, dic
kwargs.update(effort_kwargs(effort))
elif isinstance(thinking, dict) and thinking.get("budget_tokens"):
kwargs.update(budget_effort(thinking["budget_tokens"]))
return messages, tools, kwargs
return _late_system_to_user(messages), tools, kwargs
# ------------------------------------------------------------------------------------------------ output parser
-152
View File
@@ -1,152 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Strata</title>
<style>
:root { --bg:#f6f7f9; --card:#fff; --fg:#1b1f24; --mute:#667085; --line:#e3e6ea; --acc:#2f6fed; --me:#e9f0ff; --ok:#1a7f37; }
@media (prefers-color-scheme: dark) {
:root { --bg:#111418; --card:#1a1e24; --fg:#e7e9ec; --mute:#9aa3ae; --line:#2b313a; --acc:#6b9bff; --me:#1f2b44; --ok:#3fb950; }
}
* { box-sizing: border-box; }
body { margin:0; background:var(--bg); color:var(--fg); font:15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
main { max-width:860px; margin:0 auto; padding:16px; display:flex; flex-direction:column; gap:12px; min-height:100vh; }
.card { background:var(--card); border:1px solid var(--line); border-radius:12px; padding:14px 16px; }
h1 { font-size:20px; margin:0; display:flex; align-items:center; gap:8px; }
.dot { width:10px; height:10px; border-radius:50%; background:var(--mute); display:inline-block; }
.dot.on { background:var(--ok); }
.mute { color:var(--mute); font-size:13px; }
code { background:var(--bg); border:1px solid var(--line); border-radius:6px; padding:1px 6px; font-size:13px; }
.grid { display:grid; grid-template-columns: max-content 1fr; gap:4px 12px; margin-top:8px; font-size:14px; }
#log { flex:1; display:flex; flex-direction:column; gap:10px; overflow-y:auto; min-height:240px; }
.msg { padding:10px 12px; border-radius:10px; white-space:pre-wrap; word-wrap:break-word; }
.msg.user { background:var(--me); align-self:flex-end; max-width:85%; }
.msg.bot { background:var(--card); border:1px solid var(--line); }
.msg img { max-width:220px; max-height:160px; display:block; border-radius:6px; margin-bottom:6px; }
details { color:var(--mute); font-size:13px; margin-bottom:6px; }
form { display:flex; gap:8px; align-items:flex-end; flex-wrap:wrap; }
textarea { flex:1; min-width:200px; min-height:52px; max-height:200px; resize:vertical; padding:10px; border-radius:10px;
border:1px solid var(--line); background:var(--card); color:var(--fg); font:inherit; }
button, label.btn { border:1px solid var(--line); background:var(--card); color:var(--fg); border-radius:10px; padding:10px 14px;
font:inherit; cursor:pointer; }
button.primary { background:var(--acc); border-color:var(--acc); color:#fff; }
button:disabled { opacity:.5; cursor:default; }
.row { display:flex; gap:14px; align-items:center; flex-wrap:wrap; font-size:13px; color:var(--mute); }
#att { font-size:13px; color:var(--mute); }
</style>
</head>
<body>
<main>
<div class="card">
<h1><span id="dot" class="dot"></span> Strata <span id="state" class="mute">connecting ...</span></h1>
<div class="grid">
<span class="mute">Model</span><span id="model">-</span>
<span class="mute">Context</span><span id="ctx">-</span>
<span class="mute">Images</span><span id="img">-</span>
<span class="mute">OpenAI API</span><span><code id="oai"></code> (any API key<span id="keynote"></span>)</span>
<span class="mute">Anthropic API</span><span><code id="ant"></code></span>
</div>
<p class="mute" style="margin:10px 0 0">Point any app with an "OpenAI-compatible" or "Anthropic" provider at these
addresses. Close the Strata window to stop the model.</p>
</div>
<div id="log" class="card"></div>
<div class="card">
<form id="f">
<textarea id="q" placeholder="Ask something ... (Enter sends, Shift+Enter = new line)"></textarea>
<label class="btn" id="attbtn" hidden>Picture<input id="file" type="file" accept="image/png,image/jpeg,image/gif,image/bmp" hidden></label>
<button class="primary" id="send">Send</button>
<button type="button" id="stop" disabled>Stop</button>
</form>
<div class="row" style="margin-top:8px">
<label>Thinking <select id="think" style="padding:4px;border-radius:8px;border:1px solid var(--line);background:var(--card);color:var(--fg)">
<option value="none">off - answer at once</option>
<option value="low">low</option>
<option value="medium" selected>medium</option>
<option value="high">high - best on hard questions, slowest</option>
</select></label>
<span id="att"></span>
<button type="button" id="clear" style="padding:4px 10px">New chat</button>
<input id="key" type="password" placeholder="API key" hidden style="padding:6px;border-radius:8px;border:1px solid var(--line)">
</div>
</div>
</main>
<script>
const $ = id => document.getElementById(id);
const base = location.origin;
$("oai").textContent = base + "/v1";
$("ant").textContent = base + "/v1/messages";
let history = [], image = null, ctrl = null;
async function status() {
try {
const h = await (await fetch("/health")).json();
$("dot").classList.add("on"); $("state").textContent = "running";
$("model").textContent = h.model; $("ctx").textContent = h.max_context.toLocaleString() + " tokens";
$("img").textContent = h.images ? "on" : "off (run setup again and answer yes to images)";
$("attbtn").hidden = !h.images;
if (h.api_key) { $("key").hidden = false; $("keynote").textContent = " - this server needs its key"; }
} catch (e) { $("state").textContent = "not reachable - is the Strata window still open?"; }
}
status();
function bubble(cls, text) {
const d = document.createElement("div"); d.className = "msg " + cls; d.textContent = text || "";
$("log").appendChild(d); $("log").scrollTop = $("log").scrollHeight; return d;
}
$("file").onchange = () => {
const f = $("file").files[0]; if (!f) return;
const r = new FileReader();
r.onload = () => { image = r.result; $("att").textContent = "picture: " + f.name; };
r.readAsDataURL(f);
};
$("clear").onclick = () => { history = []; $("log").innerHTML = ""; };
$("stop").onclick = () => ctrl && ctrl.abort();
$("q").addEventListener("keydown", e => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); $("f").requestSubmit(); } });
$("f").onsubmit = async e => {
e.preventDefault();
const text = $("q").value.trim(); if (!text) return;
$("q").value = "";
const content = image ? [{type: "image_url", image_url: {url: image}}, {type: "text", text}] : text;
const u = bubble("user", "");
if (image) { const im = document.createElement("img"); im.src = image; u.appendChild(im); }
u.appendChild(document.createTextNode(text));
history.push({role: "user", content});
image = null; $("att").textContent = ""; $("file").value = "";
const b = bubble("bot", ""); const det = document.createElement("details"); const sum = document.createElement("summary");
sum.textContent = "thinking"; det.appendChild(sum); const th = document.createElement("div"); det.appendChild(th);
const ans = document.createElement("div"); b.appendChild(ans);
$("send").disabled = true; $("stop").disabled = false; ctrl = new AbortController();
let answer = "";
try {
const headers = {"Content-Type": "application/json"};
if ($("key").value) headers["Authorization"] = "Bearer " + $("key").value;
const body = {model: "strata", messages: history, stream: true, max_tokens: 8192};
body.reasoning_effort = $("think").value;
const r = await fetch("/v1/chat/completions", {method: "POST", headers, body: JSON.stringify(body), signal: ctrl.signal});
if (!r.ok) { ans.textContent = "error: " + ((await r.json()).error || {}).message; throw 0; }
const rd = r.body.getReader(), dec = new TextDecoder(); let buf = "";
for (;;) {
const {value, done} = await rd.read(); if (done) break;
buf += dec.decode(value, {stream: true});
let i;
while ((i = buf.indexOf("\n\n")) >= 0) {
const line = buf.slice(0, i).trim(); buf = buf.slice(i + 2);
if (!line.startsWith("data: ") || line === "data: [DONE]") continue;
const d = JSON.parse(line.slice(6)).choices[0].delta;
if (d.reasoning_content) { if (!det.parentNode) b.insertBefore(det, ans); th.textContent += d.reasoning_content; }
if (d.content) { answer += d.content; ans.textContent = answer; }
$("log").scrollTop = $("log").scrollHeight;
}
}
} catch (err) { if (err && err.name === "AbortError") ans.textContent = answer + " [stopped]"; }
history.push({role: "assistant", content: answer});
$("send").disabled = false; $("stop").disabled = true; ctrl = null;
};
</script>
</body>
</html>
+669
View File
@@ -0,0 +1,669 @@
"""serve/mcp.py - tools from MCP (Model Context Protocol) servers for the web app's chats.
The user lists servers in the run config (`"mcp_servers"`, or Claude Desktop's `"mcpServers"` block pasted as is) or
in a separate file (`--mcp-config`). Each one is either a program Strata starts (`command`, `args`, `env`, `cwd`:
the stdio transport, newline-delimited JSON-RPC 2.0 on its stdin/stdout) or an address (`url`, `headers`: the
Streamable HTTP transport, JSON-RPC POSTed, answered as JSON or as an event stream). Both are implemented here with
the standard library only; no MCP SDK is needed.
`McpHub` owns every configured server: it starts them in the background when the server starts, offers their tools to
the model as OpenAI function tools named `<server>__<tool>` (two servers may both have a `search`), and routes a call
back to the server that has it. A server that does not start is left out with one log line; the chat works without
it. A server that stops later (a crash) is started again on its next call.
Tool results reach the model as text: the MCP content blocks joined, capped at `max_result_chars` (a directory tree or
a web page can be megabytes, far more than a context holds) with a note saying so. A failed call - the tool's own
error, a crash, a timeout - becomes a result that starts with "error:", so the model can react instead of the chat
breaking.
"""
from __future__ import annotations
import collections
import json
import os
import re
import shutil
import signal
import subprocess
import sys
import threading
import time
import urllib.error
import urllib.request
PROTOCOL = "2025-06-18" # the MCP revision Strata asks for; the server's answer is used as given
DEFAULTS = {"timeout_s": 60.0, "max_result_chars": 20000, "max_rounds": 8, "start_timeout_s": 120.0}
class McpError(RuntimeError):
"""A server answered with an error, stopped, or could not be reached."""
class McpTimeout(McpError):
pass
class McpCancelled(McpError):
"""The chat was stopped while the call ran."""
class _Slot:
"""One request waiting for its response."""
def __init__(self):
self.done = threading.Event()
self.msg = None
self.error = None
def _wait(slot: _Slot, timeout: float, cancel: threading.Event | None, what: str):
"""Wait for a response, checking the chat's stop button and the deadline a few times a second."""
deadline = time.monotonic() + timeout
while not slot.done.wait(0.05):
if cancel is not None and cancel.is_set():
raise McpCancelled("stopped")
if time.monotonic() > deadline:
raise McpTimeout(f"{what} timed out after {timeout:g} s")
if slot.error:
raise slot.error
msg = slot.msg
if "error" in msg:
err = msg["error"] if isinstance(msg["error"], dict) else {"message": str(msg["error"])}
raise McpError(f"{err.get('message') or 'error'} (code {err.get('code')})")
return msg.get("result") or {}
# ------------------------------------------------------------------------------------------------ transports
class StdioTransport:
"""A server Strata starts: JSON-RPC messages one per line on its stdin and stdout; its stderr is its log."""
kind = "stdio"
def __init__(self, name: str, cfg: dict):
self.name, self.cfg = name, cfg
self.proc = None
self.lock = threading.Lock() # the pending requests
self.write_lock = threading.Lock() # stdin; separate, so a full pipe can't block the reader
self.pending: dict[int, _Slot] = {}
self.next_id = 0
self.stderr = collections.deque(maxlen=40) # its last log lines, for the error message when it fails
self.protocol = PROTOCOL
def start(self):
env = dict(os.environ)
env.update({str(k): str(v) for k, v in (self.cfg.get("env") or {}).items()})
command = str(self.cfg["command"])
# On Windows `npx`, `uvx` and friends are .cmd files that CreateProcess finds only with their extension
exe = shutil.which(command, path=env.get("PATH")) or command
extra = {}
if os.name == "nt":
extra["creationflags"] = getattr(subprocess, "CREATE_NO_WINDOW", 0)
else:
extra["start_new_session"] = True # its own process group, so close() ends its children too
try:
self.proc = subprocess.Popen([exe, *[str(a) for a in self.cfg.get("args") or []]], stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=self.cfg.get("cwd") or None,
env=env, **extra)
except OSError as e:
raise McpError(f"could not start {command!r}: {e}") from None
threading.Thread(target=self._read, daemon=True).start()
threading.Thread(target=self._read_stderr, daemon=True).start()
def alive(self) -> bool:
return self.proc is not None and self.proc.poll() is None and not getattr(self, "ended", False)
@staticmethod
def _lines(f):
"""A pipe's lines until it closes (or close() closed it under us)."""
try:
yield from f
except (OSError, ValueError):
return
def _read_stderr(self):
for raw in self._lines(self.proc.stderr):
line = raw.decode("utf-8", "replace").rstrip()
if line:
self.stderr.append(line)
if os.environ.get("STRATA_DEBUG"):
print(f"[mcp {self.name}] {line}", flush=True)
def _read(self):
for raw in self._lines(self.proc.stdout):
line = raw.decode("utf-8", "replace").strip()
if not line:
continue
try:
msg = json.loads(line)
except ValueError: # a server that prints to stdout; the spec forbids it
self.stderr.append(line)
continue
for m in msg if isinstance(msg, list) else [msg]:
if isinstance(m, dict):
self._dispatch(m)
self.ended = True
code = self.proc.poll()
err = McpError(f"the server stopped{f' (exit code {code})' if code is not None else ''}{self._tail()}")
with self.lock:
slots, self.pending = list(self.pending.values()), {}
for s in slots:
s.error = err
s.done.set()
def _tail(self) -> str:
return f": {self.stderr[-1][:300]}" if self.stderr else ""
def _dispatch(self, m: dict):
if "method" in m:
if "id" in m: # a request from the server: we offer no client features
try:
if m["method"] == "ping":
self._send({"jsonrpc": "2.0", "id": m["id"], "result": {}})
else:
self._send({"jsonrpc": "2.0", "id": m["id"],
"error": {"code": -32601, "message": f"method not supported: {m['method']}"}})
except McpError:
pass
return # notifications (log messages, list changes): not used
with self.lock:
slot = self.pending.pop(m.get("id"), None)
if slot is not None: # an answer to a call that timed out is simply dropped
slot.msg = m
slot.done.set()
def _send(self, msg: dict):
data = json.dumps(msg, ensure_ascii=False).encode("utf-8") + b"\n"
try:
with self.write_lock:
self.proc.stdin.write(data)
self.proc.stdin.flush()
except (OSError, ValueError):
raise McpError(f"the server stopped{self._tail()}") from None
def request(self, method: str, params: dict, timeout: float, cancel=None):
if not self.alive():
raise McpError(f"the server is not running{self._tail()}")
slot = _Slot()
with self.lock:
self.next_id += 1
rid = self.next_id
self.pending[rid] = slot
try:
self._send({"jsonrpc": "2.0", "id": rid, "method": method, "params": params})
return _wait(slot, timeout, cancel, method)
except (McpTimeout, McpCancelled) as e:
with self.lock:
self.pending.pop(rid, None)
try: # tell the server to stop working on it
self.notify("notifications/cancelled", {"requestId": rid, "reason": str(e)})
except McpError:
pass
raise
def notify(self, method: str, params: dict | None = None):
self._send({"jsonrpc": "2.0", "method": method, **({"params": params} if params is not None else {})})
def close(self):
"""As the spec says: close its stdin, give it a moment to exit, then end it (and what it started)."""
p = self.proc
if p is None:
return
try:
p.stdin.close()
except OSError:
pass
try:
p.wait(timeout=2)
except subprocess.TimeoutExpired:
self._kill_tree(p)
for f in (p.stdout, p.stderr):
try:
f.close()
except OSError:
pass
@staticmethod
def _kill_tree(p):
try:
if os.name == "nt": # npx.cmd -> node: end the whole tree
subprocess.run(["taskkill", "/PID", str(p.pid), "/T", "/F"], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, timeout=10)
else:
os.killpg(p.pid, signal.SIGTERM)
try:
p.wait(timeout=2)
except subprocess.TimeoutExpired:
os.killpg(p.pid, signal.SIGKILL)
except (OSError, subprocess.SubprocessError):
pass
try:
p.kill()
except OSError:
pass
class HttpTransport:
"""A server at an address (Streamable HTTP): every message is a POST; the answer is JSON or an event stream that
carries it. The session id the server hands out at `initialize` rides on every later request."""
kind = "http"
def __init__(self, name: str, cfg: dict):
self.name, self.url = name, str(cfg["url"])
self.headers = {str(k): str(v) for k, v in (cfg.get("headers") or {}).items()}
self.session = None
self.protocol = None # sent as MCP-Protocol-Version once negotiated
self.next_id = 0
self.lock = threading.Lock()
self.broken = None
def start(self):
pass
def alive(self) -> bool:
return self.broken is None
def _headers(self) -> dict:
h = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream",
"User-Agent": "strata", **self.headers}
if self.session:
h["Mcp-Session-Id"] = self.session
if self.protocol:
h["MCP-Protocol-Version"] = self.protocol
return h
def _post(self, msg: dict, rid, timeout: float, slot: _Slot | None):
try:
req = urllib.request.Request(self.url, data=json.dumps(msg, ensure_ascii=False).encode("utf-8"),
headers=self._headers(), method="POST")
with urllib.request.urlopen(req, timeout=timeout) as r:
if r.headers.get("Mcp-Session-Id"):
self.session = r.headers["Mcp-Session-Id"]
if slot is None: # a notification: 202, nothing to read
return
ctype = r.headers.get("Content-Type", "")
if "text/event-stream" in ctype:
found = self._from_events(r, rid)
else:
body = r.read()
found = None
parsed = json.loads(body) if body.strip() else None
for m in parsed if isinstance(parsed, list) else [parsed]:
if isinstance(m, dict) and m.get("id") == rid and "method" not in m:
found = m
if found is None:
raise McpError("the server's answer did not contain a response")
slot.msg = found
except urllib.error.HTTPError as e:
detail = ""
try:
detail = e.read().decode("utf-8", "replace")[:300]
except OSError:
pass
if e.code == 404 and self.session: # the session expired: a restart starts a new one
self.broken = McpError("the server ended the session")
err = McpError(f"HTTP {e.code} {e.reason}{': ' + detail if detail else ''}")
if slot is None:
raise err from None
slot.error = err
except (OSError, ValueError, McpError) as e:
err = e if isinstance(e, McpError) else McpError(f"could not reach {self.url}: {e}")
if slot is None:
raise err from None
slot.error = err
finally:
if slot is not None:
slot.done.set()
@staticmethod
def _from_events(r, rid):
"""Read the event stream until the event that answers `rid` (it may carry notifications first)."""
data = []
for raw in r:
line = raw.decode("utf-8", "replace").rstrip("\r\n")
if line.startswith("data:"):
data.append(line[5:].lstrip(" "))
elif not line and data:
try:
m = json.loads("\n".join(data))
except ValueError:
m = None
data = []
for x in m if isinstance(m, list) else [m]:
if isinstance(x, dict) and x.get("id") == rid and "method" not in x:
return x
return None
def request(self, method: str, params: dict, timeout: float, cancel=None):
if self.broken:
raise self.broken
with self.lock:
self.next_id += 1
rid = self.next_id
slot = _Slot()
# the POST runs on its own thread so the stop button and the deadline are noticed while it waits
threading.Thread(target=self._post, args=({"jsonrpc": "2.0", "id": rid, "method": method, "params": params},
rid, timeout + 5, slot), daemon=True).start()
try:
return _wait(slot, timeout, cancel, method)
except (McpTimeout, McpCancelled) as e:
threading.Thread(target=self._quiet_notify, args=("notifications/cancelled",
{"requestId": rid, "reason": str(e)}), daemon=True).start()
raise
def _quiet_notify(self, method, params):
try:
self.notify(method, params)
except McpError:
pass
def notify(self, method: str, params: dict | None = None):
self._post({"jsonrpc": "2.0", "method": method, **({"params": params} if params is not None else {})},
None, 10, None)
def close(self):
if not self.session:
return
try: # end the session on the server (it may not allow that)
req = urllib.request.Request(self.url, headers=self._headers(), method="DELETE")
urllib.request.urlopen(req, timeout=3).close()
except (OSError, ValueError):
pass
# ------------------------------------------------------------------------------------------------ one server
class McpServer:
"""One configured server: started once (and again after it stopped), with the tools it listed."""
def __init__(self, name: str, cfg: dict, settings: dict):
self.name, self.cfg, self.settings = name, cfg, settings
self.kind = "http" if cfg.get("url") else "stdio"
self.status = "idle" # idle -> starting -> ready | failed; ready -> stopped when it ends
self.error = None
self.tools: list[dict] = []
self.info: dict = {}
self.transport = None
self.lock = threading.Lock()
self.last_start = 0.0
def start(self) -> bool:
with self.lock:
return self._start()
def _start(self) -> bool:
if self.transport is not None:
self.transport.close()
self.status, self.error, self.last_start = "starting", None, time.monotonic()
t = HttpTransport(self.name, self.cfg) if self.kind == "http" else StdioTransport(self.name, self.cfg)
self.transport = t
timeout = float(self.settings["start_timeout_s"])
try:
t.start()
res = t.request("initialize", {"protocolVersion": PROTOCOL, "capabilities": {},
"clientInfo": {"name": "strata", "title": "Strata", "version": "1"}},
timeout)
t.protocol = str(res.get("protocolVersion") or PROTOCOL)
self.info = {k: v for k, v in (res.get("serverInfo") or {}).items() if k in ("name", "title", "version")}
self.info["protocol"] = t.protocol
t.notify("notifications/initialized")
tools, cursor = [], None
for _ in range(100): # pages; a server that never stops paging is cut off
try:
page = t.request("tools/list", {"cursor": cursor} if cursor else {}, timeout)
except McpError as e:
if "code -32601" in str(e) and not tools: # a server without tools (prompts/resources only)
break
raise
tools += [x for x in page.get("tools") or [] if isinstance(x, dict) and x.get("name")]
cursor = page.get("nextCursor")
if not cursor:
break
self.tools, self.status = tools, "ready"
names = ", ".join(x["name"] for x in tools[:8]) + (", ..." if len(tools) > 8 else "")
print(f"[strata] MCP server {self.name!r}: {len(tools)} tool{'s' * (len(tools) != 1)}"
f"{' (' + names + ')' if tools else ''}", flush=True)
return True
except McpError as e:
self.status, self.error, self.tools = "failed", str(e), []
t.close()
print(f"[strata] MCP server {self.name!r} did not start: {e}. Its tools are left out; the chat works "
"without them.", flush=True)
return False
def call(self, tool: str, arguments: dict, timeout: float, cancel=None) -> dict:
with self.lock:
if self.transport is None or not self.transport.alive():
# it stopped since its last call (a crash, an expired session): start it again; one that failed to
# start is tried again at most every 10 s, so a broken command does not run on every call
if self.status == "failed" and time.monotonic() - self.last_start < 10:
raise McpError(f"the server is not running ({self.error or 'it stopped'})")
print(f"[strata] MCP server {self.name!r} had stopped; starting it again", flush=True)
if not self._start():
raise McpError(f"the server could not be started again: {self.error}")
t = self.transport
try:
return t.request("tools/call", {"name": tool, "arguments": arguments}, timeout, cancel)
except McpError as e:
if not t.alive():
self.status, self.error = "stopped", str(e)
raise
def close(self):
with self.lock:
if self.transport is not None:
self.transport.close()
if self.status in ("ready", "starting"):
self.status = "stopped"
# ------------------------------------------------------------------------------------------------ the hub
def result_text(result: dict) -> str:
"""An MCP tool result as the text the model reads: the text blocks as they are, other blocks named."""
parts = []
for block in result.get("content") or []:
if not isinstance(block, dict):
continue
kind = block.get("type")
if kind == "text":
parts.append(str(block.get("text", "")))
elif kind == "resource":
res = block.get("resource") or {}
parts.append(str(res["text"]) if "text" in res else f"[resource {res.get('uri', '')} "
f"({res.get('mimeType', 'binary')}), not shown]")
elif kind == "resource_link":
parts.append(f"[resource link: {block.get('name') or ''} {block.get('uri', '')}]".replace(" ", " "))
elif kind in ("image", "audio"):
size = len(block.get("data") or "") * 3 // 4
parts.append(f"[{kind} ({block.get('mimeType', '?')}, {size:,} bytes): not shown to the model]")
else:
parts.append(json.dumps(block, ensure_ascii=False))
if not parts and result.get("structuredContent") is not None:
parts.append(json.dumps(result["structuredContent"], ensure_ascii=False))
return "\n".join(parts)
def _clean(name: str) -> str:
return re.sub(r"[^A-Za-z0-9_-]", "_", str(name)) or "x"
class McpHub:
"""Every configured server, and the merged, namespaced tool list the model sees."""
def __init__(self, servers: dict[str, dict], settings: dict | None = None):
self.settings = {**DEFAULTS, **(settings or {})}
self.servers = {name: McpServer(name, cfg, self.settings) for name, cfg in servers.items()}
self.threads: list[threading.Thread] = []
self._routes: dict[str, tuple[McpServer, str]] = {}
def start(self, wait: bool = False):
"""Start every server on its own thread (npx may download a package first: that must not delay the chat)."""
self.threads = [threading.Thread(target=s.start, daemon=True, name=f"mcp-{s.name}")
for s in self.servers.values()]
for t in self.threads:
t.start()
if wait:
self.wait(None)
def wait(self, timeout: float | None) -> None:
"""Wait (up to `timeout` s in all) for servers that are still starting."""
end = None if timeout is None else time.monotonic() + timeout
for t in self.threads:
t.join(None if end is None else max(0.0, end - time.monotonic()))
def routes(self) -> dict[str, tuple[McpServer, str]]:
"""tool name the model sees -> (server, the tool's own name). A server that stopped after it was ready keeps
its tools here: a call starts it again."""
out = {}
for s in self.servers.values():
if s.status not in ("ready", "stopped"):
continue
for t in s.tools:
name = f"{_clean(s.name)}__{_clean(t['name'])}"[:64]
n = 2
while name in out: # two names that differ only in cleaned characters
name = f"{name[:60]}_{n}"
n += 1
out[name] = (s, t["name"])
self._routes = out
return out
def template_tools(self, exclude=()) -> list[dict]:
"""The tools in the chat template's form ({name, description, parameters}); `exclude`: names the request
brought itself (those win)."""
out = []
for name, (s, tool) in self.routes().items():
if name in exclude:
continue
t = next(x for x in s.tools if x["name"] == tool)
schema = t.get("inputSchema") if isinstance(t.get("inputSchema"), dict) else {}
out.append({"name": name, "description": str(t.get("description") or t.get("title") or ""),
"parameters": schema or {"type": "object", "properties": {}}})
return out
def openai_tools(self) -> list[dict]:
return [{"type": "function", "function": t} for t in self.template_tools()]
def call(self, name: str, arguments: dict, cancel: threading.Event | None = None) -> dict:
"""Run one tool -> {"ok", "text" (what the model reads, capped), "chars" (its full length), "truncated",
"ms", "server", "tool"}. Errors become text starting with "error:"; only McpCancelled is raised."""
t0 = time.monotonic()
server, tool = self._routes.get(name) or self.routes().get(name, (None, name))
out = {"server": server.name if server else None, "tool": tool}
if server is None:
text, ok = f"error: there is no tool named {name!r}", False
else:
try:
res = server.call(tool, arguments if isinstance(arguments, dict) else {},
float(self.settings["timeout_s"]), cancel)
text, ok = result_text(res), not res.get("isError")
if not ok:
text = "error: " + (text or "the tool reported an error")
except McpCancelled:
raise
except McpError as e:
text, ok = f"error: {e}", False
cap = int(self.settings["max_result_chars"])
full = len(text)
if cap > 0 and full > cap:
text = text[:cap] + (f"\n\n[... truncated: the tool returned {full:,} characters; only the first "
f"{cap:,} are shown]")
out.update(ok=ok, text=text, chars=full, truncated=cap > 0 and full > cap,
ms=int((time.monotonic() - t0) * 1000))
return out
def status(self) -> dict:
"""GET /mcp: every server, its state and its tools (for the web app)."""
routes = self.routes()
servers = []
for s in self.servers.values():
names = {tool: n for n, (srv, tool) in routes.items() if srv is s}
servers.append({"name": s.name, "transport": s.kind, "status": s.status, "error": s.error,
"info": s.info,
"tools": [{"name": names.get(t["name"], t["name"]), "tool": t["name"],
"description": str(t.get("description") or "")[:300]} for t in s.tools]})
return {"servers": servers, "tools": len(routes),
"settings": {k: self.settings[k] for k in ("timeout_s", "max_result_chars", "max_rounds")}}
def close(self):
for s in self.servers.values():
try:
s.close()
except Exception: # noqa: BLE001 - closing is best effort
pass
# ------------------------------------------------------------------------------------------------ config
def _check_server(name, cfg, where) -> dict:
if not isinstance(cfg, dict):
raise SystemExit(f"[strata] {where}: MCP server {name!r} must be an object with \"command\" or \"url\"")
if cfg.get("url"):
if cfg.get("type") == "sse":
raise SystemExit(f"[strata] {where}: MCP server {name!r} uses the old SSE transport (\"type\": \"sse\"); "
"Strata speaks Streamable HTTP - most servers offer it at /mcp")
if not isinstance(cfg.get("headers") or {}, dict):
raise SystemExit(f"[strata] {where}: MCP server {name!r}: \"headers\" must be an object")
return cfg
if not isinstance(cfg.get("command"), str) or not cfg["command"].strip():
raise SystemExit(f"[strata] {where}: MCP server {name!r} needs \"command\" (a program to start) or \"url\"")
if not isinstance(cfg.get("args") or [], list):
raise SystemExit(f"[strata] {where}: MCP server {name!r}: \"args\" must be a list")
if not isinstance(cfg.get("env") or {}, dict):
raise SystemExit(f"[strata] {where}: MCP server {name!r}: \"env\" must be an object")
return cfg
def servers_from(block, where: str) -> dict[str, dict]:
"""{"name": {...}} with the entries checked; `"disabled": true` leaves one out."""
if block is None:
return {}
if not isinstance(block, dict):
raise SystemExit(f"[strata] {where}: the MCP servers must be an object {{\"name\": {{...}}}}")
return {str(n): _check_server(n, c, where) for n, c in block.items()
if not (isinstance(c, dict) and c.get("disabled") is True)}
def settings_from(cfg: dict) -> dict:
"""The run config's optional `"mcp"` block: timeout_s, max_result_chars, max_rounds, start_timeout_s."""
out = {}
for key, value in (cfg.get("mcp") or {}).items():
number = isinstance(value, (int, float)) and not isinstance(value, bool)
if key in ("timeout_s", "start_timeout_s"):
if not number or value <= 0:
raise SystemExit(f"[strata] config mcp.{key}={value!r}: expected a number of seconds > 0")
out[key] = float(value)
elif key in ("max_result_chars", "max_rounds"):
if not number or value != int(value) or value < 1:
raise SystemExit(f"[strata] config mcp.{key}={value!r}: expected a positive integer")
out[key] = int(value)
else:
print(f"[strata] config mcp.{key}={value!r}: unknown key, ignored", flush=True)
return out
def hub_from_config(cfg: dict, mcp_config_path: str | None = None) -> McpHub | None:
"""The run config's `"mcp_servers"` (or `"mcpServers"`) plus the servers in the --mcp-config file (Claude
Desktop's format: {"mcpServers": {...}}); a name in both takes the file's entry. None when there are none."""
servers = {}
servers.update(servers_from(cfg.get("mcp_servers"), "config mcp_servers"))
servers.update(servers_from(cfg.get("mcpServers"), "config mcpServers"))
if mcp_config_path:
try:
with open(mcp_config_path, encoding="utf-8-sig") as f:
data = json.load(f)
except (OSError, ValueError) as e:
raise SystemExit(f"[strata] --mcp-config {mcp_config_path}: {e}") from None
block = data.get("mcpServers", data.get("mcp_servers")) if isinstance(data, dict) else None
if block is None:
raise SystemExit(f"[strata] --mcp-config {mcp_config_path}: expected {{\"mcpServers\": {{...}}}}")
servers.update(servers_from(block, f"--mcp-config {mcp_config_path}"))
if not servers:
return None
return McpHub(servers, settings_from(cfg))
if __name__ == "__main__": # python -m serve.mcp config.json: list what a config offers
hub = hub_from_config(json.load(open(sys.argv[1], encoding="utf-8-sig")) if len(sys.argv) > 1 else {},
sys.argv[2] if len(sys.argv) > 2 else None)
if hub is None:
sys.exit("no MCP servers configured")
hub.start(wait=True)
print(json.dumps(hub.status(), indent=1))
hub.close()
+172
View File
@@ -0,0 +1,172 @@
"""serve/mcp_fake_server.py - a small MCP server for serve/test_mcp.py (no MCP SDK, no network beyond localhost).
python serve/mcp_fake_server.py stdio: JSON-RPC lines on stdin/stdout
python serve/mcp_fake_server.py --http 8765 Streamable HTTP on 127.0.0.1:8765/mcp
Tools: echo (returns its text), fail (a tool error, isError), sleep (answers after `seconds`), big (`n` characters),
die (the process exits mid-call), add (two integers). tools/list pages two tools at a time (`--page N`), so the
client's pagination is exercised. `--crash-at-start` exits before answering anything. With FAKE_MCP_LOG set, the
server appends what it saw (calls, cancellations) to that file, one JSON line each.
"""
from __future__ import annotations
import json
import os
import sys
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
TOOLS = [
{"name": "echo", "description": "Returns the text it is given.",
"inputSchema": {"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]}},
{"name": "fail", "description": "Always fails.", "inputSchema": {"type": "object", "properties": {}}},
{"name": "sleep", "description": "Waits, then answers.",
"inputSchema": {"type": "object", "properties": {"seconds": {"type": "number"}}}},
{"name": "big", "description": "Returns n characters.",
"inputSchema": {"type": "object", "properties": {"n": {"type": "integer"}}}},
{"name": "die", "description": "Ends the server.", "inputSchema": {"type": "object", "properties": {}}},
{"name": "add", "description": "Adds two integers.",
"inputSchema": {"type": "object", "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}}}},
]
PAGE = 2
cancelled: set = set()
def log(event: dict):
path = os.environ.get("FAKE_MCP_LOG")
if path:
with open(path, "a", encoding="utf-8") as f:
f.write(json.dumps(event) + "\n")
def text(t, error=False):
return {"content": [{"type": "text", "text": t}], "isError": error}
def handle(msg: dict):
"""-> the response for a request, None for a notification."""
method, rid, params = msg.get("method"), msg.get("id"), msg.get("params") or {}
if "id" not in msg:
if method == "notifications/cancelled":
cancelled.add(params.get("requestId"))
log({"cancelled": params.get("requestId")})
return None
if method == "initialize":
return {"jsonrpc": "2.0", "id": rid, "result": {
"protocolVersion": params.get("protocolVersion", "2025-06-18"), "capabilities": {"tools": {}},
"serverInfo": {"name": "fake", "version": "0.1"}}}
if method == "tools/list":
start = int(params.get("cursor") or 0)
page = {"tools": TOOLS[start:start + PAGE]}
if start + PAGE < len(TOOLS):
page["nextCursor"] = str(start + PAGE)
return {"jsonrpc": "2.0", "id": rid, "result": page}
if method == "tools/call":
name, args = params.get("name"), params.get("arguments") or {}
log({"call": name, "arguments": args})
if name == "echo":
result = text(str(args.get("text", "")))
elif name == "fail":
result = text("it failed on purpose", error=True)
elif name == "sleep":
time.sleep(float(args.get("seconds", 1)))
result = text("slept")
elif name == "big":
result = text("y" * int(args.get("n", 100000)))
elif name == "die":
sys.stderr.write("dying on purpose\n")
sys.stderr.flush()
os._exit(3)
elif name == "add":
result = text(str(int(args.get("a", 0)) + int(args.get("b", 0))))
else:
return {"jsonrpc": "2.0", "id": rid, "error": {"code": -32602, "message": f"unknown tool {name}"}}
return {"jsonrpc": "2.0", "id": rid, "result": result}
return {"jsonrpc": "2.0", "id": rid, "error": {"code": -32601, "message": f"no method {method}"}}
def stdio():
for f in (sys.stdin, sys.stdout, sys.stderr): # MCP's stdio is UTF-8, whatever the Windows code page is
f.reconfigure(encoding="utf-8")
out = threading.Lock()
def answer(msg):
r = handle(msg)
if r is not None and msg.get("id") not in cancelled:
with out:
sys.stdout.write(json.dumps(r) + "\n")
sys.stdout.flush()
for line in sys.stdin:
if not line.strip():
continue
msg = json.loads(line)
if msg.get("method") == "tools/call": # calls on threads: a slow one must not block cancellation
threading.Thread(target=answer, args=(msg,), daemon=True).start()
else:
answer(msg)
def http_server(port: int = 0) -> ThreadingHTTPServer:
"""Streamable HTTP at /mcp: JSON answers, except tools/call, which answers as an event stream (a progress
notification first); a session id from initialize is required afterwards."""
sessions = set()
class H(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.0"
def log_message(self, *a):
pass
def do_POST(self):
msg = json.loads(self.rfile.read(int(self.headers.get("Content-Length", 0))))
if msg.get("method") != "initialize" and self.headers.get("Mcp-Session-Id") not in sessions:
self.send_response(400)
self.end_headers()
return
r = handle(msg)
if r is None:
self.send_response(202)
self.end_headers()
return
if msg.get("method") == "tools/call":
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.end_headers()
note = {"jsonrpc": "2.0", "method": "notifications/progress", "params": {"progress": 1}}
self.wfile.write(b"event: message\ndata: " + json.dumps(note).encode() + b"\n\n")
self.wfile.write(b"event: message\ndata: " + json.dumps(r).encode() + b"\n\n")
return
body = json.dumps(r).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
if msg.get("method") == "initialize":
sid = f"s-{len(sessions) + 1}"
sessions.add(sid)
self.send_header("Mcp-Session-Id", sid)
self.end_headers()
self.wfile.write(body)
def do_DELETE(self):
sessions.discard(self.headers.get("Mcp-Session-Id"))
self.send_response(200)
self.end_headers()
httpd = ThreadingHTTPServer(("127.0.0.1", port), H)
threading.Thread(target=httpd.serve_forever, daemon=True).start()
return httpd
if __name__ == "__main__":
if "--page" in sys.argv:
PAGE = int(sys.argv[sys.argv.index("--page") + 1])
if "--crash-at-start" in sys.argv:
sys.stderr.write("cannot start: missing configuration\n")
sys.exit(2)
if "--http" in sys.argv:
server = http_server(int(sys.argv[sys.argv.index("--http") + 1]))
print(f"listening on http://127.0.0.1:{server.server_address[1]}/mcp", flush=True)
threading.Event().wait()
stdio()
+862 -54
View File
File diff suppressed because it is too large. Load diff
+247
View File
@@ -0,0 +1,247 @@
"""serve/telemetry.py - hardware readings for the web app's Monitor tab (idea from PR #22 by code-martin).
A background thread samples once a second and keeps the last 60 readings of each series for the sparklines:
- GPU: NVIDIA's own NVML library (nvml.dll / libnvidia-ml.so.1, installed with every driver) through ctypes, so no
pip package is needed: load, VRAM, temperature, power, PCIe link and throughput.
- CPU, RAM, disk: `psutil` when it is installed (setup installs it); without it the CPU and RAM readings fall back to
the OS (Windows GlobalMemoryStatusEx / GetSystemTimes, Linux /proc) and the disk rate is absent.
Anything that cannot be read is None; nothing here can stop the server.
"""
from __future__ import annotations
import collections
import ctypes
import os
import platform
import sys
import threading
import time
HISTORY = 60
# ------------------------------------------------------------------------------------------------ NVML
class _Nvml:
class Util(ctypes.Structure):
_fields_ = [("gpu", ctypes.c_uint), ("memory", ctypes.c_uint)]
class Mem(ctypes.Structure):
_fields_ = [("total", ctypes.c_ulonglong), ("free", ctypes.c_ulonglong), ("used", ctypes.c_ulonglong)]
def __init__(self, index=0):
self.lib = self.dev = None
names = ["nvml.dll", os.path.join(os.environ.get("ProgramFiles", r"C:\Program Files"),
"NVIDIA Corporation", "NVSMI", "nvml.dll")] if os.name == "nt" \
else ["libnvidia-ml.so.1", "libnvidia-ml.so"]
for n in names:
try:
self.lib = ctypes.CDLL(n)
break
except OSError:
continue
if self.lib is None:
return
try:
init = getattr(self.lib, "nvmlInit_v2", None) or self.lib.nvmlInit
if init() != 0:
self.lib = None
return
h = ctypes.c_void_p()
get = getattr(self.lib, "nvmlDeviceGetHandleByIndex_v2", None) or self.lib.nvmlDeviceGetHandleByIndex
if get(ctypes.c_uint(index), ctypes.byref(h)) != 0:
self.lib = None
return
self.dev = h
except (AttributeError, OSError):
self.lib = None
def ok(self):
return self.lib is not None and self.dev is not None
def _uint(self, fn, *args):
v = ctypes.c_uint()
try:
return v.value if getattr(self.lib, fn)(self.dev, *args, ctypes.byref(v)) == 0 else None
except (AttributeError, OSError):
return None
def name(self):
buf = ctypes.create_string_buffer(96)
try:
if self.lib.nvmlDeviceGetName(self.dev, buf, ctypes.c_uint(96)) == 0:
return buf.value.decode(errors="replace")
except (AttributeError, OSError):
pass
return None
def read(self):
out = {}
u = self.Util()
try:
if self.lib.nvmlDeviceGetUtilizationRates(self.dev, ctypes.byref(u)) == 0:
out["util"] = u.gpu
except (AttributeError, OSError):
pass
m = self.Mem()
try:
if self.lib.nvmlDeviceGetMemoryInfo(self.dev, ctypes.byref(m)) == 0:
out["mem_used"], out["mem_total"] = m.used, m.total
except (AttributeError, OSError):
pass
out["temp"] = self._uint("nvmlDeviceGetTemperature", ctypes.c_uint(0)) # NVML_TEMPERATURE_GPU
mw = self._uint("nvmlDeviceGetPowerUsage")
out["power"] = mw / 1000.0 if mw is not None else None
lim = self._uint("nvmlDeviceGetEnforcedPowerLimit")
out["power_limit"] = lim / 1000.0 if lim is not None else None
out["pcie_gen"] = self._uint("nvmlDeviceGetCurrPcieLinkGeneration") # drops at idle (power saving)
out["pcie_gen_max"] = self._uint("nvmlDeviceGetMaxPcieLinkGeneration")
out["pcie_width"] = self._uint("nvmlDeviceGetCurrPcieLinkWidth")
rx = self._uint("nvmlDeviceGetPcieThroughput", ctypes.c_uint(1)) # NVML_PCIE_UTIL_RX_BYTES, KB/s
tx = self._uint("nvmlDeviceGetPcieThroughput", ctypes.c_uint(0))
out["pcie_rx_mb"] = rx / 1024.0 if rx is not None else None
out["pcie_tx_mb"] = tx / 1024.0 if tx is not None else None
return out
# ------------------------------------------------------------------------------------------------ CPU / RAM
def _cpu_name():
if os.name == "nt":
try:
import winreg
k = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"HARDWARE\DESCRIPTION\System\CentralProcessor\0")
return winreg.QueryValueEx(k, "ProcessorNameString")[0].strip()
except OSError:
pass
elif os.path.exists("/proc/cpuinfo"):
for line in open("/proc/cpuinfo", encoding="utf-8", errors="replace"):
if line.startswith("model name"):
return line.split(":", 1)[1].strip()
return platform.processor() or None
class _CpuRamFallback:
"""CPU load and RAM without psutil."""
def __init__(self):
self.prev = self._times()
def _times(self):
if os.name == "nt":
idle, kern, user = (ctypes.c_ulonglong() for _ in range(3))
if ctypes.windll.kernel32.GetSystemTimes(ctypes.byref(idle), ctypes.byref(kern), ctypes.byref(user)):
return idle.value, kern.value + user.value # kernel time includes idle
return None
try:
f = [int(x) for x in open("/proc/stat").readline().split()[1:]]
return f[3] + f[4], sum(f)
except (OSError, ValueError):
return None
def cpu(self):
cur = self._times()
prev, self.prev = self.prev, cur
if not cur or not prev or cur[1] == prev[1]:
return None
return max(0.0, min(100.0, 100.0 * (1 - (cur[0] - prev[0]) / (cur[1] - prev[1]))))
@staticmethod
def ram():
if os.name == "nt":
class MS(ctypes.Structure):
_fields_ = [("dwLength", ctypes.c_ulong), ("dwMemoryLoad", ctypes.c_ulong),
("ullTotalPhys", ctypes.c_ulonglong), ("ullAvailPhys", ctypes.c_ulonglong),
("ullTotalPageFile", ctypes.c_ulonglong), ("ullAvailPageFile", ctypes.c_ulonglong),
("ullTotalVirtual", ctypes.c_ulonglong), ("ullAvailVirtual", ctypes.c_ulonglong),
("ullAvailExtendedVirtual", ctypes.c_ulonglong)]
m = MS()
m.dwLength = ctypes.sizeof(MS)
if ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(m)):
return m.ullTotalPhys - m.ullAvailPhys, m.ullTotalPhys
return None, None
try:
info = dict(line.split(":", 1) for line in open("/proc/meminfo"))
total = int(info["MemTotal"].split()[0]) * 1024
avail = int(info["MemAvailable"].split()[0]) * 1024
return total - avail, total
except (OSError, KeyError, ValueError):
return None, None
# ------------------------------------------------------------------------------------------------ the sampler
class Telemetry:
def __init__(self, extra=None, gpu_index=0):
"""`extra()` -> dict of more series to record each second (the server's tok/s). `gpu_index`: the card the
engine runs on, numbered as nvidia-smi and NVML number them (by PCI bus)."""
self.extra = extra
self.lock = threading.Lock()
self.now: dict = {}
self.hist = collections.defaultdict(lambda: collections.deque(maxlen=HISTORY))
self.gpu = _Nvml(gpu_index)
try:
import psutil # noqa: F401
self.ps = sys.modules["psutil"]
except ImportError:
self.ps = None
self.fallback = _CpuRamFallback()
self.static = {
"gpu_name": self.gpu.name() if self.gpu.ok() else None,
"cpu_name": _cpu_name(),
"cores": (self.ps.cpu_count(logical=False) if self.ps else None) or None,
"threads": os.cpu_count(),
"psutil": self.ps is not None,
}
self._disk_prev = None
threading.Thread(target=self._loop, daemon=True).start()
def _disk(self):
if not self.ps:
return None, None
try:
c = self.ps.disk_io_counters()
except (OSError, RuntimeError):
return None, None
t = time.time()
prev, self._disk_prev = self._disk_prev, (t, c.read_bytes, c.write_bytes)
if prev is None or t <= prev[0]:
return None, None
dt = t - prev[0]
return (c.read_bytes - prev[1]) / dt / 2**20, (c.write_bytes - prev[2]) / dt / 2**20
def sample(self):
s = {}
if self.gpu.ok():
g = self.gpu.read()
s.update({f"gpu_{k}": v for k, v in g.items()})
if self.ps:
try:
s["cpu"] = self.ps.cpu_percent(interval=None)
vm = self.ps.virtual_memory()
s["ram_used"], s["ram_total"] = vm.total - vm.available, vm.total
except (OSError, RuntimeError):
pass
else:
s["cpu"] = self.fallback.cpu()
s["ram_used"], s["ram_total"] = self.fallback.ram()
s["disk_read_mb"], s["disk_write_mb"] = self._disk()
if self.extra:
try:
s.update(self.extra())
except Exception: # noqa: BLE001 - telemetry must never take the server down
pass
return s
def _loop(self):
while True:
s = self.sample()
with self.lock:
self.now = s
for k in ("gpu_util", "gpu_mem_used", "gpu_temp", "gpu_power", "gpu_pcie_rx_mb", "cpu", "ram_used",
"disk_read_mb", "tok_s"):
v = s.get(k)
self.hist[k].append(round(v, 2) if isinstance(v, float) else v)
time.sleep(1.0)
def snapshot(self):
with self.lock:
return {"now": dict(self.now), "history": {k: list(v) for k, v in self.hist.items()},
"static": dict(self.static)}
+434
View File
@@ -0,0 +1,434 @@
"""serve/test_mcp.py - tools from MCP servers (serve/mcp.py) and the web app's tool loop, against the mock engine and
the fake MCP server in serve/mcp_fake_server.py (no GPU, no pack, no MCP SDK).
python -m unittest serve.test_mcp -v
"""
from __future__ import annotations
import json
import os
import sys
import tempfile
import threading
import time
import unittest
import urllib.error
import urllib.request
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from serve import mcp_fake_server as fake # noqa: E402
from serve.frontend import ChatTemplate # noqa: E402
from serve.mcp import McpCancelled, McpHub, hub_from_config # noqa: E402
from serve.server import ByteTokenizer, MockEngine, Service, serve # noqa: E402
ROOT = Path(__file__).resolve().parents[1]
FAKE = str(ROOT / "serve" / "mcp_fake_server.py")
CTX = 8192
def stdio(*args, **extra):
return {"command": sys.executable, "args": [FAKE, *args], **extra}
def call_script(name, **params):
"""The model's text for one tool call, as Qwen's template asks for it."""
body = "".join(f"<parameter={k}>\n{v}\n</parameter>\n" for k, v in params.items())
return f"</think>\n\nLet me check.\n\n<tool_call>\n<function={name}>\n{body}</function>\n</tool_call>"
class ScriptedEngine(MockEngine):
"""A mock engine with one script per call (the last one repeats): a tool call first, then the answer."""
def __init__(self, tok, scripts, max_context=CTX, delay_s=0.0):
super().__init__(tok, list(scripts), max_context=max_context, delay_s=delay_s)
self.prompts = []
def generate(self, ids, max_new, sampling, cancel, embeddings=None):
self.prompts.append(list(ids))
yield from super().generate(ids, max_new, sampling, cancel, embeddings)
def prompt_text(self, i):
return bytes(t for t in self.prompts[i] if t < 256).decode("utf-8", "replace")
# ------------------------------------------------------------------------------------------------ the client
class Client(unittest.TestCase):
"""The stdio client against the fake server: start, pagination, calls, errors, crashes, timeouts, the cap."""
@classmethod
def setUpClass(cls):
cls.log = tempfile.NamedTemporaryFile(delete=False, suffix=".jsonl")
cls.log.close()
os.environ["FAKE_MCP_LOG"] = cls.log.name
cls.hub = McpHub({"fake": stdio(), "paged": stdio("--page", "4"), "broken": stdio("--crash-at-start"),
"missing": {"command": "strata-no-such-program"}},
{"timeout_s": 1.5, "max_result_chars": 1000})
cls.hub.start(wait=True)
@classmethod
def tearDownClass(cls):
cls.hub.close()
del os.environ["FAKE_MCP_LOG"]
os.unlink(cls.log.name)
def test_initialize_and_list_every_page(self):
s = self.hub.servers["fake"]
self.assertEqual(s.status, "ready")
self.assertEqual(s.info["name"], "fake")
self.assertEqual(s.info["protocol"], "2025-06-18")
want = [t["name"] for t in fake.TOOLS]
self.assertEqual([t["name"] for t in s.tools], want) # 3 pages of 2
self.assertEqual([t["name"] for t in self.hub.servers["paged"].tools], want) # 2 pages of 4
def test_namespaced_openai_tools(self):
tools = self.hub.openai_tools()
names = [t["function"]["name"] for t in tools]
self.assertIn("fake__echo", names)
self.assertIn("paged__echo", names)
echo = next(t for t in tools if t["function"]["name"] == "fake__echo")
self.assertEqual(echo["type"], "function")
self.assertEqual(echo["function"]["parameters"]["properties"]["text"]["type"], "string")
def test_a_server_that_fails_is_left_out(self):
for name, why in (("broken", "missing configuration"), ("missing", "could not start")):
with self.subTest(name=name):
s = self.hub.servers[name]
self.assertEqual(s.status, "failed")
self.assertIn(why, s.error)
self.assertFalse([t for t in self.hub.template_tools() if t["name"].startswith(name + "__")])
st = {s["name"]: s for s in self.hub.status()["servers"]}
self.assertEqual(st["broken"]["status"], "failed")
self.assertEqual(st["fake"]["status"], "ready")
def test_call(self):
self.hub.routes()
r = self.hub.call("fake__echo", {"text": "hello wörld"})
self.assertEqual((r["ok"], r["text"], r["server"], r["tool"]), (True, "hello wörld", "fake", "echo"))
self.assertEqual(self.hub.call("fake__add", {"a": 2, "b": 40})["text"], "42")
def test_errors_become_text(self):
self.hub.routes()
r = self.hub.call("fake__fail", {}) # the tool's own error (isError)
self.assertFalse(r["ok"])
self.assertEqual(r["text"], "error: it failed on purpose")
r = self.hub.call("nobody__nothing", {}) # not a tool at all
self.assertTrue(r["text"].startswith("error: there is no tool"))
# a JSON-RPC error from the server
s = self.hub.servers["fake"]
with self.assertRaises(Exception) as ctx:
s.call("no-such-tool", {}, 5)
self.assertIn("unknown tool", str(ctx.exception))
def test_timeout(self):
self.hub.routes()
t0 = time.monotonic()
r = self.hub.call("fake__sleep", {"seconds": 5})
self.assertLess(time.monotonic() - t0, 4)
self.assertFalse(r["ok"])
self.assertIn("timed out after 1.5 s", r["text"])
self.assertEqual(self.hub.call("fake__echo", {"text": "still here"})["text"], "still here")
def test_truncation(self):
self.hub.routes()
r = self.hub.call("fake__big", {"n": 50000})
self.assertTrue(r["ok"])
self.assertTrue(r["truncated"])
self.assertEqual(r["chars"], 50000)
self.assertTrue(r["text"].startswith("y" * 1000 + "\n\n[... truncated"))
self.assertIn("50,000 characters", r["text"])
self.assertLess(len(r["text"]), 1200)
def test_crash_mid_call_then_restart(self):
hub = McpHub({"solo": stdio()}, {"timeout_s": 5})
hub.start(wait=True)
try:
hub.routes()
r = hub.call("solo__die", {})
self.assertFalse(r["ok"])
self.assertIn("the server stopped", r["text"])
self.assertIn("dying on purpose", r["text"])
self.assertEqual(hub.servers["solo"].status, "stopped")
self.assertIn("solo__echo", hub.routes()) # its tools stay: the next call restarts it
self.assertEqual(hub.call("solo__echo", {"text": "back"})["text"], "back")
self.assertEqual(hub.servers["solo"].status, "ready")
finally:
hub.close()
def test_cancel(self):
self.hub.routes()
cancel = threading.Event()
threading.Timer(0.3, cancel.set).start()
t0 = time.monotonic()
with self.assertRaises(McpCancelled):
self.hub.call("fake__sleep", {"seconds": 1.2}, cancel)
self.assertLess(time.monotonic() - t0, 1.0)
time.sleep(0.3)
seen = [json.loads(line) for line in Path(self.log.name).read_text().splitlines()]
self.assertTrue(any("cancelled" in e for e in seen), seen) # notifications/cancelled reached the server
class HttpClient(unittest.TestCase):
"""Streamable HTTP: JSON answers, an event-stream answer, the session id."""
@classmethod
def setUpClass(cls):
cls.httpd = fake.http_server(0)
url = f"http://127.0.0.1:{cls.httpd.server_address[1]}/mcp"
cls.hub = McpHub({"web": {"url": url}, "down": {"url": "http://127.0.0.1:9/mcp"}}, {"timeout_s": 1.5})
cls.hub.start(wait=True)
@classmethod
def tearDownClass(cls):
cls.hub.close()
cls.httpd.shutdown()
cls.httpd.server_close()
def test_list_and_call(self):
s = self.hub.servers["web"]
self.assertEqual(s.status, "ready", s.error)
self.assertEqual(len(s.tools), len(fake.TOOLS)) # pages, with the session id on each request
self.assertEqual(s.transport.session, "s-1")
self.hub.routes()
self.assertEqual(self.hub.call("web__echo", {"text": "over http"})["text"], "over http") # event stream
self.assertEqual(self.hub.call("web__fail", {})["text"], "error: it failed on purpose")
def test_timeout_and_unreachable(self):
self.hub.routes()
r = self.hub.call("web__sleep", {"seconds": 4})
self.assertIn("timed out", r["text"])
self.assertEqual(self.hub.servers["down"].status, "failed")
self.assertIn("could not reach", self.hub.servers["down"].error)
class Config(unittest.TestCase):
def test_both_spellings_and_the_file(self):
with tempfile.TemporaryDirectory() as d:
path = os.path.join(d, "claude_desktop_config.json")
Path(path).write_text(json.dumps({"mcpServers": {"b": {"url": "http://x/mcp"}, "a": stdio()}}),
encoding="utf-8")
hub = hub_from_config({"mcp_servers": {"a": {"command": "old"}},
"mcpServers": {"c": stdio(), "off": {"command": "x", "disabled": True}},
"mcp": {"timeout_s": 5, "max_result_chars": 100, "max_rounds": 3}}, path)
self.assertEqual(sorted(hub.servers), ["a", "b", "c"])
self.assertEqual(hub.servers["a"].cfg["command"], sys.executable) # the file wins a name clash
self.assertEqual(hub.servers["b"].kind, "http")
self.assertEqual((hub.settings["timeout_s"], hub.settings["max_result_chars"], hub.settings["max_rounds"]),
(5.0, 100, 3))
self.assertIsNone(hub_from_config({}))
self.assertEqual(McpHub({}).settings["max_result_chars"], 20000)
def test_bad_entries_stop_the_start(self):
for cfg in ({"mcp_servers": {"x": {}}}, {"mcp_servers": {"x": {"command": "a", "args": "b"}}},
{"mcp_servers": ["x"]}, {"mcpServers": {"x": {"url": "http://a", "type": "sse"}}},
{"mcp_servers": {"x": {"command": "a"}}, "mcp": {"timeout_s": 0}},
{"mcp_servers": {"x": {"command": "a"}}, "mcp": {"max_rounds": 1.5}}):
with self.subTest(cfg=cfg), self.assertRaises(SystemExit):
hub_from_config(cfg)
with self.assertRaises(SystemExit):
hub_from_config({}, os.path.join(tempfile.gettempdir(), "strata-no-such-mcp.json"))
# ------------------------------------------------------------------------------------------------ the tool loop
class ToolLoop(unittest.TestCase):
"""The web app's chat (`"strata_mcp": true`): the model calls an MCP tool, the server runs it and the model
answers with its result; plain API requests never see the MCP tools."""
def setUp(self):
self.log = tempfile.NamedTemporaryFile(delete=False, suffix=".jsonl")
self.log.close()
os.environ["FAKE_MCP_LOG"] = self.log.name
self.hub = McpHub({"fake": stdio(), "broken": stdio("--crash-at-start")},
{"timeout_s": 10, "max_result_chars": 500, "max_rounds": 2})
self.hub.start(wait=True)
self.httpd = None
def tearDown(self):
if self.httpd:
self.httpd.shutdown()
self.httpd.server_close()
self.hub.close()
del os.environ["FAKE_MCP_LOG"]
os.unlink(self.log.name)
def start(self, *scripts, delay_s=0.0):
tok = ByteTokenizer()
self.engine = ScriptedEngine(tok, list(scripts), delay_s=delay_s)
self.svc = Service(self.engine, tok, ChatTemplate(ROOT / "serve/chat_template.jinja"))
self.svc.mcp = self.hub
self.httpd = serve(self.svc, port=0)
self.base = f"http://127.0.0.1:{self.httpd.server_address[1]}"
def post(self, body, headers=None, stream=True):
body = {"model": "m", "messages": [{"role": "user", "content": "check it"}], "stream": stream, **body}
req = urllib.request.Request(self.base + "/v1/chat/completions", data=json.dumps(body).encode(),
headers={"Content-Type": "application/json", **(headers or {})})
try:
with urllib.request.urlopen(req, timeout=30) as r:
text = r.read().decode()
return r.status, text
except urllib.error.HTTPError as e:
with e:
return e.code, e.read().decode()
@staticmethod
def chunks(text):
return [json.loads(line[6:]) for line in text.splitlines() if line.startswith("data: {")]
def test_tool_call_then_answer(self):
self.start(call_script("fake__echo", text="hello from the tool"), "</think>\n\nThe tool said hello.")
code, text = self.post({"strata_mcp": True})
self.assertEqual(code, 200, text)
cs = self.chunks(text)
mcp = [c["strata_mcp"] for c in cs if "strata_mcp" in c]
self.assertEqual([m["event"] for m in mcp], ["start", "call", "result"])
self.assertEqual(mcp[1]["arguments"], {"text": "hello from the tool"})
self.assertEqual((mcp[1]["server"], mcp[1]["tool"]), ("fake", "echo"))
self.assertEqual((mcp[2]["ok"], mcp[2]["text"]), (True, "hello from the tool"))
self.assertEqual(len({m["id"] for m in mcp}), 1)
content = "".join((c["choices"][0]["delta"].get("content") or "") for c in cs)
self.assertEqual(content, "Let me check.The tool said hello.")
self.assertFalse([c for c in cs if c["choices"][0]["delta"].get("tool_calls")]) # nothing for the client to run
self.assertEqual(cs[-1]["choices"][0]["finish_reason"], "stop")
self.assertEqual(len(self.engine.prompts), 2)
first, second = self.engine.prompt_text(0), self.engine.prompt_text(1)
self.assertIn('"name": "fake__echo"', first) # the MCP tools are in the prompt
self.assertNotIn("broken__", first) # the server that failed is left out
self.assertIn("<function=fake__echo>\n<parameter=text>\nhello from the tool\n</parameter>", second)
self.assertIn("<tool_response>\nhello from the tool\n</tool_response>", second)
self.assertEqual(cs[-1]["usage"]["prompt_tokens"], len(self.engine.prompts[1]))
def test_non_stream(self):
self.start(call_script("fake__add", a=2, b=3), "</think>\n\n5.")
code, text = self.post({"strata_mcp": True}, stream=False)
self.assertEqual(code, 200, text)
msg = json.loads(text)["choices"][0]["message"]
self.assertEqual(msg["content"], "Let me check.5.")
self.assertEqual(msg["strata_mcp"][-1]["text"], "5")
def test_plain_requests_get_no_mcp_tools(self):
self.start(call_script("fake__echo", text="x"), "</think>\n\nnever")
code, text = self.post({})
self.assertEqual(code, 200, text)
self.assertNotIn("fake__echo", self.engine.prompt_text(0))
cs = self.chunks(text)
self.assertFalse([c for c in cs if "strata_mcp" in c])
self.assertEqual(cs[-1]["choices"][0]["finish_reason"], "tool_calls") # returned to the client as always
self.assertEqual(len(self.engine.prompts), 1)
self.assertNotIn('"call"', Path(self.log.name).read_text()) # nothing ran
for req in ({"strata_mcp": "yes"}, {"strata_mcp": 1}): # only a real true opts in
self.post(req)
self.assertNotIn("fake__echo", self.engine.prompt_text(-1))
def test_own_tools_still_go_to_the_client(self):
own = [{"type": "function", "function": {"name": "lookup", "parameters": {"type": "object", "properties": {
"q": {"type": "string"}}}}}]
self.start(call_script("lookup", q="x"), "</think>\n\nnever")
code, text = self.post({"strata_mcp": True, "tools": own})
cs = self.chunks(text)
calls = [tc for c in cs for tc in c["choices"][0]["delta"].get("tool_calls") or []]
self.assertEqual(calls[0]["function"]["name"], "lookup")
self.assertEqual(cs[-1]["choices"][0]["finish_reason"], "tool_calls")
self.assertIn('"name": "lookup"', self.engine.prompt_text(0))
self.assertIn('"name": "fake__echo"', self.engine.prompt_text(0))
self.assertEqual(len(self.engine.prompts), 1)
def test_tool_error_is_a_result(self):
self.start(call_script("fake__fail"), "</think>\n\nIt failed.")
code, text = self.post({"strata_mcp": True})
mcp = [c["strata_mcp"] for c in self.chunks(text) if "strata_mcp" in c]
self.assertEqual((mcp[-1]["ok"], mcp[-1]["text"]), (False, "error: it failed on purpose"))
self.assertIn("<tool_response>\nerror: it failed on purpose\n</tool_response>", self.engine.prompt_text(1))
self.assertEqual(self.chunks(text)[-1]["choices"][0]["finish_reason"], "stop")
def test_truncated_for_the_model(self):
self.start(call_script("fake__big", n=3000), "</think>\n\nLong.")
self.post({"strata_mcp": True})
second = self.engine.prompt_text(1)
self.assertIn("y" * 500 + "\n\n[... truncated: the tool returned 3,000 characters", second)
self.assertNotIn("y" * 501, second)
def test_max_rounds(self):
self.start(call_script("fake__echo", text="again")) # the model never stops calling
code, text = self.post({"strata_mcp": True})
mcp = [c["strata_mcp"] for c in self.chunks(text) if "strata_mcp" in c]
self.assertEqual(len(self.engine.prompts), 3) # 2 rounds of tools, then the limit
self.assertEqual(sum(m["event"] == "call" for m in mcp), 2)
self.assertIn({"event": "limit", "max_rounds": 2}, mcp)
self.assertTrue(mcp[-1].get("skipped"))
self.assertEqual(self.chunks(text)[-1]["choices"][0]["finish_reason"], "stop")
def test_stop_during_a_tool(self):
"""Closing the connection while a slow tool runs stops the tool (notifications/cancelled) and the loop."""
self.start(call_script("fake__sleep", seconds=8), "</think>\n\nnever")
body = {"model": "m", "messages": [{"role": "user", "content": "x"}], "stream": True, "strata_mcp": True}
req = urllib.request.Request(self.base + "/v1/chat/completions", data=json.dumps(body).encode(),
headers={"Content-Type": "application/json"})
r = urllib.request.urlopen(req, timeout=30)
for raw in r:
if b'"event": "call"' in raw:
break
r.close() # the web app's Stop aborts the fetch
t0 = time.monotonic()
while time.monotonic() - t0 < 6 and "cancelled" not in Path(self.log.name).read_text():
time.sleep(0.1)
self.assertIn("cancelled", Path(self.log.name).read_text())
self.assertLess(time.monotonic() - t0, 6)
time.sleep(0.5)
self.assertEqual(len(self.engine.prompts), 1) # no second round
self.assertFalse(self.svc.status["busy"])
def test_only_from_the_app_s_own_page(self):
self.start("</think>\n\nhi")
req = urllib.request.Request(self.base + "/v1/chat/completions", data=json.dumps(
{"model": "m", "messages": [{"role": "user", "content": "x"}], "strata_mcp": True}).encode(),
headers={"Content-Type": "text/plain"}) # a cross-site "simple" request
with self.assertRaises(urllib.error.HTTPError) as ctx:
urllib.request.urlopen(req, timeout=10)
self.assertEqual(ctx.exception.code, 415)
code, _ = self.post({"strata_mcp": True}, {"Origin": "http://evil.example"})
self.assertEqual(code, 403)
self.assertEqual(self.engine.prompts, [])
host = self.base.split("://", 1)[1]
code, _ = self.post({"strata_mcp": True}, {"Origin": "http://" + host})
self.assertEqual(code, 200)
def test_get_mcp(self):
self.start("</think>\n\nhi")
with urllib.request.urlopen(self.base + "/mcp", timeout=10) as r:
st = json.loads(r.read())
servers = {s["name"]: s for s in st["servers"]}
self.assertEqual(servers["fake"]["status"], "ready")
self.assertEqual(servers["fake"]["transport"], "stdio")
self.assertIn("fake__echo", [t["name"] for t in servers["fake"]["tools"]])
self.assertEqual(servers["broken"]["status"], "failed")
self.assertIn("missing configuration", servers["broken"]["error"])
self.assertEqual(st["tools"], len(fake.TOOLS))
self.assertEqual(st["settings"]["max_rounds"], 2)
self.svc.api_key = "k"
with self.assertRaises(urllib.error.HTTPError):
urllib.request.urlopen(self.base + "/mcp", timeout=10)
self.svc.api_key = ""
class NoServers(unittest.TestCase):
def test_opt_in_without_servers_is_a_plain_chat(self):
tok = ByteTokenizer()
svc = Service(ScriptedEngine(tok, ["</think>\n\nhi"]), tok, ChatTemplate(ROOT / "serve/chat_template.jinja"))
httpd = serve(svc, port=0)
try:
base = f"http://127.0.0.1:{httpd.server_address[1]}"
with urllib.request.urlopen(base + "/mcp", timeout=10) as r:
self.assertEqual(json.loads(r.read()), {"servers": [], "tools": 0})
req = urllib.request.Request(base + "/v1/chat/completions", data=json.dumps(
{"model": "m", "messages": [{"role": "user", "content": "x"}], "strata_mcp": True}).encode(),
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=10) as r:
self.assertEqual(json.loads(r.read())["choices"][0]["message"]["content"], "hi")
finally:
httpd.shutdown()
httpd.server_close()
if __name__ == "__main__":
unittest.main()
+530
View File
@@ -0,0 +1,530 @@
"""serve/test_server.py - the max tokens budget over both APIs, against the mock engine (no GPU, no pack).
python -m unittest serve.test_server -v
"""
from __future__ import annotations
import json
import os
import sys
import unittest
import urllib.error
import urllib.request
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from serve.frontend import ChatTemplate # noqa: E402
from serve.server import CTX_SLACK, ByteTokenizer, EngineDied, MockEngine, Service, StrataEngine, serve # noqa: E402
ROOT = Path(__file__).resolve().parents[1]
CTX = 4096
ANSWER = "x" * 2000 # longer than the old 1024 fallback: one token per byte
class RecordingEngine(MockEngine):
def generate(self, ids, max_new, sampling, cancel, embeddings=None):
self.last_max_new = max_new
yield from super().generate(ids, max_new, sampling, cancel, embeddings)
class MaxTokens(unittest.TestCase):
@classmethod
def setUpClass(cls):
tok = ByteTokenizer()
cls.engine = RecordingEngine(tok, "</think>\n\n" + ANSWER, max_context=CTX)
cls.svc = Service(cls.engine, tok, ChatTemplate(ROOT / "serve/chat_template.jinja"))
cls.httpd = serve(cls.svc, port=0)
cls.base = f"http://127.0.0.1:{cls.httpd.server_address[1]}"
@classmethod
def tearDownClass(cls):
cls.httpd.shutdown()
cls.httpd.server_close()
def post(self, path, body):
req = urllib.request.Request(self.base + path, data=json.dumps(body).encode(),
headers={"Content-Type": "application/json"})
try:
with urllib.request.urlopen(req, timeout=30) as r:
return r.status, json.loads(r.read())
except urllib.error.HTTPError as e:
with e:
return e.code, json.loads(e.read())
def call(self, api, text="hi", **budget):
"""-> (status, body, prompt tokens, completion tokens); `budget` is merged into the request as given."""
msgs = [{"role": "user", "content": text}]
if api == "openai":
s, b = self.post("/v1/chat/completions", {"model": "m", "messages": msgs, **budget})
u = b.get("usage", {})
return s, b, u.get("prompt_tokens"), u.get("completion_tokens")
s, b = self.post("/v1/messages", {"model": "m", "messages": msgs, **budget})
u = b.get("usage", {})
return s, b, u.get("input_tokens"), u.get("output_tokens")
def test_unset_budget_is_the_rest_of_the_context(self):
cases = {"openai": [{"max_tokens": -1}, {"max_tokens": 0}, {}, {"max_tokens": None},
{"max_completion_tokens": -1}, {"max_completion_tokens": None, "max_tokens": None}],
"anthropic": [{"max_tokens": -1}, {"max_tokens": 0}, {}, {"max_tokens": None}]}
for api, budgets in cases.items():
for budget in budgets:
with self.subTest(api=api, budget=budget):
s, b, pt, ct = self.call(api, **budget)
self.assertEqual(s, 200, b)
self.assertEqual(self.engine.last_max_new, CTX - CTX_SLACK - pt)
self.assertGreater(ct, 1024) # the whole answer, not cut at the old 1024 fallback
def test_explicit_budget_is_honoured(self):
for api, budget in [("openai", {"max_tokens": 50}), ("openai", {"max_completion_tokens": 50}),
("openai", {"max_completion_tokens": 50, "max_tokens": 9}),
("anthropic", {"max_tokens": 50}), ("openai", {"max_tokens": 1500}),
("anthropic", {"max_tokens": 1500})]:
with self.subTest(api=api, budget=budget):
want = budget.get("max_completion_tokens") or budget["max_tokens"]
s, b, _, ct = self.call(api, **budget)
self.assertEqual(s, 200, b)
self.assertEqual(self.engine.last_max_new, want)
self.assertEqual(ct, want)
def test_explicit_budget_over_the_context_is_rejected(self):
for api in ("openai", "anthropic"):
with self.subTest(api=api):
s, b, _, _ = self.call(api, max_tokens=CTX)
self.assertEqual(s, 400)
self.assertIn("exceeds the context", b["error"]["message"])
def test_unset_budget_with_a_near_full_prompt(self):
_, _, pt0, _ = self.call("openai", max_tokens=1)
overhead = pt0 - len("hi") # the template's tokens around the user text
for api in ("openai", "anthropic"):
_, _, pa, _ = self.call(api, max_tokens=1)
over = pa - pt0 # the Anthropic template may differ slightly
with self.subTest(api=api, room=5): # a few tokens left: the budget is exactly those
text = "y" * (CTX - CTX_SLACK - overhead - over - 5)
s, b, pt, ct = self.call(api, text=text, max_tokens=-1)
self.assertEqual(s, 200, b)
self.assertEqual(self.engine.last_max_new, 5)
self.assertEqual(ct, 5)
with self.subTest(api=api, room=0): # nothing left: rejected, not truncated
text = "y" * (CTX - CTX_SLACK - overhead - over)
s, b, _, _ = self.call(api, text=text)
self.assertEqual(s, 400, b)
self.assertIn("no room to answer", b["error"]["message"])
def test_debug_log_shows_the_resolved_budget(self):
import contextlib
import io
os.environ["STRATA_DEBUG"] = "1"
try:
for api in ("openai", "anthropic"):
with self.subTest(api=api):
out = io.StringIO()
with contextlib.redirect_stdout(out):
_, _, pt, _ = self.call(api, max_tokens=-1)
self.assertIn(f"max_new={CTX - CTX_SLACK - pt} ", out.getvalue())
finally:
del os.environ["STRATA_DEBUG"]
class FitMaxTokens(unittest.TestCase):
"""PR #24: --fit-max-tokens clamps an explicit budget that overshoots the context instead of a 400."""
@classmethod
def setUpClass(cls):
tok = ByteTokenizer()
cls.engine = RecordingEngine(tok, "</think>\n\n" + ANSWER, max_context=CTX)
cls.svc = Service(cls.engine, tok, ChatTemplate(ROOT / "serve/chat_template.jinja"), fit_max_tokens=True)
cls.httpd = serve(cls.svc, port=0)
cls.base = f"http://127.0.0.1:{cls.httpd.server_address[1]}"
@classmethod
def tearDownClass(cls):
cls.httpd.shutdown()
cls.httpd.server_close()
post = MaxTokens.post
call = MaxTokens.call
def test_overshoot_is_clamped_to_the_room(self):
for api in ("openai", "anthropic"):
with self.subTest(api=api):
s, b, pt, ct = self.call(api, max_tokens=CTX)
self.assertEqual(s, 200, b)
self.assertEqual(self.engine.last_max_new, CTX - CTX_SLACK - pt)
def test_a_budget_that_fits_is_unchanged(self):
s, b, _, ct = self.call("openai", max_tokens=50)
self.assertEqual(s, 200, b)
self.assertEqual(self.engine.last_max_new, 50)
def test_no_room_is_still_a_400(self):
_, _, pt0, _ = self.call("openai", max_tokens=1)
overhead = pt0 - len("hi")
s, b, _, _ = self.call("openai", text="y" * (CTX - CTX_SLACK - overhead), max_tokens=100)
self.assertEqual(s, 400, b)
self.assertIn("no room to answer", b["error"]["message"])
class ClientShapes(unittest.TestCase):
"""What real clients send: Claude Code posts /v1/messages?beta=true (issue #55) and puts hook context into the
conversation as a mid-conversation system message (issue #56); some OpenAI clients send a late developer message."""
@classmethod
def setUpClass(cls):
tok = ByteTokenizer()
cls.engine = RecordingPrompt(tok, "</think>\n\n2", max_context=CTX)
cls.svc = Service(cls.engine, tok, ChatTemplate(ROOT / "serve/chat_template.jinja"))
cls.httpd = serve(cls.svc, port=0)
cls.base = f"http://127.0.0.1:{cls.httpd.server_address[1]}"
@classmethod
def tearDownClass(cls):
cls.httpd.shutdown()
cls.httpd.server_close()
def post(self, path, body):
req = urllib.request.Request(self.base + path, data=json.dumps(body).encode(),
headers={"Content-Type": "application/json", "anthropic-version": "2023-06-01"})
try:
with urllib.request.urlopen(req, timeout=30) as r:
return r.status, json.loads(r.read())
except urllib.error.HTTPError as e:
with e:
return e.code, json.loads(e.read())
def prompt_text(self):
return bytes(i for i in self.engine.last_ids if i < 256).decode("utf-8", "replace")
def test_query_string(self):
body = {"model": "x", "max_tokens": 20, "messages": [{"role": "user", "content": "hi"}]}
for path in ("/v1/messages?beta=true", "/v1/chat/completions?api-version=1", "/v1/messages/?beta=true"):
status, b = self.post(path, body)
self.assertEqual(status, 200, (path, b))
status, _ = self.post("/v1/nothing?beta=true", body)
self.assertEqual(status, 404)
def test_anthropic_mid_conversation_system(self):
status, b = self.post("/v1/messages?beta=true", {
"model": "x", "max_tokens": 50,
"system": [{"type": "text", "text": "You are terse."}],
"messages": [
{"role": "user", "content": [{"type": "text", "text": "1+1? digits only"}]},
{"role": "system", "content": [{"type": "text", "text": "<system-reminder>answer in digits</system-reminder>"}]}]})
self.assertEqual(status, 200, b)
text = self.prompt_text()
self.assertIn("You are terse.", text)
self.assertIn("<system-reminder>answer in digits</system-reminder>", text)
self.assertLess(text.index("You are terse."), text.index("1+1?")) # the first system stays first
self.assertLess(text.index("1+1?"), text.index("answer in digits")) # the late one stays in place
def test_openai_late_developer_and_system(self):
status, b = self.post("/v1/chat/completions", {
"model": "x", "max_tokens": 50,
"messages": [{"role": "system", "content": "Be brief."}, {"role": "user", "content": "hello"},
{"role": "assistant", "content": "hi"}, {"role": "developer", "content": "Now use digits."},
{"role": "system", "content": "Also this."}, {"role": "user", "content": "1+1?"}]})
self.assertEqual(status, 200, b)
text = self.prompt_text()
for part in ("Be brief.", "Now use digits.", "Also this.", "1+1?"):
self.assertIn(part, text)
def test_leading_system_unchanged(self):
from serve.frontend import anthropic_to_messages, openai_to_messages
msgs, _, _ = openai_to_messages({"messages": [{"role": "developer", "content": "D"}, {"role": "user", "content": "u"}]})
self.assertEqual([m["role"] for m in msgs], ["system", "user"])
msgs, _, _ = anthropic_to_messages({"system": "S", "messages": [{"role": "user", "content": "u"}]})
self.assertEqual([m["role"] for m in msgs], ["system", "user"])
class SamplingKeys(unittest.TestCase):
"""The GEN line's sampling keys: top_k 0 ("off") or wider than the engine's 64 get the widest list, 64 (they used
to fall back to the engine default 20); a penalty always carries its window."""
def keys(self, **sampling):
return StrataEngine.sampling_keys(sampling).split()
def test_top_k(self):
self.assertIn("top_k=10", self.keys(temperature=0.7, top_k=10))
self.assertIn("top_k=64", self.keys(temperature=0.7, top_k=64))
self.assertIn("top_k=64", self.keys(temperature=0.7, top_k=0))
self.assertIn("top_k=64", self.keys(temperature=0.7, top_k=100))
for bad in (-1, True, 2.5, "20"):
self.assertFalse([k for k in self.keys(temperature=0.7, top_k=bad) if k.startswith("top_k=")], bad)
def test_tune_keys(self):
k = self.keys(temperature=0, strata_tune={"pcie_frac": 0.2, "spec_min_p": 0.7})
self.assertIn("pcie_frac=0.2", k)
self.assertIn("spec_min_p=0.7", k)
bad = self.keys(strata_tune={"pcie_frac": 3, "spec_min_p": True, "pool_workers": 2})
self.assertFalse([x for x in bad if x.split("=")[0] in ("pcie_frac", "spec_min_p", "pool_workers")])
def test_penalty_window(self):
self.assertIn("penalty_last_n=64", self.keys(presence_penalty=1.5))
self.assertIn("penalty_last_n=4096", self.keys(repetition_penalty=1.1, penalty_last_n=4096))
self.assertFalse([k for k in self.keys(temperature=0.7) if k.startswith("penalty")])
class GpuChoice(unittest.TestCase):
"""Issue #51: the config's \"gpu\" reaches the engine as CUDA_VISIBLE_DEVICES, numbered like nvidia-smi."""
def test_env(self):
from serve.server import child_env
env = child_env({"gpu": 1})
self.assertEqual(env["CUDA_VISIBLE_DEVICES"], "1")
self.assertEqual(env["CUDA_DEVICE_ORDER"], "PCI_BUS_ID")
plain = child_env({}) # no choice: the environment as it was (existing installs)
self.assertEqual(plain.get("CUDA_VISIBLE_DEVICES"), os.environ.get("CUDA_VISIBLE_DEVICES"))
self.assertEqual(plain.get("CUDA_DEVICE_ORDER"), os.environ.get("CUDA_DEVICE_ORDER"))
class RecordingPrompt(MockEngine):
def generate(self, ids, max_new, sampling, cancel, embeddings=None):
self.last_ids = list(ids)
yield from super().generate(ids, max_new, sampling, cancel, embeddings)
class DyingEngine(MockEngine):
"""Issue #27: an engine that dies after a few tokens of its first answer, and comes back when restarted."""
def __init__(self, tok, script, max_context):
super().__init__(tok, script, max_context=max_context)
self.dead, self.restarts, self.die_after = False, 0, 5
def alive(self):
return not self.dead
def restart(self):
self.dead, self.die_after = False, None
self.restarts += 1
def generate(self, ids, max_new, sampling, cancel, embeddings=None):
for i, t in enumerate(super().generate(ids, max_new, sampling, cancel, embeddings)):
if self.die_after is not None and i == self.die_after:
self.dead = True
raise EngineDied("the engine stopped unexpectedly (exit code -9)")
yield t
class EngineDeath(unittest.TestCase):
"""Issue #27: a dead engine is an error (not "length"), and the next request starts it again."""
def test_error_then_restart(self):
tok = ByteTokenizer()
eng = DyingEngine(tok, "</think>\n\n" + ANSWER, max_context=CTX)
svc = Service(eng, tok, ChatTemplate(ROOT / "serve/chat_template.jinja"))
httpd = serve(svc, port=0)
base = f"http://127.0.0.1:{httpd.server_address[1]}"
try:
def post(body):
req = urllib.request.Request(base + "/v1/chat/completions", data=json.dumps(body).encode(),
headers={"Content-Type": "application/json"})
try:
with urllib.request.urlopen(req, timeout=30) as r:
return r.status, r.read().decode()
except urllib.error.HTTPError as e:
with e:
return e.code, e.read().decode()
msgs = [{"role": "user", "content": "hi"}]
code, text = post({"model": "m", "messages": msgs, "max_tokens": 50, "stream": True})
self.assertEqual(code, 200)
self.assertIn('"error"', text)
self.assertIn("stopped unexpectedly", text)
self.assertTrue(text.rstrip().endswith("data: [DONE]"))
self.assertEqual(svc.metrics()["requests"][0]["finish"], "error")
code, text = post({"model": "m", "messages": msgs, "max_tokens": 50})
self.assertEqual(code, 200, text)
self.assertEqual(eng.restarts, 1)
self.assertEqual(json.loads(text)["usage"]["completion_tokens"], 50)
finally:
httpd.shutdown()
httpd.server_close()
def test_engine_err_mid_stream(self):
"""The engine's ERR line after the stream started reaches the client as an error event (it used to be a
400 written into the open stream, which clients read as an empty answer)."""
class ErrEngine(MockEngine):
def generate(self, ids, max_new, sampling, cancel, embeddings=None):
yield None # a prompt-progress heartbeat: the stream has started
raise ValueError("verify: layer 31 never rang (an illegal memory access was encountered)")
tok = ByteTokenizer()
svc = Service(ErrEngine(tok, ANSWER, max_context=CTX), tok, ChatTemplate(ROOT / "serve/chat_template.jinja"))
httpd = serve(svc, port=0)
base = f"http://127.0.0.1:{httpd.server_address[1]}"
try:
for path, body in [("/v1/chat/completions", {"model": "m", "stream": True, "max_tokens": 20,
"messages": [{"role": "user", "content": "hi"}]}),
("/v1/messages", {"model": "m", "stream": True, "max_tokens": 20,
"messages": [{"role": "user", "content": "hi"}]})]:
req = urllib.request.Request(base + path, data=json.dumps(body).encode(),
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=30) as r:
text = r.read().decode()
self.assertIn("illegal memory access", text, path)
self.assertNotIn("HTTP/1", text, path)
self.assertEqual(svc.metrics()["requests"][0]["finish"], "error")
finally:
httpd.shutdown()
httpd.server_close()
class SharedSettings(unittest.TestCase):
"""The web app's "Use for other apps too": POST /settings makes its Chat settings every client's defaults."""
@classmethod
def setUpClass(cls):
import tempfile
class Sampled(RecordingEngine):
def generate(self, ids, max_new, sampling, cancel, embeddings=None):
self.last_sampling = dict(sampling or {})
yield from super().generate(ids, max_new, sampling, cancel, embeddings)
tok = ByteTokenizer()
cls.engine = Sampled(tok, "</think>\n\nhello", max_context=CTX)
cls.svc = Service(cls.engine, tok, ChatTemplate(ROOT / "serve/chat_template.jinja"))
cls.tmp = tempfile.TemporaryDirectory()
cls.svc.shared_path = os.path.join(cls.tmp.name, "strata-x.shared-settings.json")
cls.httpd = serve(cls.svc, port=0)
cls.base = f"http://127.0.0.1:{cls.httpd.server_address[1]}"
@classmethod
def tearDownClass(cls):
cls.httpd.shutdown()
cls.httpd.server_close()
cls.tmp.cleanup()
def req(self, path, body, headers=None, raw=None):
h = {"Content-Type": "application/json", **(headers or {})}
r = urllib.request.Request(self.base + path, data=raw if raw is not None else json.dumps(body).encode(), headers=h)
try:
with urllib.request.urlopen(r, timeout=30) as resp:
return resp.status, json.loads(resp.read())
except urllib.error.HTTPError as e:
with e:
return e.code, json.loads(e.read())
def chat(self, **extra):
return self.req("/v1/chat/completions", {"model": "m", "messages": [{"role": "user", "content": "hi"}], **extra})
def tearDown(self):
self.svc.set_shared(None)
def test_other_apps_get_the_chat_settings(self):
d = {"temperature": 0.3, "top_p": 0.9, "top_k": 10, "seed": 7, "max_tokens": 77,
"reasoning_effort": "low", "experimental_speed_projection": False}
code, b = self.req("/settings", {"defaults": d})
self.assertEqual(code, 200, b)
self.assertTrue(b["shared"])
self.assertTrue(os.path.exists(self.svc.shared_path))
code, _ = self.chat() # a client that sets nothing
self.assertEqual(code, 200)
got = self.engine.last_sampling
for k in ("temperature", "top_p", "top_k", "seed", "experimental_speed_projection"):
self.assertEqual(got[k], d[k], k)
self.assertEqual(self.engine.last_max_new, 77)
code, _ = self.chat(temperature=0.9, max_tokens=5) # its own values win
self.assertEqual(self.engine.last_sampling["temperature"], 0.9)
self.assertEqual(self.engine.last_max_new, 5)
r = self.svc.with_shared({"messages": []}, "openai")
self.assertEqual(r["reasoning_effort"], "low")
self.assertEqual(self.svc.with_shared({"reasoning_effort": "high"}, "openai")["reasoning_effort"], "high")
self.assertEqual(self.svc.with_shared({}, "anthropic")["output_config"], {"effort": "low"})
def test_off_again(self):
self.req("/settings", {"defaults": {"temperature": 0.3}})
code, b = self.req("/settings", {"defaults": None})
self.assertEqual((code, b["shared"]), (200, False))
self.assertFalse(os.path.exists(self.svc.shared_path))
self.chat()
self.assertNotIn("temperature", self.engine.last_sampling)
def test_only_strata_s_own_page_may_set_them(self):
code, _ = self.req("/settings", None, {"Content-Type": "text/plain"}, raw=b'{"defaults": {"temperature": 1}}')
self.assertEqual(code, 415)
code, _ = self.req("/settings", {"defaults": {"temperature": 1}}, {"Origin": "http://evil.example"})
self.assertEqual(code, 403)
code, b = self.req("/settings", {"defaults": {"temperature": 9}})
self.assertEqual(code, 400)
self.assertIn("temperature", b["error"]["message"])
self.assertEqual(self.svc.shared, {})
host = self.base.split("://", 1)[1]
code, _ = self.req("/settings", {"defaults": {"temperature": 1}}, {"Origin": "http://" + host})
self.assertEqual(code, 200)
def test_they_need_the_key_when_one_is_set(self):
self.svc.api_key = "secret"
try:
self.assertEqual(self.req("/settings", {"defaults": {"temperature": 1}})[0], 401)
self.assertEqual(self.req("/settings", {"defaults": {"temperature": 1}},
{"Authorization": "Bearer secret"})[0], 200)
finally:
self.svc.api_key = ""
class WebApp(unittest.TestCase):
"""The web app (PR #22's dashboard idea, rebuilt): its page and files, and GET /metrics."""
@classmethod
def setUpClass(cls):
tok = ByteTokenizer()
cls.svc = Service(RecordingEngine(tok, "</think>\n\nhello", max_context=CTX), tok,
ChatTemplate(ROOT / "serve/chat_template.jinja"))
cls.httpd = serve(cls.svc, port=0)
cls.base = f"http://127.0.0.1:{cls.httpd.server_address[1]}"
@classmethod
def tearDownClass(cls):
cls.httpd.shutdown()
def get(self, path, headers=None):
req = urllib.request.Request(self.base + path, headers=headers or {})
try:
with urllib.request.urlopen(req, timeout=10) as r:
return r.status, r.headers.get("Content-Type", ""), r.read()
except urllib.error.HTTPError as e:
return e.code, e.headers.get("Content-Type", ""), e.read()
def test_page_and_files(self):
code, ctype, body = self.get("/")
self.assertEqual(code, 200)
self.assertIn("text/html", ctype)
self.assertIn(b"/web/app.js", body)
for path, want in (("/web/app.js", "javascript"), ("/web/app.css", "text/css"), ("/web/tokens.css", "text/css"),
("/web/components.css", "text/css"), ("/web/sprite.svg", "image/svg+xml")):
with self.subTest(path=path):
code, ctype, _ = self.get(path)
self.assertEqual(code, 200)
self.assertIn(want, ctype)
def test_only_the_app_files_are_served(self):
for path in ("/web/..%2Fserver.py", "/web/index.html", "/web/test.py", "/fonts/..%2F..%2Fsetup.py",
"/fonts/missing.woff2", "/fonts/x.ttf"):
with self.subTest(path=path):
self.assertEqual(self.get(path)[0], 404)
def test_metrics(self):
data = json.dumps({"model": "m", "messages": [{"role": "user", "content": "hi"}], "max_tokens": 5}).encode()
urllib.request.urlopen(urllib.request.Request(self.base + "/v1/chat/completions", data=data,
headers={"Content-Type": "application/json"}), timeout=10).read()
code, ctype, body = self.get("/metrics")
self.assertEqual(code, 200)
m = json.loads(body)
for key in ("engine", "live", "requests", "hardware", "hardware_static", "history"):
self.assertIn(key, m)
self.assertEqual(m["engine"]["max_context"], CTX)
self.assertEqual(m["live"]["state"], "idle")
self.assertEqual(m["requests"][0]["output_tokens"], 5)
def test_metrics_need_the_key_when_one_is_set(self):
self.svc.api_key = "secret"
try:
self.assertEqual(self.get("/metrics")[0], 401)
self.assertEqual(self.get("/metrics", {"Authorization": "Bearer secret"})[0], 200)
self.assertEqual(self.get("/")[0], 200) # the page itself asks for the key
finally:
self.svc.api_key = ""
if __name__ == "__main__":
unittest.main()
Loaded 100 of 161 files, more files were not shown because too many files have changed in this diff. Show more