Compare commits

...
1931 Commits
Author SHA1 Message Date
Niko1221 146df3c468 setup.py run(): the app's installer also runs the bundled Mac preparation tools in metal/tools (metal_repack.py, stream_experts.py); a fresh Mac Terminal or app install of UD-Q4_K_XL or UD-IQ4_XS-v31 stopped with "only runs its bundled preparation tools" since af1ffd913. Test: they run, and -m, another interpreter and other folders are still refused 2026-11-07 01:29:36 +01:00
Niko1221 9780a8c27d Merge main (54828c7ef): four sponsors in Loyal supporters, all 8 READMEs 2026-11-06 21:53:35 +01:00
Niko1221 311926416a Merge f-0212-tools (ea765a571): tool-call fixes in serve/ and the native gateway, with their tests and docs 2026-11-06 21:53:34 +01:00
Niko1221 0fe4ae5090 qsa_select tp_select: no __match_any_sync below sm_70 (a plain add per thread there; integer atomics, the same sums): the CUDA 12 release engine (sm_60..89) builds again 2026-11-06 21:39:07 +01:00
Niko1221 ea765a5717 App gateway: an unknown model name on /v1/messages uses the one loaded model (Claude Desktop sends claude-5-5-sonnet); chat and Responses stay exact, and with several models loaded the 400 stays (#5312) 2026-11-06 19:55:58 +01:00
Niko1221 c49f198c77 Test: the Anthropic route serves the loaded model whatever name the request carries (#5312, claude-5-5-sonnet) 2026-11-06 19:51:51 +01:00
Niko1221 b1541ab245 Parser: the prefixed-name and misspelt-opener checks look only at the end of the buffer (the new text and 256 characters before it), so a long call is not rescanned with every token (#5409, #5276) 2026-11-06 19:48:21 +01:00
Niko1221 8a42a9457d moe_iq4_w4a8: the device side compiles for sm_60..75 targets (a trap there, as moe_fused_iq.cu does; arch_ok() already declines those cards, so no behaviour changes): the CUDA 12 and CUDA 13 release engines build again 2026-11-06 19:42:58 +01:00
Niko1221 05506f9532 Tool names: a namespaced MCP tool written by its last segment (get_asset_info for mcp__assets__get_asset_info) is that tool when exactly one declared tool ends that way (#5281) 2026-11-06 19:40:40 +01:00
Gabriel A. Devenyi 95381770f8 Anthropic tool results follow tool_use_id: results sent in the order the tools finished are put in call order before the template pairs them by position (PR #4832) 2026-11-06 19:36:59 +01:00
Gabriel A. Devenyi d7e555bdbb tool_choice "required" with several tools: write only "<tool_call>\n", because from a written "<function=" the model went on with names no tool has and the reply came back as text (PR #4828) 2026-11-06 19:36:43 +01:00
Niko1221 75acfca321 Config undeclared_tool_calls (env STRATA_UNDECLARED_TOOL_CALLS): "call" returns a well-formed call of a tool the request did not declare as a tool call; the default "text" is as before (#4622) 2026-11-06 19:36:04 +01:00
Niko1221 63fd759227 Tool calls: a misspelt opener (<toolcall>, <tool-call>) followed by <function=NAME> of a declared tool opens the call, where a <tool_call> would; prose and code stay text (#5276) 2026-11-06 19:34:47 +01:00
Niko1221 54828c7efb README: Loyal supporters adds themactep, macintog, bisonbet and vizv (public GitHub sponsors) in all 8 languages 2026-11-06 19:33:46 +01:00
Niko1221 68ab15ddc1 Tool names: <function=tools NAME> (a space for the dot) is the declared tool NAME, and a bare <function=tools> is the one declared tool that takes the call's parameters (with tool_call_recovery); anything else stays text (#5409) 2026-11-06 19:32:59 +01:00
Niko1221 7be42963e6 Version 0.2.12 / engine 0.1.58: Terminal and app 0.2.12, Control and every engine package 0.1.58 (the 0.2.11 bump list, plus the v0.1.58 line in the setup.py version history) 2026-11-06 19:21:19 +01:00
Niko1221 5b1728358b test_download_integrity: a server that keeps serving the wrong file ends with the file kept as .bad (the dlsha behaviour: the new mismatch line, the file not deleted) 2026-11-06 17:43:19 +01:00
Niko1221 706efc5b0f prefill_fused_iq_test: skip the Q5_0 down pairs where the native kernels do not cover them (CUDA has none; the gfx12 / gfx1151 builds run them); --only=NAME still insists 2026-11-06 16:29:26 +01:00
Niko1221 0a0e4463ea file tier: the re-check after the RAM copy counts only the page cache the group can really have (on by default; STRATA_FILE_TIER_HONEST=0 turns it off)
I/O path only: the same bytes are read, only HOW the experts outside the RAM copy are read (unbuffered or through the file cache) is chosen differently.

file_tier_unbuffered took its `avail` from cgroup_available_bytes, which credits the group's whole `file` counter as reclaimable. On a RAM-budget
config that credit includes pages the engine holds itself: P2003 (RTX 3060, 46 GB, 36 GiB group), UD-IQ4_XS-v31, 22 GiB RAM copy: the re-check read 18.7 GiB
available when the group had 11 GiB left (limit 36, current 24.8). With held = 0.52 >= 0.5 the timed drive probe was skipped and the file tier switched to the file
cache (direct reads 7.9 GB/s vs 2.5 GB/s cached): prompts 2.5-3x slower (pp2k 85 vs 273 tok/s, pp8k 195 vs 580), decode 10-14% slower. The one start that read
12.0 GiB kept the fast mode, which is why the first start of a session was sometimes fast.

Now the smaller of that figure and the conversation cache's sampler (only the clean inactive file cache is credited, the group's own limit, no operator
cap) decides. The --memory-limit-mib cap is not applied: it bounds the engine's own allocations, not the page cache (counting it sent IQ3_S unbuffered and cut its decode
2-3x). A flip now prints once: "strata: expert reads now go through the file cache (X GiB of RAM free for them, Y GiB limit)"; the technical line is behind STRATA_TRACE.
STRATA_UNBUFFERED_LOAD still forces either mode.

Measured on P2003 (build wip11, n >= 3 starts per arm, interleaved fresh engines, I/O-quiet before each start, memory watch on, floor 14336 MiB):
 v31 unforced: main flipped in 5 of 6 starts, this change 0 of 9 (6 + 3): pp2k 85-94 -> 263-273 tok/s, pp8k 187-198 -> 580-590, code_py decode +14 to +20%;
 v31 DFlash2 vs MTP in the kept state (3 rounds): DFlash2 +2 to +14% per kind (json_extract -3.5%);
 IQ3_S: the file cache stays (9 of 9 starts), decode level with main (code_py 17.0 vs 15.6 tok/s, json_extract 14.2 vs 13.4, page cache evicted before every start);
 low RAM (6-7 GiB ballast, budget 16 GiB): no regression, MemAvailable minimum 14911 MiB;
 memory minimum with the unbuffered state 15.6-15.8 GB (floor 14.3 GB), no arm stopped.
Not P1.8: main and the P1.8 build flip alike.

(cherry picked from commit be3831e9d944fb44ac36d176f4764e30df0f9f6b)
2026-11-06 15:51:21 +01:00
Niko1221 e77bd43bc9 prefill: a Q5_0 down (UD-IQ4_XS-v31: 33 of 48 layers) on the fused int8 kernel on gfx12, opt-in STRATA_PF_FUSED_Q50=1
native_w11_kernel takes a Q5_0 down block (d, qh, qs[16]; 22 bytes): the 5-bit codes are rebuilt with the qh bits and kept as the signed int8 q - 16 (no minimum
term), the scale is d.  d_covered_on() says yes for T_Q5_0 only on gfx12 with the switch; the layer otherwise leaves the fused path for the gather + FP16
dequantize + hipBLASLt route as before.  hip_prefill_fused_iq gets the pairs IQ3_XXS / Q5_0 and IQ4_XS / Q5_0 (reference-checked: the build has no Q5_0 MMQ).

R9700 (p620, v31, DFlash2, same executable with and without the switch, 3 rounds x 2 reps): prompt 2K +44%, 8K +40%, 32K +13%; decode unchanged.
First-token KL against the FP16 route (self-variation 0): 2K mean 0.016, max 0.068, top-1 24/24.  Off by default in this commit.

(cherry picked from commit 97c3033f566d27ea1d741d7b893ac13e0f4e8526)
2026-11-06 15:48:47 +01:00
Niko1221 37b5cf0db2 gfx12: the start-up line for the length-gated chunked GDN scan, and STRATA_GFX12_DEFAULTS=0 turns the gate off
"strata: RDNA4 (gfx12): prompt chunks of 30,000+ tokens use the chunked GDN scan (STRATA_GDN_CHUNKED=0 turns it off)" is printed once at start with the
other gfx12 default lines (owner's wording, 11-06): only on gfx12, only while STRATA_GDN_CHUNKED is unset and STRATA_GFX12_DEFAULTS is not 0.
The gate follows the gfx12 defaults' one off switch, so the line is true whenever it is printed.

(cherry picked from commit b2d1680a864195383697559edb9b6cf8180b9a19)
2026-11-06 15:47:44 +01:00
Niko1221 c0e7a6f4f8 gfx12: the chunked GDN scan is the default for prompt chunks of 30,000 tokens or more
STRATA_GDN_CHUNKED unset on a gfx12 card (RDNA4): the FP16 matrix-core chunked recurrence runs for prompt chunks of 30,000 tokens or more
(--prefill auto takes chunks up to 32,768 there, so a 32K prompt is one chunk of about 31,996 rows).  Shorter chunks keep the kernels they
ran, so every prompt up to 20K is bit for bit what it was.  STRATA_GDN_CHUNKED=0 turns it off, =1 is the old opt-in (every chunk of 64
tokens or more), STRATA_GDN_CHUNKED_MIN=N moves the line.  Owner OK 11-06.

R9700 (p620, IQ3_S, DFlash2, interleaved pairs against the same tree without the change, 5 rounds x 2 reps): prompt 32K +6.1% (5/5),
64K +6.7% (5/5).  First-token KL against the unchanged build (self-variation 0.00000): all 24 prompts of 2K, 8K, 20K and the 8K agent turn
identical; 32K 0.00005, 32K+ 0.00004, 64K 0.00086; top-1 equal on all 30 prompts.  No log line yet (wording is with the coordinator).

(cherry picked from commit 7b579367aaa56978232cb60ef405987a199d736f)
2026-11-06 15:47:44 +01:00
Niko1221 645b9177e3 setup: wording for the kept .bad file (NEEDS OWNER APPROVAL): the mismatch line says the file was kept, and says when the server sends the very same bytes; one line when the kept file is removed for room
(cherry picked from commit ae3c3bad63c23b2b51c72f232832d2db195ef72c)
2026-11-06 15:47:44 +01:00
Niko1221 071e4dc51e setup: a downloaded file with the wrong SHA-256 is kept as <name>.bad instead of deleted; the next run hashes it again and takes it back if it matches, a good new download or too little room removes it (visible text unchanged in this commit)
(cherry picked from commit e547295924d5cb560febafca6d1cbadeed33d2b9)
2026-11-06 15:47:44 +01:00
bulhenvaster bca7931f3d peer tier: STRATA_ADAPT_OVERLAP=1 with --peer-device (the peer's copies land while the next window runs)
In peer mode the adaptive overlap was refused, so every decode window first waited for the previous round's copies. With the overlap the peer
tier issues its copies in layer order, signals each layer's landing the way the primary does (a stream-ordered D2H copy of the layer number into
a pinned int, or host functions), its residency table takes the swaps at the window's start (flip_pending), and a layer waits for a tier's copies
only when that tier swapped in it (AdaptGate: peer_landed, peer_has, per-tier last). The wait before a window clears the peer's marks with the
primary's. Without the variable nothing changes.

Ported from #2720 (bulhenvaster), rebased by hand onto the current gate (the watchdog label stays). Not ported: STRATA_PEER_LAUNCH_IN_POOL
(the author measured no gain on his build), the new decode timing line and the documentation paragraph.

(cherry picked from commit e78b64a7348d685dfd11799fdb395661f2120436)
2026-11-06 15:47:43 +01:00
bulhenvaster 2b9f05093d prefill: STRATA_PF_PEER_CPU=1 (opt-in) runs the CPU share beside a peer GPU
With --peer-device the prompt path never gave the CPU pool a share (cpu_maybe required !m.pp), so a short read at long context streamed every
expert neither card holds over the primary's link while the CPU sat idle. With the switch the share runs beside the peer: it takes only experts
neither card computes, its rows go between the primary's and the peer's block (the peer's stays the tail its combine reads from back_at), and the
staged chunks go up to cpu_share_max()'s 3072 tokens. Without the switch nothing changes.

Ported from #2721 (bulhenvaster), rebased by hand onto the current prompt path (the default-armed share's limit and the routed-stream gate).

(cherry picked from commit c8b6c43c1f2653636d15fe14abf53112357570aa)
2026-11-06 15:47:43 +01:00
sergqwer 24c6c2f682 Admit the PCIe share's fetched misses to the GPU cache (opt-in STRATA_ADAPT_FETCH=1|2)
Decode reads missed expert blobs from host RAM three ways; the PCIe share's
blobs cross into VRAM anyway and were dropped after the window. With the
switch set, the share takes each layer's most-routed misses and a fetched
expert routed enough more than the layer's least-routed resident expert that
the window does not route is copied straight into that expert's slot
(fetch_blobs gets an optional dst2 target per blob). =2 leaves the tier's own
swaps out of a layer where an admission ran. STRATA_ADAPT_FETCH_VERIFY=1 reads
every admitted slot back. Off, with a log line saying why, where it cannot run.
Unset changes nothing.

Ported from PR 2964 (sergqwer) onto f-0216-core; the strata generate verify
loop was merged by hand (main's speculative-q lines kept) and the DETAILS.md
note became a row of the opt-in switch table.

l620 (RTX PRO 5000, IQ3_S, 3,896-slot cache, --pcie-frac 0.25): decode =2 +10.2%,
=1 +5.0% (4 rounds); token forcing over 14,167 tokens: mismatch 2.73% vs 2.53% /
2.53% for two unset arms (+0.19 pt, 90% CI -0.02..+0.42), PASS.

(cherry picked from commit b2bfaef906acee4f86bdf9f052aee6a4d295fb4e)
2026-11-06 15:47:43 +01:00
bitaccesscomau 386f9959f1 Exact two-pass QSA prompt selection (opt-in STRATA_SELECT_TWO_PASS=1)
Prompt-path block scores in single-pass TF32 with a per-block error bound,
3xTF32 rescoring only near each query's top-k cut-off, so qsa_block_topk
returns the same cells in the same order, ties included. Off unless the
variable is set; says once where it cannot run (HIP, Metal, SYCL, layer-major
path). Adds qsa_two_pass_test, qsa_two_pass_status_test and the GPU parity
test qsa_two_pass_parity.

Ported from PR 4119 (bitaccesscomau); the log line of the per-prompt counters
is shortened without dropping a field.

l620 (sm_120, IQ3_S), greedy ids identical off vs on in every pair:
prompt 128K +1.65% (n=12), 256K +4.10% (n=8), 32K -0.14% (noise).

(cherry picked from commit bbf7c55f1351d913233250fe7869642f7c2b98a1)
2026-11-06 15:47:43 +01:00
Niko1221 f281e3fe26 force_check.py: absolute path for the profile copy
(cherry picked from commit 1fb1e134809f6015c72802f19a99d70a1b1636db)
2026-11-06 15:47:43 +01:00
Niko1221 f46e88d610 force_check.py: cfg profile_src gives each engine start a fresh copy of the expert profile
(cherry picked from commit e47164c8e9d4d0ae0826d10177cc4d74369ec0bf)
2026-11-06 15:47:43 +01:00
Niko1221 d486fbe7d6 serve: STRATA_FOLLOW / STRATA_FOLLOW_LOGPOS token forcing through the real decode/verify windows (test and measurement only, off by default); force_check.py score --path decode
(cherry picked from commit af68878f0cc4c5aa4618da8f96b10f9262230721)
2026-11-06 15:47:43 +01:00
Niko1221 65909b2d0f verify: STRATA_VERIFY_PROFILE and STRATA_ROUTE_STATS reports cover windows up to 16 rows (measurement only; default behaviour unchanged); tools/force_check.py: the token-forcing check (scorer driver + the paper's two rules)
(cherry picked from commit b3b8c1d5a088c998947895a3a4007138b085da66)
2026-11-06 15:47:43 +01:00
Niko1221 cae562fa01 verify: STRATA_FORCE_FAULT=<m>:<bias> test knob for the token-forcing scorer (adds a bias to every m-th token's logit in the STRATA_LOGPOS rows; off by default, silent)
(cherry picked from commit dcb0483f06810f397cfdce9c1db976d8e67e2a24)
2026-11-06 15:47:43 +01:00
Niko1221 63f063852f prefill: UD-IQ4_XS-v31's Q5_0 down layers (33 of 48, with IQ3_XXS or IQ4_XS gate/up) take the fused F16 matrix-core kernel on gfx1151 under Windows instead of leaving the fused path for gather + FP16 dequantize + hipBLASLt. The Q5_0 block decode (qh bits spread, int8 codes q - 16, scale d) is the RDNA4 patch's; native_f16_kernel and the FP16 down rows are the K-quant experts' route. On by default there; STRATA_PF_FUSED_Q50=0 or STRATA_GFX1151_DEFAULTS=0 turns it off (the layers keep the earlier route), =1 turns it on for any gfx1151 build; it needs the F16 prompt set (STRATA_PF_F16). Other GPUs are untouched. Parity test pairs IQ3_XXS / Q5_0 and IQ4_XS / Q5_0 against the FP32 dequant reference, with 256/64, 33/16, 1/16, 130/32 and 517/32 token/expert cases: rel RMS 2.7e-3 against the int8-rounded model, 1.4-1.5e-2 against FP32 (the IQ pairs' usual level)
(cherry picked from commit 12f9547785781f77da41d6d08c2f541077de2df0)
2026-11-06 15:47:42 +01:00
Niko1221 cc3f9142e3 Auto on the 12 GB / streaming CUDA class: IQ3_S starts the MTP, UD-IQ4_XS-v31 starts DFlash2 with the tuned pack at the class's own 0.8
drafting.CLASS_DEFAULTS: one removable entry per set, read from the signals the confidence already reads (a CUDA card whose engine slots are under the last-but-one CONF_STEPS step).
IQ3_S: on P2003 the file cache makes the MTP faster than DFlash2 by 12-44% (RTX2 t4b). UD-IQ4_XS-v31: DFlash2 beats the MTP by about 6.5% (t8, 3 starts; DF-VAL forced-fast tuned 0.8 x1.037).
The v31 entry DEPENDS ON RTX2's FILE-TIER FIX (unbuffered reads) BEING IN THE SAME RELEASE; without it remove that entry.
Anything chosen by name wins. Resident CUDA, Strix, gfx1201, Arc and Metal entries and the conf tables are unchanged. The sm_86 support line reads the set: UD-IQ4_XS-v31 gets the existing
'faster on some kinds of text and slower on others' line, every other model keeps 'slower'.

(cherry picked from commit 42412d42ff835d6a66a78fbf6d119911908eda1b)
2026-11-06 15:47:42 +01:00
Niko1221 f138653938 DFlash2 on Strix Halo: the tuned pack for UD-IQ4_XS-v31 too, at Strix's current confidence
One more SET_DEFAULTS line for the measured cell (DF-VAL, setup's config, budget 38, 3 rounds: tuned 0.35 against stock 0.35 x1.046, 3 of 3 rounds, every kind 3 of 3).

(cherry picked from commit c2b801157e71a503da859b66baff12fff28cd299)
2026-11-06 15:47:42 +01:00
Niko1221 ce6bbd7d54 DFlash2 on Intel Arc: the tuned bf16-selector pack for SYCL + IQ3_XXS at Arc's own confidence 0.6
The SYCL 0.2.11 engine reads only bf16 selector codebooks, so Arc gets its own tuned file (qwen3.8-flash-next-dflash2.strata1.q4_0-bf16sel.pack, 522,458,112 bytes, own SHA-256 pin) through
SetDefault.pack. Only IQ3_XXS, the set measured on the A750 (tuned 0.6 x1.054, 3 of 3 rounds); other Arc sets keep the stock pack. SYCL never gets the selq4 file, CUDA / HIP / Metal never the
bf16sel one. NOTICE names the second file and no longer overclaims about the text.

(cherry picked from commit 6111c583b70e3c88a3d54039e13d76b19f15742d)
2026-11-06 15:47:42 +01:00
Niko1221 aba4ddaca7 Auto on HIP gfx1201 starts the MTP until the DFlash2 prompt stall is fixed (one entry: drafting.AUTO_MTP)
11 of 50 engines on 0.2.11 never got past the prompt on this card class. Only Auto changes: DFlash2 chosen by name, a saved choice and the person's own options still win; gfx1151, other AMD, CUDA, Metal and SYCL and the confidence tables are unchanged. The support line under the Drafting dropdown says it in plain words (app catalog entry in all nine languages).

(cherry picked from commit 8b4398743aa67368d72277310bbe8093bbe4923c)
2026-11-06 15:47:42 +01:00
Niko1221 31928fbe34 DFlash2: Strata's tuned drafter pack on CUDA (every expert resident) and Strix Halo, at their current confidence
SET_DEFAULTS entries carry a card condition (tuned_applies: the CONF_STEPS first-step slots or the Spark's shared memory for CUDA, gfx1151 for HIP); no entry names a conf, so
conf_for / CONF_STEPS / BACKEND_CONF run as before and only the pack changes. Enabled only for the cells measured at the shipped conf (CUDA-resident IQ3_S and UD-IQ4_XS-v31, Strix IQ3_S, UD-Q4_K_XL, UD-IQ4_XS).
12 GB and 24 GB cards, several cards, gfx1201, other AMD and SYCL never fetch or use it. Fetch keeps tuned, then stock, then MTP; silent offline; feed backends are asked per start's backend.

(cherry picked from commit f096dcd750b5928ace6f9eca1e2936b9e35578d4)
2026-11-06 15:47:42 +01:00
Niko1221 10831eceb2 Metal build: stubs for the f-0216-cpu 512-bit entry points (q2_rows_512_ok, iq512f_supported, iq512_fused_gu_rows, iq512_reference_gu_rows, q50_512_down_rows) 2026-11-06 09:57:08 +01:00
Niko1221 7604725595 arm64 portable build: stubs for cpu_vnni_ok, q2_rows_512_ok and iq512_reference_gu_rows (the Spark link failed on them) 2026-11-06 09:51:02 +01:00
Niko1221 2a8217c0ce generate: the CPU expert kernel line reads 'CPU expert kernels: <X>' (owner's short wording) 2026-11-06 09:44:37 +01:00
Niko1221 a7cc8faa5d split: one constant for the expert window floor, and the IQ3_S pool test
EXPERT_WINDOW_SLOTS (2 * MAXT * TOPK) is the slots a size pool must hold for one window's picks to have a place; ExpertCache's check, the residency
planner's floor and the arena's keep all read it. expert_residency_test now also covers the GSQ-RCO IQ3_S set's real pools (7, two of them a single
layer), a sweep of RAM targets against a copy of the old global ranking as the negative control, a refusal beyond what the floors allow, and that
the card's sliced 16-byte-padded strides partition the layers like the whole-expert bytes do. Class E (placement only).
2026-11-06 09:36:37 +01:00
MIDHAT NASHAR 20885f7af0 split: retain a window in every expert pool
Mixed-quant models can fail to load in the split engine after cold-expert offload: the global residency ranking can leave one size pool below the
2 * MAXT * TOPK slots Up::expert_cache() needs even while other pools can still give up experts. Group experts by both the gate/up and the
down byte strides, keep the cold-first ranking but skip eviction from a pool that has reached the window floor, count only the extra evictions
that really happen, and say so before the model is uploaded when the request cannot be placed.

Ported from #5158 (midhatn) unchanged; the tests and the engine check follow in the next commit.
2026-11-06 09:36:37 +01:00
Niko1221andhavok2-htwo abf2953be0 native_dense: STRATA_HC_Q8 copies only the stage's own Q8_0 hyper-connection rows on a trimmed layer split (P1.2)
With --trim-stage-weights every later stage's HC_Q8 loop over the GGUF copied the rows of all 48 layers and read its own. The loop now skips a
tensor outside the stage's layer range, the filter the upload loop above it and the bf16-pack path below it already use. The Q8_0 rows copied
and the bytes not copied are recorded in the run manifest (STRATA_MANIFEST). STRATA_HC_Q8_ALL_STAGES=1 copies every layer as before.
No value changes: a stage never reads another stage's layers; without a trim the range is unset and nothing is skipped. Class E.

Co-authored-by: havok2-htwo <[email protected]>
2026-11-06 09:36:37 +01:00
Niko1221 78c5a834e3 Merge f-0216-dfship into f-0216-all 2026-11-06 09:36:34 +01:00
Niko1221 51548e5539 Merge f-0216-strix2 into f-0216-all 2026-11-06 09:36:34 +01:00
Niko1221 7597e383cf Merge f-0216-rtx into f-0216-all 2026-11-06 09:36:30 +01:00
Niko1221 f2eedfe9f2 Merge f-0216-cpu into f-0216-all 2026-11-06 09:36:30 +01:00
Niko1221 bd0e878f42 Merge f-0216-core into f-0216-all 2026-11-06 09:36:29 +01:00
Niko1221 4084979c95 prefill: STRATA_PF_ALLOC_TRACE=1 prints the free-memory figure around Prefill::init's allocations against what the code asked for (stager ring, routing tables, PLE rows, GEMM scratch, the chunk's buffers, library warm-up). Off by default, no behaviour change 2026-11-06 08:56:14 +01:00
Niko1221 530a29ea2d serve: on an integrated GPU with a page-locked expert complement (Strix Halo), the free-VRAM startup line and the request-start warning no longer advise a larger --vram-reserve-mib (it cannot raise that figure: the driver counts pinned host memory and the prompt path's buffers about twice, so the cache giving up experts to the complement only lowers it). New startup line: "N MiB of graphics memory reported free. On this PC the graphics chip shares the system memory, so this figure reads low, and a larger --vram-reserve-mib won't raise it. If decode drops to a few tok/s, lower --max-context." New request-start note, printed once: "Only N MiB of graphics memory reported free at the start of this request. On this PC the figure reads low because the graphics chip shares the system memory. If decode drops to a few tok/s, lower --max-context." Every other GPU keeps the earlier text. Wording only: greedy ids identical to engI on agent8k, agent, pp4k and code_py (UD-Q4_K_XL, setup config) 2026-11-06 08:56:13 +01:00
Niko1221 3e8d3b2940 Mac: DFlash2 is the default drafter for UD-Q4_K_XL and UD-IQ4_XS-v31, with Strata's tuned drafter at --draft-conf 0.7
SET_DEFAULTS is the one per-(backend, set) table (Metal only; IQ3_S stays on the MTP). Setup, strata launch and strata drafting fetch fetch the tuned pack through the signed feed
(pinned size and SHA-256, resumable, silent offline, the standard drafter and then the MTP as fallbacks). Apache-2.0 licence and the modified-by-Strata notice ship in
data/dflash2-tuned and are copied beside the file. The Metal 'not tested' line is replaced for these sets.
2026-11-06 07:52:31 +01:00
Niko1221 039cecc5a5 feed: a drafters member for DFlash2 drafter files Strata hosts (pack-drafter, build-feed --drafters, make_local_feed --drafter) 2026-11-06 07:52:31 +01:00
Niko1221 d735d66a4d docs: STRATA_IQ4_W4A8 on sm_120 and its exit counters; STRATA_FILE_TIER_IDLE and the idle policy of the file tier 2026-11-06 01:49:09 +01:00
cctan 7bf5ec4ebf prefill: opt-in int8 kernel for the IQ4_XS / IQ4_NL expert products (STRATA_IQ4_W4A8), MMQ's bits
Ported from #4871 (cctan): src/prefill/moe_iq4_w4a8.cu, tried first by mmq::Context::run; unset, nothing changes.
Changes on top of the PR: the "would MMQ split this launch along K" test is now mmq::launch_splits_k, which reads the
device's real MMQ configs (tile width, stream_k flag, tile efficiency) instead of an Ampere table, so the kernel declines
exactly the launches whose sums MMQ adds in another order, on any architecture; sm_120 joins sm_80..sm_88 (bit-for-bit
against MMQ on an RTX PRO 5000 and an RTX 3060); the kernel counts what it ran and why it declined and prints that at exit;
prefill_iq4_w4a8_test takes --tokens N (tails), --wide (scale extremes) and --bench (MMQ against the kernel per product set).

Class E: 0 differing output words in every case (tokens 97 / 2049 / 4096 / 8191 / 15360 / 32768, uniform and log-normal routing,
group buffers and pointer table). Kernel time against MMQ on the down products (IQ4_NL): +9..+70% on sm_86, +13..+50% on sm_120.
2026-11-06 01:49:09 +01:00
Niko1221 178d1c74b5 merge f-0215-mac-pp (host-row pipeline, qsa_merge_h, ragged expert tiles) into f-0215-mac-int 2026-11-06 01:25:09 +01:00
Niko1221 eecd7f61d7 merge f-0215-mac-out (step profile, opt-in screens, Metal DFlash2 conf 0.5, calibrated gate) into f-0215-mac-int 2026-11-06 01:25:09 +01:00
Niko1221 86c3367a89 native_expert.cpp: the fused-rows type list terminator is '\0' (a NUL byte had slipped into the character constant; MSVC rejects it) 2026-11-06 01:05:40 +01:00
Niko1221 04b73d061d kq_window_bench: --dump-trace writes the synthetic windows in the route-dump text form 2026-11-06 01:01:14 +01:00
Niko1221 1daec2a5b8 check_isa_guard: iq512_reference_gu_rows and iq512x_rows are listed (main already reports them as unlisted; both are reached only behind cpu_avx512_ok / cpu_vnni_ok) 2026-11-06 00:51:10 +01:00
Niko1221 b94ab5f4c6 kq_window_bench: the CPU pool's part of a decode window replayed without a GPU (per-layer format pairs from the pack, expert and row counts calibrated on the engine's logged means, cold DRAM) 2026-11-06 00:50:40 +01:00
Niko1221 445cdf4540 Run manifest: say what the unset STRATA_ROUTE_TAIL_SKIP means (the engine default applies, tails are skipped only where the configuration allows) 2026-11-06 00:47:28 +01:00
Niko1221 8463d15c84 STRATA_ROUTE_DUMP: terminator fix 2026-11-06 00:42:08 +01:00
Niko1221 1c768f9d12 STRATA_ROUTE_DUMP: newline fix 2026-11-06 00:41:18 +01:00
Niko1221 b0b0348c2b kernels: opt-in fused gate/up rows in ggml order for 2+ tokens (STRATA_IQ512_FUSED_MAX; N, after PR 5243's row_gu_dot); STRATA_ROUTE_DUMP routing trace for offline placement replays 2026-11-06 00:39:11 +01:00
Niko1221 7e3fae57ca PR 5120 tier below VBMI is opt-in (STRATA_VNNI_TIER=1): its 512-bit rows round differently from the AVX-2 rows such a CPU runs today (SDE Cascade Lake: all multi-token gate/up lines and the Q2_0 rows differ), so by default every CPU keeps its output 2026-11-06 00:22:37 +01:00
Niko1221 32944ce1ee cpu_dispatch_dump: the Q2_0 rows (q2_rows_any) next to ggml's, so the substitute unpack can be compared with the multishift 2026-11-06 00:22:30 +01:00
Michel Pollet f000d4a455 cpu/experts: the 512-bit VNNI rows on a CPU without AVX-512-VBMI (PR 5120)
cpu_vnni_ok() = AVX-512 F + DQ + BW + VL + VNNI + OS state, cpu_vbmi_ok() = F + VL + VBMI; the Q2_0 row dot and the q8 quantizer take the 512-bit form
on VNNI without VBMI (the one VBMI op, vpmultishiftqb, has a bit-for-bit substitute unpack), and the i-quant / Q5_0 rows of iq_avx512.cpp, whose object holds
no VBMI instruction, gate on cpu_vnni_ok() instead of cpu_avx512_ok().  Adjustments over the PR: DQ is asked for (the file is compiled with it), the i-quant rows
gate on VNNI (the rows use vpdpwssd/vpdpbusd), and STRATA_NO_VBMI=1 runs the substitute unpack on a CPU that has VBMI (a test knob).
2026-11-06 00:22:30 +01:00
Niko1221 6368dc68ab cpu: drop the IQ4_NL 512-bit down rows (5-11% slower than iq4nl256_down_rows at 2-4 tokens on Zen 4); Q5_0 down keeps its 512-bit rows; vpdpwssd in the i-quant rows' integer sums (same integers, one op less) 2026-11-06 00:22:29 +01:00
Niko1221andbsorensen110 55dc17697c cpu: IQ4_XS gate/up and Q5_0 / IQ4_NL down rows in 512-bit lanes on AVX-512 CPUs, bit-identical to the code they replace
IQ4_XS gate/up: ggml-cpu's dot order (the integer lanes of its AVX2 dot, f16 scale times y.d, eight float lanes, its hsum) for every token count;
before, an AVX-512 CPU ran ggml's one-token dot per token.  Q5_0 down: ggml's dot order, weights decoded once per block pair for all tokens.
IQ4_NL down: iq4nl256_down_rows' order, 512-bit.  Each is checked once against the code it replaces; STRATA_NO_IQ4XS512=1 / STRATA_NO_QD512=1 switch
them off.  The IQ4_XS nibble-plane decode follows the layout of PR 5243.

Co-authored-by: bsorensen110 <[email protected]>
2026-11-06 00:22:29 +01:00
Niko1221 241b2f203c cpu_dispatch_dump: a hash per (pair, token count, row range) of the pool's native expert calls, to compare a change switched off and on 2026-11-06 00:22:29 +01:00
Niko1221 044105d8de kq_cpu_bench: --part gu|down times one projection alone, --hot K walks K distinct experts (cache-resident case) next to the DRAM walk 2026-11-06 00:22:29 +01:00
Phlimosx 994029b02f generate: the file tier's Linux defaults and the routing-aware prefetch stay off while every expert is in the GPU caches
Ported from #4972 (Phlimosx), extended: the residency is checked again at every request's start, so a
smaller cache (VRAM resize) or an eviction turns io prefetch, its decode gate and the look-ahead back on,
and a regrown cache turns them off again. Only what the defaults switched on is touched: an explicit
STRATA_IO_PREFETCH=1 keeps io prefetch and the look-ahead, STRATA_LOOKAHEAD=1 keeps the look-ahead,
STRATA_FILE_TIER_IDLE=0 turns the policy off. set_io_prefetch clears the gate flag with the prefetch.

Class E: the answers are identical (45 of 45 decode answers on l620, --mmap-experts, 24576 of 24576 resident).
2026-11-06 00:22:11 +01:00
Niko1221 89f4fa18ee Run manifest (lite): STRATA_MANIFEST=<file> writes the selected fast paths, fallback reasons, tensor formats, engine choice and budgets as JSON (P0.1)
A diagnostic for testers and for the performance work, off by default. With STRATA_MANIFEST unset every call returns at once
and nothing is printed or written. With it set, the engine writes <file> (whole file, replaced by rename) when it is ready and
again after a request if something new was recorded; the launcher (strata_control/split_engine.py) writes the all-cards split
decision to <file>.launch.json. Sorted keys, no timestamps, process ids or addresses, so two matched runs give the same text.

Content:
- engine: version, engine kind (single GPU or layer split), stages, KV format, drafter, spec window, pipeline windows, PLE I/O.
- budgets: context, KV resident cells, expert slots and cache MiB (all tiers and primary), VRAM reserve, prefill chunk cap and
  stages, pool workers, memory limit, resident budget, batch slots, conversation cache.
- paths: one entry per decision with selected/declined and the reason in plain words: prompt experts per format class (MMQ,
  FP16 WMMA tiles, native fused kernels, FP16 GEMM, with the gate/up and down formats and the layer count), MMQ direct and group
  sort, HC_Q8, the output head (VRAM or pinned host, repacked or not), route tail skip, DMA batch mode.
- tensors: dense and head formats as held (count, bytes), the packed and HC_Q8 copies made at load.
- launch: split-engine mode, engine chosen, reasons, card architecture and count, routed-expert types, and the AMD auto rule
  condition by condition (architecture, card count, expert types), so four gfx1201 with the IQ3_S set is recognised and two or
  three cards are not promoted by the four-card rule.

The only text it prints is "strata: STRATA_MANIFEST: cannot write <file>" on stderr, once, when the variable is set and the file
cannot be written.

Tests: tests/core/run_manifest_test.cpp (off is free and writes nothing, matched runs in any recording order give the same text,
replace-on-rerecord, escaping, dirty tracking, unwritable target), tests/split/test_split_engine.py::LaunchManifest.
2026-11-06 00:21:53 +01:00
Niko1221andAlanJager e2d3159996 Build the flag-order test on HIP too: hip_flag_order_test is the same source through the compat header (P1.1)
Co-authored-by: AlanJager <[email protected]>
2026-11-06 00:20:28 +01:00
Niko1221andmartifaj 1ed923f59e PLE reader: issue() folds a small batch in a stack buffer instead of a hash table and a job vector (P1.6)
Class E: the queued jobs (page offsets, read lengths, Use order inside a job) and the dedup_rows count are the same as
before; the bytes delivered are the same.

PleReader::issue() built a std::unordered_map by page and a std::vector<Job> on the heap on every call, then sorted the
jobs by offset. A decode step asks for 1..16 rows (the prefetch for PLE_N_HEADS = 16), so those allocations and the
hashing were most of the work. A batch of up to 64 rows (PR #4910) now collects its misses into a fixed stack array,
sorts them by (page offset, request order), and merges equal pages in one pass straight into the queue. Nothing is shared
between calls, so concurrent issuers (they hold the reader's mutex when threaded) have no scratch to race on.

Batches above 64 rows (a prompt chunk: thousands of rows) keep the previous hash path. The PR also sorted every miss in
that case with a reusable vector; measured, that is slower there (see below), so it was not taken. A batch of more than
2^32 rows is refused like a failed reader, as the 32-bit order/ticket counters cannot hold it (the old code could not
queue it either).

Measured with tests/ngram/ple_issue_bench.cpp (this thread's heap allocations and thread CPU time inside issue(), threaded
reader, 3000 calls per cell, 2 rounds that agree within 3%; Ryzen 5 5600X, g++ -O2, synthetic 300k-row table, rows are a
4000-row window / uniform / skewed):
  rows   allocations/issue before -> after    CPU us/issue before -> after
     1        5.1 -> 2.1                          1.69 -> 1.54
     4       13.4 -> 5.4                          2.00 -> 1.82
    16       40.3 -> 17.4                         3.44 -> 2.53
    64      125.2 -> 51.8                         8.07 -> 5.01
  above 64: unchanged (the PR's sort-everything variant took 159 vs 66 us at 3072 rows and 2.9 vs 0.55 ms at 49152).
The decode impact is not measured here (see the round's results file).

Tests: ple_reader_test --selftest now also checks, for batch sizes 0, 1, 2, 15, 16, 63, 64, 65, 66, 127..129, 300, 1000,
4096 (random, clustered, one row n times, descending, out-of-range mixed in, page-straddling rows twice), that the bytes
are right, requests grew by the rows asked for and dedup_rows by (rows read - distinct pages). With the existing
selftest and the multi-client stress test (rows across pages, injected delays and read errors, late writers, failed
reader): pass plain, under ASan+UBSan, and the stress test under TSan.

Co-authored-by: martifaj <[email protected]>
2026-11-06 00:12:28 +01:00
Niko1221andcloud4t0r aeaca205eb Mixed-HIP pipelined prompt read: stage 0 is served to its end before the drafter's blocking prefill on its card (P1.9, part 2)
Correctness (class E): the same windows are served in the same order, only a little earlier; the tokens do not change.

With --pipeline-windows 2 on a two-stage layer split the drafter sits on the first stage's card (mtp_first).
read_windows_pl called mtp.prefill(...) right after a stage-1 window finished, while stage 0 still had a window in
flight on that same card. MtpDrafter::prefill blocks the host until its stream is idle (on HIP it synchronizes once
per group whatever its `sync` argument says: prefill_per_group_sync), a stage-0 window only advances when this same
thread serves its doorbells, so the host sat in hipStreamSynchronize and the window it waited for could never
finish: the first request of a fresh engine read its prompt, produced 0 tokens, and the watchdog ended the engine
60 s later (gfx1201 + gfx1030, issue #5147). Neither 20 s guard in Verifier::service / done can fire, because the
loop never runs again.

serve_stage0_to_end() serves stage 0's windows to their end before that prefill, when the drafter shares stage 0's
card. The HIP per-group synchronization (which exists because the unsynchronized E-4 queue hangs on gfx1201 /
ROCm 7.2.4) is left as it is: the caller is fixed, not the mitigation. Pipeline windows 0 and 1 take no new path
(mtp_first is false there). The overlap lost around each drafter prefill has not been measured.

Not testable on the boxes of this round: it needs a mixed pair of HIP cards (gfx1201 + gfx1030) and a two-stage split;
p620 has one R9700. Compiles on CUDA (z440, sm_86).

Co-authored-by: cloud4t0r <[email protected]>
2026-11-05 23:57:51 +01:00
Niko1221andquantforge-br f225844d19 Prompt MMQ row tables get the stream-k fixup's over-read margin, sized from the backend's tile bound (P1.9, part 1)
Correctness (class E, not a speedup): the product values do not change; the rows past T*K are never routed to.

llama.cpp's mul_mat_q_stream_k_fixup loads ids_dst[col_low + jt*J + j] for a whole J-column tile before it compares
the tile with the expert's rows (the main kernel skips an empty tile first; the fixup only skips its writes), and its
tiles run to ceil(max_rows / J) * J for every expert of the launch. A table of exactly T*K entries is read past its
end by up to roundup(max_rows, J), and the main kernel reads up to J - 1 past its last row. Strata's `ids_identity`
was an exact-sized allocation: when it ended a 2 MiB page the over-read faulted on the next (Xid 31 FAULT_PDE,
"prefill copy_i32: an illegal memory access" on a long prompt on a CUDA layer split; issue #1468 and others).

PR #5108 added T + 256 rows by hand. Here the margin is derived: J is bounded by ggml's mul_mat_q_switch_J
(J = 8 .. 128 in steps of 8), the tile loops of moe_mmq.cu now use that one constant (kMmqTileColsMax /
kMmqTileColsStep), and the table is T*K + roundup(T, J) + J rows (an expert takes at most one row per token), with
checked 64-bit arithmetic: a size that does not fit fails the carve instead of wrapping to a small table. The same
size is used by carve, bytes_needed_impl and the three places that write the identity (helper rebind, normal prompt,
layer-major), so the estimate and the carve stay identical. The peer prefill's row table, an exact cudaMalloc used by
the same MMQ launches, gets the same margin.

Tests: tests/prefill/mmq_ids_margin_test.cpp enumerates every index the fixup and the main kernel can read for 2000
random layouts (late experts, empty tiles, every J), checks the exact-sized table of before is overrun by the late
expert layout, and the overflow refusals. Builds on CUDA sm_86 (z440).

Co-authored-by: quantforge-br <[email protected]>
2026-11-05 23:46:57 +01:00
40849df0a8 Transfer completion: flag B stays behind queued copies, and the adaptive-copy range covers the whole round (P1.1)
Correctness (class E): kept even without a speedup.

Flag B (PR #5096): the graph waits for "flag B >= layer + 1" and flag B is one counter that only rises. A non-empty
plan raises it from a host function on the copy stream, after its uploads. An empty plan (n <= 0) raised it on the
spot, even when the copy stream still held an earlier layer's, or the other token group's, uploads: the counter
jumped past a layer whose bytes had not landed. Verifier::raise_flag_b_ordered() now raises it through the copy
stream whenever copies were queued in this window, and directly only when none were. The same ordering is used at
the four places that publish an empty plan when the pool did not (run, run_slot_rows, batch_serve, service), which
had the same hazard. A failed host-function launch used to be ignored (flag B never rose and the window hung); it
now stops the engine like a failed upload does, after releasing the GPU waits.

Adaptive-copy bookkeeping (PR #4844): a round planned on the pipelined loop whose loop drained before step 3 is
marked resident on the serial rule, which used the lo/hi/bi of an earlier round, so only that range of swaps was
marked and the others were lost (the slot held the new expert, the table still named the old one). The pipelined plan
now names the whole round. The range arithmetic moved into AdaptiveCopyRange (adaptive_range.hpp) so a host test can
walk the rounds.

Tests: tests/core/flag_order_test.cu (delayed host-function copy standing in for a slow DMA, a consumer kernel that
reads staging once flag B allows it, a negative control that shows the old direct raise releases it early, flag only
rises, nothing queued, graph replay twice, every device; passes on both A4000s of z440), tests/program/
adaptive_range_test.cpp (serial batches for 0, 1, many swaps and several caps, the stale-range loss, empty round).

Co-authored-by: AlanJager <[email protected]>
Co-authored-by: uvlad23 <[email protected]>
2026-11-05 23:39:06 +01:00
a5b820a415 Request accounting and prompt timing marks: yielded prompt legs summed once, admission time without a double count, unknown stays unknown (P0.4)
Reporting only (class E): no engine speedup, no change to what the engine computes.

serve/server.py
- A prompt read that gives way (BYIELD) and resumes keeps the request's external prefix boundary, and the prompt
  time and tokens of every leg are summed exactly once (prefill_leg_stats; PR #5318).
- The admission's DONE times one token and BDONE counts that token but times only the slot: the slot time now
  includes the admission time, and the output token is not counted twice. A figure the engine never reported
  (a close at the admission token, in the slot, a carry back to the solo path) stays unknown (None) instead of
  turning into 0 or into the API's output count (PR #5307).
- The two patches overlap in the request finalization; ended_on_yield is taken before the unknown-figures step so
  the identity check still sees the yield's own record.

PfTimer::mark(): a mark in the phase already running is not recorded (PR #4006). The totals are the same, a
fraction of the event records; under WDDM every record is a submission of its own and the launch gaps they made
were charged to "dequant". The rule is a pure function in prefill/timer_marks.hpp with a host test.

Tests: serve/test_yield_accounting.py (new: two yields, solo and admission yields, cancel after one and two
yields), serve/test_parallel.py BatchLegAccounting (EOS and length finishes, zero slot time, close at the
admission token and in the slot, slot to solo carry), serve/test_request_accounting.py (unknown stays unknown,
explicit zero), tests/prefill/timer_marks_test.cpp (same totals with 18 of 23 records skipped).

Co-authored-by: YusenZhang0601 <[email protected]>
Co-authored-by: shoucandanghehe <[email protected]>
Co-authored-by: Vop3p <[email protected]>
2026-11-05 23:31:14 +01:00
Niko1221 b97433df05 spec: STRATA_DF2_GATE=1 (opt-in): DFlash2's verify window from a per-request calibrated survival (position x confidence bucket, prior C^1.3) against the cost of one more row, instead of a fixed --draft-conf; window_rule_test covers it 2026-11-05 23:18:51 +01:00
Niko1221 64cc20a8e2 Merge f-0215-fit (setup wording) into f-0215-int 2026-11-05 22:47:02 +01:00
Niko1221 1d342f0836 setup: on a unified-memory GPU whose RAM budget leaves no room for a numeric expert cache, say where the cache is sized from instead of warning that it may take the OS's room: "The expert cache is sized from the graphics memory (<N> GB carve-out + shared), not from the RAM." N is the carve-out setup detected (no number when none was). The old warning and its --resident-budget-gib tip are gone; the test checks the new line with 2 GB and 48 GB carve-outs 2026-11-05 22:44:44 +01:00
Niko1221 0e1d440713 Merge f-0215-pk into f-0215-int 2026-11-05 22:36:02 +01:00
Niko1221 19136808f9 Merge f-0215-fit into f-0215-int 2026-11-05 22:35:59 +01:00
Niko1221 2c0ad33f71 Metal prompt: ragged expert tiles on by default for the types that have kernels (STRATA_PROMPT_V6=0 off); drop the unused Mac PLE threading and the tall-tile probes 2026-11-05 21:59:37 +01:00
Niko1221 3cc5714f37 Strix Halo (Windows gfx1151): STRATA_SMALL_UNR=3 and STRATA_GR_Q8_PFS=1 (load-ordering variants of the decode kernels, same arithmetic) join the default table. UD-Q4_K_XL, DFlash2 serve, 4 rounds x 2 passes x 4 prompts interleaved: window time -0.25% (25 of 32 pairs faster, same window and token count in every pair); first-token logits and 220-token outputs identical on 3 prompts for UD-Q4_K_XL and UD-IQ4_XS. Linux gfx1151 and STRATA_DEFAULTS=026 keep their tables; arch_defaults_test updated and passing 2026-11-05 21:19:24 +01:00
Niko1221 687a7d79d7 Metal prompt: host rows beside the GPU (embedding + PLE rows of the next chunk on a thread, parallel PLE gather on Macs), qsa_merge_h (merge writes the fp16 operand), ragged expert tiles (mm_gu6/mm_down6/mm_scan6, STRATA_PROMPT_V6), STRATA_PROMPT_KPROF per-dispatch profile, matmul2d probes 2026-11-05 20:36:54 +01:00
Niko1221 6e31946cbe Strix Halo (gfx1151, Windows): the F16 prompt expert kernels write the down rows as FP16 and a combine kernel reads them (STRATA_PF_DM16, on by default there, =0 turns it off; from the Strix dm16 work patch, extended to the F16 K-quant and IQ kernels). 8K prompt, same engine, interleaved pairs: IQ3_S +2.2% (4 of 4), UD-IQ4_XS +2.6% (3 of 3), UD-Q4_K_XL +1.2% (3 of 4); first-token KL 1e-4 at 8K, 0.02-0.04 on agent transcripts 2026-11-05 19:59:50 +01:00
Niko1221 66b346c29f drafting: Metal gets its own DFlash2 confidence (0.5, BACKEND_CONF) instead of the CUDA slot steps, which wrote 0.8 for the Mac's --expert-cache 64; M5 Max UD-Q4_K_XL +14.3% over MTP spec 2 on six kinds 2026-11-05 19:54:31 +01:00
Niko1221 830732a33d metal: step profile scales every pass by the cool whole-window time; opt-in screens STRATA_METAL_HCLD=1 (HC loads in flight: -10.9%, off), STRATA_METAL_WPF_MIB (weight prefetch beside gr_up), STRATA_METAL_MOE_GU / MOE_DN (expert tile override) 2026-11-05 19:54:31 +01:00
Niko1221 e431a4a8e2 Strix Halo (Windows gfx1151): the V2K grouped decode experts for Q4_K / Q5_K gate/up and Q5_1 / Q8_0 down are on by default (STRATA_EXPERT_V2K=0 turns them off). UD-Q4_K_XL, DFlash2 serve, 16 request pairs: window time -4.0% (16 of 16 faster, mean x1.040), same window and token count in every pair; first-token logits and 220-token outputs identical on 3 prompts (the kernels are bitwise the old ones) 2026-11-05 19:27:35 +01:00
Niko1221 4dbaf703fd Strix Halo (gfx1151, Windows): the K-quant prompt experts of UD-Q4_K_XL (Q4_K / Q5_K gate/up, Q5_1 / Q8_0 down) take the fused F16 matrix-core kernels by default (STRATA_PF_FUSED_KQ=0 or STRATA_GFX1151_DEFAULTS=0 keeps the earlier route). Setup config, n=1: 8K prompt 885 -> 1,387 tok/s, 4K 611 -> 1,316; first-token KL against the earlier route 1e-5 .. 6e-2 on prompts of 1K+ tokens (information), same greedy ids 2026-11-05 18:54:42 +01:00
Niko1221 c4fa1d9e1e metal: STRATA_METAL_STEPPROF=T, the in-situ step profile by part (prefix runs at every barrier group, weight bytes and GB/s per part, distinct experts per layer); run_graph can run the first N dispatches 2026-11-05 18:41:45 +01:00
Niko1221 9f7d977d22 README: a Loyal supporters section under the Sponsor and coffee buttons in all 8 languages, with the public GitHub sponsors as linked profile pictures (first: ryan4yin) and a one-line template for the next one 2026-11-05 14:54:54 +01:00
Niko1221 5d37280db1 Docker: build.sh copies metal/tools/metal_repack.py and stream_experts.py into the build context (the Terminal bundle carries them since the Mac Unsloth sets; the v0.2.11 image build stopped at the missing metal_repack.py); the context test checks every INSTALLER_METAL_TOOLS name 2026-11-05 14:02:27 +01:00
Niko1221 dc9dfcfd29 GitHub Sponsors: FUNDING.yml names github: Niko1221 (the repository's Sponsor button), and every README gets a Sponsor badge beside the coffee badge, a Sponsor button in Support Strata and a support line that names both 2026-11-05 14:02:27 +01:00
Niko1221 c3b3f50355 Merge f-0214-int d7f5c6dd1 (Strix first-launch plan fix: expert cache auto on UMA, Python only) into f-0211-rc 2026-11-05 12:54:06 +01:00
Niko1221 d7f5c6dd11 Merge f-0214-strixplan 0c0b7b875 (Strix plan fix) into f-0214-int 2026-11-05 12:53:44 +01:00
Niko1221 0c0b7b8751 setup: uma_holds_experts is never true on a Mac (the signed Mac plans stay exactly as they were); test 2026-11-05 12:29:14 +01:00
Niko1221 86ffdbeb17 setup: on a unified-memory GPU a v3.1 Unsloth set whose experts fit the shared pool gets expert cache auto and nothing pinned (UD-IQ2_XS's first launch failed with the fixed cache and the pinned part on a 64 GB Strix Halo); the other sets keep their plans; tests 2026-11-05 12:25:53 +01:00
Niko1221 aac067021c Merge f-0214-int e2b53784 (METAL test-only change: metal/tests/moe_mm_parity.cpp) into f-0211-rc 2026-11-05 11:25:35 +01:00
Niko1221 e2b5378403 Merge f-0214-metal ba5bef2d6 (parity test: IQ4_XS gate/up runs v2) into f-0214-int 2026-11-05 09:42:34 +01:00
Niko1221 ba5bef2d61 moe_mm_parity: IQ4_XS gate/up runs the v2 path (v1 is not an engine path and has no IQ4_XS kernel) 2026-11-05 09:41:13 +01:00
Niko1221 3e0210663b Merge f-0214-int 437668151 (round 0214: the new Unsloth Qwen quants, the Q5_0 dot fix, the Mac app branch) into f-0211-rc 2026-11-05 09:39:50 +01:00
Niko1221 437668151d Merge f-0214-macapp 408a5ca37 (Mac app offers Unsloth sets) into f-0214-int 2026-11-05 09:37:11 +01:00
Niko1221 0bdcef4682 Merge f-0214-engamd aa678f10d (Q5_0 expert-dot fix) into f-0214-int 2026-11-05 09:35:26 +01:00
Niko1221 408a5ca37b tests: the Mac model list includes Unsloth's sets 2026-11-05 09:33:32 +01:00
Niko1221 10df7ace1d tests: the Mac app's running-engine hint 2026-11-05 09:25:00 +01:00
Niko1221 d6eaab500f Mac app: the running-engine hint says to unload the model in Strata (approved wording); test 2026-11-05 09:24:49 +01:00
Niko1221 aa678f10d0 Q5_0 expert dot: the -16 offset takes the sum of the quantized activations (CUDA/HIP and SYCL), like Q5_1 and Q4_0
vec_dot_q5_0_q8_1 used the q8_1 block's ds.y (the sum of the original activations). With the SwiGLU outputs (not zero-mean) the whole-expert error with Q5_0 down was 2.1e-2 .. 3.1e-2 against 1.2e-2 for every other down type. Now 1.2e-2, and the GPU agrees with the CPU path. The Q5_0 parity limit is tightened to 1.8e-2.
2026-11-05 09:07:47 +01:00
Niko1221 af1ffd913a Mac app: Unsloth's sets are offered and installed as the Terminal's setup_mac does (metal_repack to one GGUF, --ple-gguf the table's shard, disk counts the repacked file); the repack tools ship in the installer bundle; tests 2026-11-05 09:05:01 +01:00
Niko1221 edb7c4819b native_grouped_parity: pin the CUDA layout (STRATA_EXP_MODE=0) on gfx1151, where mode 8 and the strided call sum in a different order 2026-11-05 08:54:01 +01:00
Niko1221 74a4441354 Merge f-0214-engamd into f-0214-int 2026-11-05 08:53:25 +01:00
Niko1221 bee9168114 Merge f-0214-engnv into f-0214-int 2026-11-05 08:53:25 +01:00
Niko1221 f699fc0a7d Merge f-0214-metal into f-0214-int 2026-11-05 08:53:24 +01:00
Niko1221 96d48dff45 Merge f-0214-catalog into f-0214-int 2026-11-05 08:53:24 +01:00
Niko1221 99cbdd5592 tests: the Terminal model list needs a taller screen for the seven new rows; adopt's unsupported-family example is nvfp4 2026-11-05 08:45:15 +01:00
Niko1221 7faeb8c264 Parity tests: v1-vs-new grouped diagnosis (differing words listed), v1 against the float reference in native_expert_parity, Q5_0 gets 32-value scale blocks in the grouped test's random weights 2026-11-05 08:41:41 +01:00
Niko1221 39ea3e5216 launch: Unsloth's sets are written for an Intel Arc card (adopt.FAMILIES), without --ple-gguf; test 2026-11-05 08:30:25 +01:00
Niko1221 60ab1e52f3 catalog: the MCP tool description says Unsloth's 1- to 8-bit files 2026-11-05 08:11:01 +01:00
Niko1221 bfbe7fab2b MMQ parity screen: skip the trellis types when the ggml has no host traits for them (it read past the type table on Windows) 2026-11-05 08:06:52 +01:00
Niko1221 85e7c83d96 catalog: the Unsloth family credit line reads 1- to 8-bit 2026-11-05 08:05:38 +01:00
Niko1221 199fbc7a45 catalog: approved wording (about lines, SUPPORTED_GGUFS, UD-IQ1_S reason, Unsloth family line); METAL_Q5_0_DOWN on (f-0214-metal cbef1a9ff) so the Mac lists all seven sets 2026-11-05 08:04:15 +01:00
Niko1221 9ed3c77899 catalog: Unsloth v3.1 Qwen3.8-Flash-Next sets (UD-IQ1_M, UD-Q2_0, UD-IQ2_XS, UD-Q2_K_XL, UD-IQ3_XXS, UD-Q3_K_XL, UD-IQ4_XS-v31) at d9620119 with a per-model revision; UD-Q4_K_XL's four-shard layout accepted from local files; manual-load refusal lifted (UD-IQ1_S stays refused); mac_fit switch for the Q5_0 down sets; header-only fixtures and tests (about lines pending wording) 2026-11-05 07:23:41 +01:00
Niko1221 c1d5e4d5e5 Tests for the new Unsloth Qwen3.8-Flash-Next expert and dense type combinations (HIP, CUDA, SYCL)
native_expert_parity: --bench decode micro-number, +0/-0 is no fp16 difference.
Registers every gate/up x down pair of the new sets (CUDA and HIP, SYCL), a dense type test
(native_dense_types_parity), Q5_0 down in native_grouped_parity, new-set types in the MMQ screen,
and a --synthetic mode in the SYCL native_expert_parity.
2026-11-05 07:22:34 +01:00
Niko1221 14a4636b83 native_dense_parity: Q4_0/Q5_0 decode tolerance 4e-2 (offset term uses the unrounded activation sum, as llama.cpp CUDA) 2026-11-05 07:19:16 +01:00
Niko1221 5d56ff8c06 Parity tests for the Unsloth v3.1 Qwen3.8-Flash-Next quant mixes (CUDA)
- native_expert_parity: ctest entries for every gate/up x down pair the new sets use
  (IQ1_M, IQ2_XXS, IQ2_XS, IQ2_S, IQ3_XXS, IQ4_XS gate/up; Q2_0, IQ4_NL, Q5_0, Q8_0 down)
  at hidden 2560 / FF 640.
- native_dense_parity: dense Q2_0..Q6_K, IQ4_NL, IQ4_XS decode (MMVQ, 1-8 columns) and the
  prompt dequantizers (f32, f16, bf16) against ggml.
- prefill_mmq_v31_test: the new expert formats through MMQ (Q5_0 only in a
  STRATA_ORCA_Q4KS_MMQ build), plus a --bench of Q5_0 MMQ against dequantize + cuBLAS.
2026-11-05 07:18:29 +01:00
Niko1221 cbef1a9ff6 Metal: Q5_0 down experts, decode (moe.metal, moeu) and prompt (moe_mm v1/v2/v3), host tiles, honest gu_qk/d_qk; parity tests take Q5_0 2026-11-05 07:14:30 +01:00
Niko1221 2b8179f3e6 Merge f-0213-int ce4197ce8 (AppImage ELF check: an ELF test instead of 'every ET_DYN is a static PIE', #4337) into f-0211-rc 2026-11-05 05:29:02 +01:00
Niko1221 ce4197ce81 appimage_elf: a shared library is not a static PIE; the check tells them apart without name rules (#4337 checker)
The checker treated every ET_DYN without PT_INTERP as a static executable, so the RUNPATH $ORIGIN that Pillow, numpy and sharp libraries carry was
reported as 'static PIE with a damaged dynamic section' (35 files in 0.2.11, 18 Pillow libraries in the published 0.2.10 Control; false positives).
A static PIE is now an ET_DYN with no interpreter, a non-zero entry point and DF_1_PIE in DT_FLAGS_1 and no DT_SONAME; a library (no entry point, or a
SONAME and no PIE flag) is never checked. A candidate whose dynamic section cannot be read is reported as damaged, because the overwrite that crashes
rg also erases the flags, so the test for 'executable' uses the entry point, which the damage does not touch. Tests (tools/test_appimage_elf.py, 16, run
on p1006): shared libraries with and without a SONAME, entry point or RUNPATH pass; a static PIE passes; a static PIE with a 0x5A .dynamic or an added
RUNPATH fails; a real gcc shared library with RUNPATH $ORIGIN passes; a real gcc static PIE passes and its overwritten copy fails. Build tool only.
2026-11-05 05:24:48 +01:00
Niko1221 3fda37bd8c Merge f-0213-int 3c4afa8d7 (Models page: no Connect engine / Download & load flash after a start) into f-0211-rc 2026-11-04 23:37:34 +01:00
Niko1221 3c4afa8d78 QAGUI #2 (source): /api/native says checking until Control has answered; a failed start-time read is retried in 3 s, not held 45 s; page shows Checking, not Connect engine 2026-11-04 23:26:47 +01:00
Niko1221 975eaab2bb Version 0.2.11 / engine 0.1.57: Terminal and app 0.2.11, Control and every engine package 0.1.57 (the 0.2.10 bump list: strata_cli, apps/host, apps/desktop, Cargo, CMake, Metal, the build scripts, the version tests, AGENTS.md, the bug-report template) 2026-11-04 23:23:33 +01:00
Niko1221 0fa4288d30 markdown: keep the list line as it was written (a source guard in ui-do.test.mjs reads it) 2026-11-04 22:34:01 +01:00
Niko1221 90c77357fb markdown: a file with Windows line ends no longer runs the page out of memory (follow-up to 68bead108)
The sidebar now shows a file's current content, so a CRLF file reaches the Markdown renderer. Every line then ends in a carriage return, which '.'
does not match: a list line passed the list-start test but not the item pattern, the list loop took no line, and the outer loop never advanced - an
endless loop pushing nodes (150 MB to 4.4 GB in 20 s on a 60 byte file; QAGUI final pass, the agent's Add-Content writes CRLF). 0.2.10 never showed
the changed text so it never met it; the bug is older and any CRLF Markdown list opened in the preview would hit it.
Line ends (CRLF, CR, LF) are read as one in the renderer and in the preview slicer, and the list loop always moves on (a line the item pattern rejects is
shown as text). Test: tests/markdown-crlf.test.mjs renders in a child process with a 200 MB cap and a 20 s limit; on 68bead108 all 4 cases abort out of
memory, with the fix all pass. Edge 156, seeded live turns (stream, finish with the card, file rewritten with CRLF, cards opened 6 times): heap stays
near 19 MB with the fix. No visible text.
2026-11-04 22:10:41 +01:00
Niko1221 1814da7c44 Merge f-0213-overlay (aba888972) into f-0213-int 2026-11-04 21:49:55 +01:00
Niko1221 64b48c0d00 Merge f-0213-qafix into f-0213-int 2026-11-04 21:49:19 +01:00
Niko1221 5d363f11ea Merge f-0213-qatui into f-0213-int 2026-11-04 21:49:19 +01:00
Niko1221 aba888972a select: the list is placed by the shared overlay function, as tall as it needs and no taller than the room above or below its button
The Select popup (every dropdown in Settings, Models and the tune dialogs) was capped at 320 px whatever the window and opened upward through a
transform, so in a short window or at a large Interface size it scrolled although the window had room, or started above the top of the window
(Edge 156 matrix: Select popups 4 of 72 and 3 of 72 cases right on 548fdf061, 69 of 72 and 61 of 72 after; the rest are harness not-open rows).
It now uses lib/overlay-place.ts like the menus, measures its real height once it is on the screen, and writes maxHeight. The Mode menu and the
sidebar More menu already passed on 548fdf061 (77 of 77, 84 of 84; 43 of 77 and 49 of 84 on v0.2.10). No visible text.
2026-11-04 21:33:24 +01:00
Niko1221 a87c399c91 F12: French s9038 uses the typographic apostrophe (french-catalog test) 2026-11-04 20:35:53 +01:00
Niko1221 e10be676b1 F13: the ? key list wraps at the keys on narrow terminals (80x24 and 60x16) so every key shows 2026-11-04 20:31:12 +01:00
Niko1221 0269e2c42a F13: the monitor's ? key list stays up on every view instead of vanishing at the next poll 2026-11-04 20:26:01 +01:00
Niko1221 6f5cf9f6b7 nvidia-smi is asked once per field and 30 s: one launch --plan on Windows with an engine installed started it 19 times 2026-11-04 20:24:56 +01:00
Niko1221 be2d275361 QAGUI #2: Models page waits for Control's installed list before offering Download & load 2026-11-04 20:24:31 +01:00
Niko1221 c7272c70db QAGUI #1: start-up reads have a time limit and bounded retries; optional reads no longer hold back live 2026-11-04 20:23:27 +01:00
Niko1221 8768e95f12 F12: port-denied text names the right cause on Linux and macOS (QATUI F12)
Windows keeps the Hyper-V/firewall text. serve listen_problem and the host
ports.mjs pick by platform; new locale key s9038 in 9 languages.
2026-11-04 20:21:11 +01:00
Niko1221 548fdf061e Merge f-0213-life (3e009d2b7, process-tree test hardening) into f-0213-int 2026-11-04 19:15:09 +01:00
Niko1221 3e009d2b75 tests: process-tree waits on the real conditions (a process that is running, not a zombie; the child's own ready line; a marker file) with long limits instead of fixed delays 2026-11-04 18:59:35 +01:00
Niko1221 e82c706981 Merge f-0213-preview (68bead108) into f-0213-int 2026-11-04 18:46:17 +01:00
Niko1221 68bead1082 preview: the sidebar shows the file's current content after it changed on disk (follow-up to #5180)
The preview kept a file's first content: openPreview dropped the freshly read item when its id was already a tab, the panel's loading effect had no
content version, and the effect that re-adds a message's artifacts put the stored first copy back. All three change together, through pure rules in
lib/preview-merge.ts: opening a changed file replaces its tab in place; a message's artifact only adds a missing tab or replaces an older, different
copy (each copy carries the time it was read or made) and never overwrites a newer one; both return the same array when nothing changed, so no update
and no loop follows. The panel reloads when (id, content version) changes: a hash of the text, or the host's mtime and size for pictures and PDFs
(/api/files/preview now sends mtime). The reading view is reset by the file, not by its content. Tests: stale case, 2000 alternations without a
change, one load per real change. Real Edge 156 with the real panel: the old rules keep Version One, the new ones show Version Two through 300
sync/render alternations with 0 Markdown renders. No visible text.
2026-11-04 18:45:06 +01:00
Niko1221 85269ca155 Merge f-0213-guidata (final 1d5096beb) into f-0213-int 2026-11-04 18:43:27 +01:00
Niko1221 1d5096beba Privacy note: conversations stay on this computer, in Strata's data folder (owner wording; all nine languages) 2026-11-04 18:33:30 +01:00
Niko1221 e647e132d5 locale-gaps: the notice catalog (sav-en) is a known catalog (#5192) 2026-11-04 17:59:26 +01:00
Niko1221 8ad605789b agent turn: a reloaded page takes a running turn over again; chats resume after they are read from the host
A reload does not stop an agent turn, but the page that came back showed "Network error. Try sending the message again."
with no Stop button, and a new message was refused with "This chat is still working". The host now answers GET /api/chat/active
(the chats with a turn running) and POST /api/chat/attach: a running turn sends one catch-up (the start, the text and thinking
so far as a resync frame, every tool card as it is now with the approvals waiting for a yes, the open questions) and then its
live frames; a turn that ended while the page was away is shown whole once, named by the request id of the cut-off reply.
At start the page asks for the running turns, takes each over in the reply it already has (running, Stop button, the
failure it wrote is cleared), and asks once about cut-off replies of the other recent chats; nothing to take over leaves the
reply as it was. Existing states and texts only.

The things that used to run at bootOnce from the chats in memory (follow time-skip runs, end loops the last window left running,
mark tool cards of vanished turns) now run once the chats have been read from the host (resumeSessions), because the chats are
no longer in the page at that moment. The save retry waits a little longer each time (up to a minute) and never keeps a closing
page alive. Also: tests for a second window's changes (applyChatChange) in the parallel-chats test.
2026-11-04 17:48:03 +01:00
Niko1221 792b626980 Merge f-0213-secure (fix 0855796e6) into f-0213-int
Conflicts in strata_cli/bench_facts.py (the redactor), three hunks: kept every side. The patterns stay QATUI's (the key and the names are matched as whole tokens, a hyphen next to the key does not hide it, the names are not touched inside a label the function wrote), each entry carries SECURE's third field (is a login or host name) for redact_document, and the host names come from socket.gethostname() and sysinfo.node() (LIFE), both marked as names.
2026-11-04 17:40:48 +01:00
Niko1221 cd66956ca6 Merge f-0213-tuicfg (fix 71395ec9c) into f-0213-int 2026-11-04 17:38:00 +01:00
Niko1221 16904848bc setup: the low-RAM mode's copy of the experts is checked for room where it is written (the data folder's packs folder), and the refusal names that folder in the approved words instead of the model folder; test 2026-11-04 17:35:55 +01:00
Niko1221 0855796e68 bench: results.json redacts the machine's login and host name only in the text people and servers wrote, not in the report's own words (#4484)
On a PC whose login is an ordinary word ("user", "test", "admin") the whole-document pass turned status reasons and field names into <user>, and the report failed its own validation. Paths, addresses and the key are still replaced everywhere; the name rules apply to hardware, model and configuration text, error messages, engine options and request checks.
2026-11-04 17:35:01 +01:00
Niko1221 71395ec9c7 setup: the pinned-shard mark helper needs no 'hf' key (the NVFP4 families have none) (#4088)
main() and mac_control_setup() read fam['hf'] to build the pin lookup URL; the NVFP4 families download a file list and have no such key, so every NVFP4 install raised KeyError.
The helper takes None and then keeps the old mark. Found by the INT3 merge run (tools/test_setup_nvfp4.py, tools/test_setup_dl_fix.py).
2026-11-04 17:33:39 +01:00
Niko1221 9cb1dc6454 App Host: the start-up probe for the share port listens on the loopback address, not on 0.0.0.0 (it made Windows Defender Firewall ask 'allow Node.js on these networks?' on the first start of every new install path); test records every listen() of a started host 2026-11-04 17:33:27 +01:00
Niko1221 b247f4ecda Merge f-0213-qatui (final d33d45ade) into f-0213-int 2026-11-04 17:33:14 +01:00
Niko1221 d33d45ade2 update --check on a source checkout prints the tree's own version for Control instead of '?' (display only; decide_control is unchanged) 2026-11-04 17:20:23 +01:00
Niko1221 cabb271145 launch: 'A new API key was made' is said only once the run config holding the key has been written (never in the plan or before a refusal) 2026-11-04 17:19:20 +01:00
Niko1221 094bb3f0a3 chats are files in the profile's data folder, not browser storage: the 5 MB limit is gone (#5192)
The app host saves each chat as its own file (data/chats/<id>.json, an index, a version and a revision in
every file), written to a temporary file, flushed and renamed over the chat, so a crash or a full disk in
the middle keeps the older version whole. The folder and files are owner-only. The page talks to the host
through /api/chats (the page must name itself with x-strata-client; other sites are refused), sends only the
chats that changed, never cuts anything, and keeps a chat the host cannot write whole in memory, retries it,
and says so with the notice and the "Copy unsaved chats" button in all nine languages. A save from an older
copy (a second window) keeps the version it displaces in chats/conflicts. The browser's two older layouts
(the 0.2.10 list and the per-chat entries) are handed to the host once at start and left untouched as a
fallback for one release; a last-moment copy covers a page that is closed before the host confirmed a save.
Other windows are told by an event and take over what changed.
2026-11-04 17:15:42 +01:00
Niko1221 0481347ec0 bench redaction: a key next to a hyphen is replaced (it leaked in results.json), and redacting twice gives what once gives (no <<user>>) 2026-11-04 16:04:18 +01:00
Niko1221 b75e8a975d launch: a profile or run config of the launch that cannot be read stops it before anything is written, so the API key is not replaced (F1, found on l620); owner-approved wording 2026-11-04 16:04:18 +01:00
Niko1221 2204582932 settings file check: args_written, user_args and gpus_by, which setup itself writes, are known keys (a freshly set-up model showed 'args_written is not a setting Strata reads' in the settings panel); test reads SETUP_KEYS from setup.py 2026-11-04 16:00:01 +01:00
Niko1221 22a3d70688 Merge f-0213-guidata (4c4596746) into f-0213-int 2026-11-04 15:59:53 +01:00
Niko1221 d8b6ce356c Windows: the display-adapter CIM query is asked once per process and 2 minutes instead of 18 times in one launch --plan (a stuck WMI cost 18 x 15 s, #4975) 2026-11-04 14:45:52 +01:00
Niko1221 d73ce8608e bench run --profile: SIGTERM ends the run like Ctrl+C, so the server and engine it started are stopped; an interrupt during the model load stops them too (found on l620 and m1001) 2026-11-04 14:45:52 +01:00
Niko1221 127d829018 Merge f-0213-life (final 74f390538) into f-0213-int 2026-11-04 13:43:15 +01:00
Niko1221 4c45967460 chat titles are cut at a character through shortTitle, not in the middle of an emoji (#4533) 2026-11-04 13:34:07 +01:00
Niko1221 74f390538c serve: the error of a request that ended without an answer says the next request restarts the engine only where the engine was really ended (#3961) 2026-11-04 13:34:06 +01:00
Niko1221 50c2db6905 merge f-0213-guidata-shell (#5180 #5079 #5071 #4653 #4533 #4337) 2026-11-04 13:33:50 +01:00
Niko1221 a8767488ce serve: a request that ended without its end event ends the engine only if it is still the process that request ran on and nobody has the turn, so the next request is never killed with it (#3961) 2026-11-04 13:18:36 +01:00
Niko1221 f4d8688e8c merge f-0213-guidata-avail (#4397 #4535 #4221 #4878 #3750 tests) 2026-11-04 12:27:00 +01:00
Niko1221 4d482e627c Merge f-0213-own (final daf4ac087) into f-0213-int
Conflict in serve/server.py main(): LIFECYCLE wraps _main in the shared activity lease (#4463), OWN wraps it in the decoupled console (#4479 / #2481). Kept both: the lease is taken first, and _main runs inside decoupled_console() unless STRATA_BLOCKING_CONSOLE=1; the lease is closed in the same finally.
2026-11-04 12:26:56 +01:00
Niko1221 66331f9bc0 Merge f-0213-life (final 576d06553) into f-0213-int
Conflict in strata_cli/bench_facts.py (the redaction helper): kept both sides' intent. The host name comes from sysinfo.node() (LIFE) and the key is redacted with usable_key(key), word=True, exact=True (SECURE).
2026-11-04 12:24:01 +01:00
Niko1221 daf4ac087a serve --batch: the drain and quarantine limits count silence, never progress (#4451 follow-up)
DRAIN_TIMEOUT_S (300 s) and QUARANTINE_S (600 s) apply only after a request has left and the server has sent STOP / BSTOP:
_drain_control (the finally of generate_batched), _quarantine_control and the BDONE wait of _release_slot_when_done. They
never cover a generation a client is still reading. But they were counted from the start of the drain, so an engine that
was told to stop and kept producing lines (an older engine runs on to max_new; a long prompt read ends at its next chunk)
could be ended after 900 s, with every other request in it. Now every line the engine writes starts the limit again: only
an engine that says nothing at all for 300 s + 600 s after the stop is ended.

Tests (fake engine, limits scaled to 0.3 s + 0.4 s): an engine that keeps talking for ~1.8 s after the stop is not ended and
the request in another slot is untouched (fails with the previous commit's absolute limits); a silent one is ended; the
drain-timeout leak test now really closes the stream mid-read (the old version was ended by a BYIELD before the drain);
long replies, far over the limits, complete. Also: a client that is idle while the engine thinks is not cut by the 60 s
write limit (0.3 s between tokens, 0.1 s limit, the whole reply arrives).
2026-11-04 12:18:12 +01:00
Niko1221 576d06553c launch: _host_platform imports sysinfo (the earlier edit left the name undefined, #4975) 2026-11-04 12:10:31 +01:00
Niko1221 2bfd151578 tests: the adopted-load test lets Control's first answer be 'starting' and the next 'failed' (#4293) 2026-11-04 11:59:36 +01:00
Niko1221 049ae1c678 Merge f-0213-tuicfg (final c6dda74bf) into f-0213-int 2026-11-04 11:45:45 +01:00
Niko1221 39851463bb serve --batch: vram() and session_file() take the control lines like a request (#4451, same class)
On a batch engine the control lines carry one exchange at a time and every request takes `ctl` before it sends or reads
there. VRAM and SAVE / RESTORE read the same lines for their answer without it (POST /v1/vram took it itself, the image
encoder's cache resize, the load-time reserve and the session handler did not): run beside a request they took its
T / PP / DONE lines, and the request lost its reply (or session_file ended the engine as out of step).

StrataEngine.vram / session_file now run under _exclusive_control(): ctl on a batch engine (a thread that already holds it,
as Service.vram does, is marked and does not wait for itself), nothing to take for one request at a time. Waiting longer
than CONTROL_WAIT_S raises the existing "a request is still running; try again when it has finished". No output changes.

Test: the fake engine answers VRAM / SAVE / RESTORE; a request streams while the call is made. On the base the request
ends with 212 of 421 tokens; with the fix it is complete and the call is answered.
2026-11-04 11:43:02 +01:00
Niko1221 3c95402c4f tools: stream_ownership_probe.py - interleaved streaming clients with canaries and random disconnects against a running server (#4451, #2892)
Each client puts its own random canary in its prompt and asks the model to repeat it; some leave before the first byte,
during the prompt read or after a few chunks. Every text a client receives (content and reasoning) is searched for the
canaries issued in the run: a canary that is not the client's own is a cross-delivery (exit 1). Used for the real-engine
A/B of the round 0213 ownership fixes.
2026-11-04 11:35:16 +01:00
Niko1221 b776e1cd02 serve: console output goes through a bounded queue, so a blocked console cannot hold a request (#4479, #2481)
print() in the server blocks when the console stops taking output (a Windows console in selection mode, a paused
terminal, a launcher that stopped reading the pipe), in whatever thread called it - including those that own the
engine's turn, a cancel or an admission. While serve main() runs, stdout and stderr (when they are the process's own)
are replaced by serve/nonblocking_out.NonBlockingOut: write() queues and returns, one daemon thread writes to the real
stream, the oldest text is dropped (and counted) when the queue is over 1 MiB / 4000 chunks. Restored and drained at the
end of main(). Protocol output (engine pipe, HTTP) is not touched. STRATA_BLOCKING_CONSOLE=1 turns it off.
No visible text is added; a drop is counted, not announced.

Tests: a plain print to an unread pipe blocks (control); the wrapper never blocks, keeps the newest lines in order, and
drains. Approach credit: the proposal in #4479 (one process-owned output consumer).
2026-11-04 11:26:31 +01:00
Niko1221 c08fd2ecc8 serve: a streaming client that stops reading no longer holds the engine's turn (#4271, part 1)
The thread that writes the SSE stream to a client also holds the engine's turn (the one-request path). A client that
stops reading without closing (a hung process, a dropped network) filled the socket buffers and the write blocked until
the OS gave the connection up: the engine idle, every other request waiting. A write that makes no progress for
STREAM_WRITE_TIMEOUT_S (60 s; STRATA_STREAM_WRITE_TIMEOUT_S, 0 = off) now ends as a disconnect: the engine is
stopped and the turn given back. No text changes.

Test: a client that never reads a 400k-token stream; on the base the turn is still held after 20 s, with the fix it is
given back at once. Part 2 of #4271 (arrival order of the one-request path: the turn is a plain Lock) is not done here.
2026-11-04 11:26:30 +01:00
Niko1221 97da46cc1c Merge f-0213-secure (f374b5029) into f-0213-int 2026-11-04 11:21:31 +01:00
Niko1221 f1fd753de8 serve: a request waiting for the control lines does not miss a wake-up (#4738)
_take_control read its turn, gave up the lock and then waited: a notification sent in between was lost and the waiter
slept up to 0.5 s although it could go. It now reads the turn (and the short-first arbitration) again under the lock
before waiting, and a waiter that leaves (cancelled, closed) notifies the others.

Approach credit: PR #4738. Tests are new (a hooked Condition forces the schedule; on the base the waiter waits).
2026-11-04 11:16:42 +01:00
Niko1221 b3c6338689 serve: a refused batch admission is an error row, not a completed request; engine output with a non-UTF-8 byte is read (#5044)
- GpuBusy before any segment and with zero tokens: the history row says "error" with no prompt tokens and takes no part
  in the totals, latencies or last timings (#3040); a continuation after native work keeps the old accounting;
- the engine's and the image encoder's stdout are decoded with errors="replace": on Windows a byte such as 0x8e ended
  the reader thread (UnicodeDecodeError) and the server lost the engine's output. Protocol lines stay ASCII.

Not taken from PR #5044: the q36 engine OOM messages (engine C++, and the wording goes to the coordinator first).
Approach credit: PR #5044 (Python parts). Tests: test_parallel (refused admission accounting), test_serve_robustness
(a 0x8e byte before READY and in a reply).
2026-11-04 11:16:42 +01:00
Niko1221 0d922698f2 serve: a failed engine start no longer crashes when another thread closed the process (#4792)
StrataEngine.__init__ cleaned up a start that ended before READY through self.proc; restart()'s close() on another
thread had set it to None (AttributeError: NoneType has no attribute stdin, a recoverable engine abort became a 400).
The cleanup now uses the process object of THIS start, captured once right after it was spawned.

Approach credit: PR #4792 (same idea; the test is new: the closing thread is forced inside proc.wait()).
2026-11-04 11:16:42 +01:00
Niko1221 3f6960f1fc serve: ConvCacheLog.poll runs under a lock (#3443)
Polls come from several request and monitor threads. A second poll moved `pos` between the first one's size check and
its read: lines were read twice (parks counted double) and a read length could go negative (ValueError). Found by
HWSTALL while triaging #3443.

serve/test_serve_robustness.py: the overlap is forced with a hooked getsize; on the base every parked line is counted
twice (8 != 4).
2026-11-04 11:16:41 +01:00
Niko1221 65cff54c0d serve --batch: every frame and the end of an exchange belong to one request on one engine process (#4451)
A --batch request read its slot's lines from `self.slot_q[slot]`, the control lines from `self.lines`, and sent its
BSTOP / BYIELD / STOP with the slot number, all looked up when used. After the engine was restarted the same names meant
the NEW process: a request of the old one read (and could stop) the new one's requests. And a request that left before
the engine had ended its exchange (a drain that timed out after 300 s) gave the slot and the control lines back, so the
engine's late T / PP / DONE / BADM / BT frames reached the next request as its own.

- the request notes the queues of the engine process it was admitted on and reads only those; a send meant for that
  process (BSTOP, BYIELD, STOP) is not sent to a later one (EngineDied instead); slot state is only given back on the
  same process;
- an exchange that is not drained in DRAIN_TIMEOUT_S is quarantined: the control lines and the admission's slot stay with
  it, in the background, until the engine gives DONE / BADM (then a continued admission is BSTOPped and freed at its
  BDONE); if the engine never does within QUARANTINE_S it is ended, as a silent engine is (#481). A slot's BDONE wait that
  times out ends the engine too, instead of freeing a slot whose lines may still come;
- the pump drops BT / BDONE frames for a slot no request has claimed (counted in stale_frames);
- _ctl_mode, _ctl_result, _yielded, _last_done, _ctl_erred are per request thread on a batch engine;
- serve/test_stream_ownership.py (BatchOwnership): interleaved clients with canaries, a cancel in the middle of the prompt
  read followed by reuse, a drain that times out (DEAFSTOP), the engine ending during a drain and restarting. On the base
  the restart case fails: requests of the old process stay attached to the new process's slots.

The other cases of the register (mid-prefill cancel, drain timeout, slot not handed on) pass on the base with the fake
engine; they are kept as regression tests. The real-engine check follows.
2026-11-04 11:16:19 +01:00
Niko1221 66385404ff replicas: a request owns its replica's pipe until its native exchange is over; DONE figures are the caller's (#2892)
ReplicaEngine.generate released the per-replica run lock in its finally without closing the child generator. The
child's own STOP and drain-to-DONE ran later, when the wrapper frame was collected, after the next request had sent
its GEN: the two read each other's tokens and DONE (an empty reply, or another request's text), and a cleanup error
was lost as an ignored generator exception. The unbatched engine's shared `last` was also read by Service after the
lock was free, so a reply could carry the next request's figures.

- close the child before the lock is given back; a close that fails ends the replica (heal() starts it again) and the
  error still reaches Service (EngineSilent handling is unchanged);
- copy the new DONE figures into a per-caller object while the replica is owned; with no new DONE the caller's earlier
  object stays, so an old result is never recorded as new;
- serve/test_stream_ownership.py: canary replies from a fake engine, a hooked run lock that forces the legal schedule
  (the successor takes the replica right after release), 30 forced hand-offs and 60 unforced back-to-back requests.
  On the base: empty reply for the successor, the predecessor's canary in its text, 30 of 30 forced hand-offs crossed,
  figures of the other request.

Approach credit: PR #2886 (scheme A: explicit child close, caller-local DONE snapshot). Written in this tree.
2026-11-04 11:15:36 +01:00
Niko1221 632b611df9 packaging: check the static PIEs of an AppImage (the bundled ripgrep) and gate the repack on them (#4337)
linuxdeploy rewrites the RPATH of every ELF in the AppDir; for a static PIE (the bundled rg) that leaves .dynamic overwritten and the program does not
start (shown on a real gcc static PIE in tools/test_appimage_elf.py). repack-appimage.sh already put the staged bytes back, but --check covered only
engine-bundle and node, nothing ran the rg, and nothing stopped a pack with a damaged one. appimage_elf.py scans for a damaged .dynamic (garbage tags,
RUNPATH on a static PIE, filled relocation table, filler runs) and compares to the staged files; --check now covers control, harness, node and engine-bundle,
scans, and runs the bundled rg; the pack path runs the same check before it squashes. Whether the released AppImages were repacked is outside this repo.
2026-11-04 11:11:56 +01:00
Niko1221 f8f97d9f36 titles: a chat title is cut at a character, not in the middle of an emoji (#4533, helper only)
shortTitle() cuts at a grapheme (Intl.Segmenter) or a code point, so a title never ends in half an emoji, a lone surrogate or a ZWJ. lib/store.ts still
cuts with prompt.slice(0, 47) in three places (not edited here: the GUIDATA owner's file); they should call shortTitle. Tests cover ordinary emoji,
ZWJ families, flags, skin tones, keycaps, combining marks, Arabic, Hebrew, CJK, and a JSON save and restore.
2026-11-04 11:11:56 +01:00
Niko1221 5af35d050e overlays: menus and popovers are placed in screen pixels and written in the page's pixels, so the sidebar More menu opens at any Interface size (#4653)
Interface size scales the page with CSS zoom on the root. In WebView2 (Edge 156) getBoundingClientRect and innerWidth are screen pixels, but left/top/
width/max-height written on an element are zoomed pixels, so a menu placed from its anchor landed (scale - 1) too far right and down: at 125% on a
1080p window the More menu was entirely off the screen (14 of 18 size and scale cases broken, measured in Edge 156). lib/overlay-place.ts places in
screen pixels (below, else above, else the roomier side shortened, kept inside the window) and divides by a scale measured from the page at the end.
Used by FloatingMenu (More, chat actions, preview file actions), Popover, the Select popup and the two tooltips. No layout or text change at 100%.
2026-11-04 11:11:55 +01:00
Niko1221 c6dda74bf2 tune: when candidates were skipped for memory the result says so instead of 'already the fastest' (#5122)
'N settings could not be tried: not enough free memory' is the result message (and the Terminal's tuning line) only when some candidates were skipped for memory
in a result that otherwise keeps the current settings; the structured outcome is unchanged and values.not_tried carries the count. Owner decision 11-04.
2026-11-04 11:06:16 +01:00
Niko1221 64abc62393 linux window: keep the COLRv1 colour emoji font away from WebKitGTK, which aborts drawing it (#4533, idea of #4485)
The window process (only it) is pointed at a fontconfig file that includes the system configuration, rejects *COLRv1* font files inside
<selectfont> and prefers the outline Noto Emoji. Written once under the data folder, only when a COLRv1 font is installed; FONTCONFIG_FILE already
set, STRATA_FONT_CONFIG=off or =<file> leave it alone. fontguard.rs is plain std with unit tests that run on every platform. The three variables
join the crash report. Also: the desktoplog crash-file test asserted the literal 0.2.0 and failed on any other version; it now follows the
crate version. Linux-only wiring (run_window_process) is not compiled on this PC; no WebKitGTK here to show the crash or its absence.
2026-11-04 11:02:10 +01:00
Niko1221 25fe8bca8e monitor: the Windows drive-temperature reader asks NVMe drives only, starts only for the Monitor screen's own ask and not in the first minute, leaves a marker across a crash and gives up a stuck first pass (#4661) 2026-11-04 11:00:21 +01:00
Niko1221 d0f86f8359 tuning: a memory wait that found nothing freed is not repeated for every later candidate (#4895) 2026-11-04 11:00:21 +01:00
Niko1221 c30aa64754 serve and update: a server started by running serve.py holds the activity lease so strata update sees it, the update defaults to the checkout's engine store, the server's own picture encoder is released when its engine died, and a stream that ends without its end event is an error that restarts the engine (#4463 #4352 #3961) 2026-11-04 11:00:21 +01:00
Niko1221 8431dde5d4 bench run --profile: the engine tree it starts is put in a job object and ended through it, so a refused taskkill no longer leaves it running (#4260) 2026-11-04 11:00:20 +01:00
Niko1221 efaa100b37 host: Stop and a timed-out command end the whole process group on Linux and macOS, as taskkill /T does on Windows (#4529 #4570) 2026-11-04 11:00:20 +01:00
Niko1221 0b5096edd4 app: a start Control stopped waiting for is kept and watched while Control says it is loading, and a ready instance whose engine Control says is gone is marked stopped (#5177 #5059 #4293 #4973) 2026-11-04 11:00:20 +01:00
Niko1221 b526f98104 tests: 256K context + Q4 KV on a 12 GiB / 16 GB card keeps a 1536 MiB reserve and a positive expert cache; the reporter's 3256 file and log (#3750) 2026-11-04 10:56:29 +01:00
Niko1221 fce58233fb App Host: a quiet chat stream carries a beat frame every 15 s so the desktop web view does not drop it; the timer ends with the response; a response that already began ends in the outer catch (#4878) 2026-11-04 10:56:28 +01:00
Niko1221 acbaa4391b connect: the output cap grows with the window, and a rewrite keeps a cap the person set (#5006; #4528 checked, no change)
limit.output, CLAUDE_CODE_MAX_OUTPUT_TOKENS and maxTokens are max(4096, context/8): 16384 at 128K, so OpenCode's compaction summary has room (the tested 32K server keeps
4096). The input limit and compaction point stay as tested. Rewriting a config keeps an output cap other than the 4096 older versions wrote. #4528: the documented
provider block is still accepted by OpenCode's schema (deprecated in favour of 'providers'); no change. Idea as in PR #4230, written here.
2026-11-04 10:56:16 +01:00
Niko1221 5899c5bbfd ui-flows: the saved-chat check reads the per-chat entries (#5192), fixes a syntax slip 2026-11-04 10:55:07 +01:00
Niko1221 380212689a launch: --set mmap-experts takes a word for strata-dsv41 and stays a bare switch for Qwen (#5027)
The engine's flag schema picks which options are valueless: on DeepSeek --mmap-experts on|off|auto keeps its word through derive_config, the one argument
precedence (arg_layers) and a second launch. Approach as in PR #5028, written here.
2026-11-04 10:54:12 +01:00
Niko1221 c3109534d0 setup: a shard with a pinned SHA-256 is hashed before it is marked finished (#4088)
The pre-pass that gave a hand-copied full-length shard the 'whole' mark also marked a shard left by a run that stopped between the rename and the hash check,
so download() skipped it and the pinned hash was never looked at. A file the pin table names is left unmarked now, and download() hashes and repairs it;
--gguf-dir files and files without a pin keep the mark. Both setup flows call one helper. Approach as in PR #4318.
2026-11-04 10:54:12 +01:00
Niko1221 3434ed0e3b composer: the Enter that commits an IME conversion no longer sends, in the event order of each engine (#5071)
WKWebView sends compositionend before the committing keydown, with isComposing false and keyCode 229; WebView2 sends the keydown first. The
composer asks a small guard (lib/ime-enter.ts): a key that says it is composing or has keyCode 229 belongs to the IME, and the first Enter in the
few milliseconds after a compositionend is the commit; the flag is used up by the next key and expires, so the next Enter sends exactly once.
The guard runs before the command menu too. Replay tests for WebView2, WKWebView, WebKitGTK, Latin input, Shift+Enter, a cancelled conversion
and shortcuts. No visible text.
2026-11-04 10:53:24 +01:00
Niko1221 474fe147de desktop: a renderer that dies is recovered, not left on the browser error page (#5079)
Windows: the shell subscribes to WebView2 ProcessFailed; macOS: to the web content process termination. The page's own renderer dying is
written to the run log (kind, reason, exit code, the address it showed) and the page is loaded again after a short wait: the same screen at
/?recovery=1 first, home at /?recovery=2 second, and nothing after two recoveries in two minutes (the window is left as it is). The decisions
are in src/recovery.rs (unit tested). The page reads ?recovery=n before it renders (lib/recovery.ts), removes it from the address, and starts
with the sidebar previews closed until the person opens one or another chat. The chats are untouched. No visible text.
2026-11-04 10:53:20 +01:00
Niko1221 fe4a03e2f5 tuning: the worker ignores STRATA_API_KEY, restart candidates are judged against the base's own free memory, an engine abort shows its own error line and is an OOM (#5056, #5122, #5078)
#5056 the worker is started without STRATA_API_KEY, so it answers to the key the tuner made for it. #5122 a candidate that adds no memory needs what the base run
started with (min of the old peak-RSS rule and base free memory plus the candidate's delta). #5078 a 5xx from the worker adds the engine log's last error lines;
an allocation failure is an OOM, other aborts fail that candidate alone.
2026-11-04 10:44:36 +01:00
Niko1221 a09ab1c781 runtime owner: answers status while it plans and launches, gives the first token its own budget, leaves when its server is gone, follows a process it cannot read only after repeated looks, and is recoverable before its first launch (#5177 #5059 #4293 #4574 #4893 #4250 #4973 #4824) 2026-11-04 10:40:01 +01:00
Niko1221 bae1e1d4d3 serve: a branching follow-up goes to the slot of its chat (#2518)
pick_slot also recognises an earlier message boundary of a slot's held stream (the last question replaced), so the request no longer overwrites another chat's idle slot. A boundary every held conversation shares (one client's system prompt) names no owner; busy slots are never picked. Tests: branch affinity, busy owner, shared root, interleaved branches (retained prefix 96000 -> 115200 of 115200 tokens).
2026-11-04 10:38:48 +01:00
Niko1221 ac944fc1f8 chats sharing a folder: a turn reports only the files its own tools wrote (#5008)
Two chats in one workspace folder: the end-of-turn sweep of chat A listed the files chat B had just written as
chat A's deliverables, and the other way round. A registry shared by the turns of the host (FolderClaims) now
records which chat's file tool wrote which file; the sweep skips files another chat claimed, and for turns that
overlapped in the same folder it also leaves out files nobody claimed (a script's output cannot be told apart),
rather than give them to the wrong chat. A turn that ran alone behaves as before. Chat records, tool cards and
approvals were already keyed by chat id and session id; a store-level test with two interleaved live chats and
the same call id in both pins that down. The per-folder bucket, the shared model cache and the approval-policy
notice described in the issue are Harness and engine behaviour, not changed here.
2026-11-04 10:38:45 +01:00
Niko1221 faf3db296e Web page: /?q= fills in the question and focuses the box; nothing is sent until the person presses Send (#4221) 2026-11-04 10:38:41 +01:00
Niko1221 16ff27af85 App Host: an empty or malformed Host header (or a request target that is not a path) gets a 400 and the host keeps running (#4397); the shared listener compares the API key in constant time over SHA-256 digests, as do the other two header secrets (#4535) 2026-11-04 10:38:41 +01:00
Niko1221 5bf9626fed SYCL server: the engine_args wrapper takes the base signature (exe, announce) and forwards it (#5086)
Every SYCL start died with TypeError: unexpected keyword 'announce' because install_two_card_spec wrapped engine_args(cfg) while serve/server.py calls it with exe
and, for the memory budget check, announce=False. The quiet call prints no line. Tests run the real base function under the wrapper.
2026-11-04 10:38:22 +01:00
Niko1221 f374b5029d quant_formats --hf: the header takes the file's size from the tree listing, so gguf_reader's bounds checks apply (#5075)
Approach from PR #5075. --hf failed on every repository with AttributeError: 'Header' object has no attribute '_size'; a cut or huge-length header is now a ValueError that names the file.
2026-11-04 10:38:09 +01:00
Niko1221 3bbf0f399e tool parser: streamed arguments keep a file that lists the closing tags (#5040, #4701)
Approach from PR #5040: _close_verdict holds while </tool_call> is still arriving, and the streaming scan of a body whose end is known runs as final. #4701 (final call) and #4702 (longer code fence) are already fixed on the base; the corpus gets the four 4701 specimens (PR #4762 and #5040 fixtures) and runs them whole, in pieces and cut at every byte, with streamed arguments on and off.
2026-11-04 10:37:34 +01:00
Niko1221 06eb02b1da engine choice: an integrated GPU no longer outvotes a discrete card; unlisted Arc models and card numbers above 9 are still seen (#4971, #5111)
_drm_vendor_backend skips a make whose every card reports a small own memory (the 512 MiB carve-out of an APU) while another make has a card;
intel_arc_cards reads each entry on its own, takes card10 and up, and counts an unlisted Battlemage/Alchemist id as an Arc of the family's smallest memory.
The launcher's merge of the Arc beside other makes was already in place. Backend half as in PR #5184, written here.
2026-11-04 10:37:15 +01:00
Niko1221 c7596ce873 preview: bound the Markdown preview so an unchanged file costs nothing while tokens stream (#5180)
The sidebar parsed and rebuilt the whole file on every render of the chat. It now renders through PreviewMarkdown: memoised by the text, cut into slices at block boundaries (each memoised, keyed by place and content), two slices at first and two more per waiting step (one timer, cancelled by a newer text), a block above 40000 characters and a file above 200000 shown as plain text. No visible text, no layout change.
2026-11-04 10:36:19 +01:00
Niko1221 ad142b468c launch: one precedence for engine options; an explicit --set draft-conf and calibrated values survive (#5178, #5132, #4985)
Explicit launch values, then persisted overrides (user_args and hand edits to the launch config), then calibrated values (recorded by
engine calibrate --apply-result with the model and context they were measured for), then Auto. derive_config builds the argv once through
strata_control/arg_layers.py; explicit values go to user_args so the DFlash2 policy keeps them, and are not remembered when left out next time.
2026-11-04 10:36:06 +01:00
Niko1221 23dda3099f launch --stop: a failed stop exits nonzero and no longer ends with 'Nothing is running' (#4398)
Failed stops are counted apart from stopped ones; the empty-list line is for zero targets only. No wording changes: the existing lines are reused.
Approach as in PR #4568.
2026-11-04 10:36:05 +01:00
Niko1221 2d5319cef1 launch: a damaged profile no longer lends its neighbour's config or mints a new API key (#4395, #4396)
running_launches binds prof before the try so a failed load uses this row's defaults; kept_key skips list rows that carry only an error
and keeps looking for a valid key. Approach as in PR #4562 and #4563; tests use real temporary profiles in both orders.
2026-11-04 10:36:05 +01:00
Niko1221 ff36fa9282 images: the prompt reaches sd-cli in an owner-only file (--prompt-file), not as a process argument (#4202)
Approach from PR #4204. sd-cli at the pinned stable-diffusion.cpp commit reads --prompt-file as raw bytes (checked in examples/common/common.cpp at 2988060). The job folder is the 0700 mkdtemp one; the file is 0600, created exclusively and removed with the job's other files. The stand-in sd-cli reads the file; a canary test checks the recorded argument list.
2026-11-04 10:36:00 +01:00
Niko1221 4be6a003aa bench: results.json gets the same redaction as the other report files; a credential of "none" is no key; the key is replaced as a whole word in its own case (#4484, #4483)
Approach from PRs #4500 (one redact_object call before validation) and #4599 (none is no key, whole word, own case). tools/strata_bench.py gets the same none rule and key matching.
The 8-character floor of #4599 is not taken: a short key is still removed where it stands alone.
2026-11-04 10:34:11 +01:00
Niko1221 8181522f15 Pillow 13.0.0 on Python 3.11+ (GHSA-78g4-xqwv-q3g8, OpenJPEG heap overflow) and a decoder format list (#4220)
requirements.txt and the four hash-locked terminal locks (windows, linux x86_64, linux aarch64, macOS arm64) move to 13.0.0; Python 3.10 stays on
12.3.0. Image.open() in the vision path, the EXIF probe and the DeepSeek picture reader now only opens JPEG, PNG, GIF, BMP, WebP, TIFF and AVIF, so a
JPEG 2000 file never reaches OpenJPEG on any Pillow version. Approach from PR #4220 (version split by Python range).
2026-11-04 10:34:11 +01:00
Niko1221 a837fb450b chats: save every chat whole, one entry per chat, and report a full storage instead of trimming (#5192)
The saved chat list cut long fields, dropped the oldest messages of a long chat and left old chats out of the
list when it passed 3.5M characters, and every restart trimmed the chats again (12 messages of 140,000
characters came back as 6). Now each chat has its own storage entry holding the chat exactly as it is, the index
only keeps the order, and a chat the browser refuses keeps its last good copy, stays whole in memory, is tried
again at every save and is reported (state.sessionSave, unsavedSessionsJson). The old single-entry list is moved
over chat by chat and a chat leaves it only after its own entry was written. Approach from PR #5192 (never remove
chats) kept; the field clipping it still did is gone.
2026-11-04 10:31:29 +01:00
Niko1221 888c5eca84 tests: the GB10 doctor row pins sys.platform 2026-11-04 10:26:52 +01:00
Niko1221 eb4f41fb9a Control: the OS and architecture are read without WMI on Windows, so a stuck winmgmt cannot hang models list or a load (#4975, #5185 as reference) 2026-11-04 10:26:40 +01:00
Niko1221 a3a10b90af owner decisions: plan cards keep the screens' count and add "(--gpu N)"; refusal says "free"; app tells the plan which engine it will unload
- gpu_roles.describe(card_numbers=True) prints "GPU <n+1> (--gpu <n>)"; the two test_launch.py assertions are back on the old digits plus the hint. The Terminal has no translation catalog for this line.
- split_engine: the refusal sentence ends "has 7 GiB free)" when the free memory was read (the size, as before, when it was not).
- the desktop app asked `drafting plan` / `drafting fetch` before it unloads the running model, so its own engine counted as busy memory on a model switch and the plan could pick DFlash2 and fetch the 1.2 GB drafter. New flag --engines-unloading (hidden from --help), sent by native-control.mjs for a load that unloads the running instances (not for a model added beside them); same effect as Runner.card_probe. Logic only, no text.
- tests: python (drafting plan with and without the flag), node split-app.test.mjs (flag on the argv, a 2-card model switch, a model added beside).
2026-11-04 09:32:34 +01:00
Niko1221 0e76645a73 tests: the plan's cards line in test_launch.py counts from card 0 (the numbers --gpu takes) 2026-11-04 07:26:12 +01:00
Niko1221 ac1dfef883 launcher: draft layer also found in --data-dir, split engine counts free VRAM, plan card numbers are the --gpu numbers
- discovery.prepared_beside(gguf, data_dirs): after the models-folder walk it also looks in --data-dir and the remembered data folder for mtp/rt, the pack and the picture reader (setup writes them there); plan, download_plan and the own-files preview pass them. A model folder outside the data folder no longer asks for the 5.2 GB layer a second time.
- split_engine: Card.free_mib from the same nvidia-smi call (memory.free; AMD: the render node's vram_used), fit = min(size, free) - 700 MiB; busy cards get the layer split with DFlash2 and no draft-layer part. Runner.card_probe leaves free memory out while a server of this launch (same profile or port) is about to be stopped. Free cards decide as before.
- plan: the cards line prints the numbers --gpu takes and leaves out cards CUDA_VISIBLE_DEVICES (AMD: ROCR/HIP_VISIBLE_DEVICES) hides; the split probe respects the same variables when no card is named. Digits only.
- tests/terminal/test_fix3_0212.py (41 tests); launch_support.Home clears the visibility variables.
2026-11-04 07:11:31 +01:00
Niko1221 1fef27e836 setup: no "--prefill auto:32768" tip where a bare auto already takes 32768 (one NVIDIA card of compute capability 8.0+, no STRATA_NV_CHUNK=0 / STRATA_DEFAULTS=026, no layer split); docs: DETAILS.md describes the NVIDIA ceiling
Logic only, no text changed: bench_tips takes the GPU and leaves the tip out for those configs; older cards, AMD, a layer split and no GPU information keep it. The explicit auto:32768 warning is unchanged. test_setup_bench_tips covers it (10 passed on l620).
2026-11-03 20:37:45 +01:00
Niko1221 aa12025515 NVIDIA: a bare --prefill auto takes a 32768-token chunk ceiling on cards of compute capability 8.0 and up (owner 11-03, after the full suite passes)
The gfx12 rule in generate.cpp now serves both: nv_chunk.hpp (STRATA_NV_CHUNK=0 or STRATA_DEFAULTS=026 keep 8192, =16 keeps 16384, =1 skips the RAM guard), the fit scan still picks a smaller chunk when the cache cannot lend the buffers, the #2090 RAM guard steps 32768 -> 16384 -> 8192, and a layer split takes 8192 back (STRATA_SPLIT_CHUNK16K=1 keeps the ceiling). An explicit --prefill N, auto:N or STRATA_PREFILL_AUTO_MAX is never touched. Volta / Turing / Pascal keep 8192.
A6000 UD-Q4_K_XL, 13.1K of 24.6K experts in VRAM, MMQ_KQ on: prompt 20K +43.4%, 32K +22.5%, 64K +21.3%, 8K 0.0%; cards that hold every expert: flat.
nv_defaults_test covers the ceiling, the switches, the card rule and the step-down.
2026-11-03 20:37:28 +01:00
Niko1221 dbdd81a675 Merge f-0212-strix (final 8b98b6f1c) into f-0212-int 2026-11-03 19:14:15 +01:00
Niko1221 8b98b6f1ca Strix Halo: an oversized prompt chunk is refused at the start instead of killing the engine after the prefill
An integrated GPU cannot report how much room is left (the pool reads 0 MiB free on a start that works). With a chunk above the 16384 default that the person or the environment names (STRATA_PREFILL_AUTO_MAX=32768, --prefill auto:32768) and a UD-Q4_K_XL cache that fills the pool, the driver could not create a stream (PAL failed to submit CMD) and the first launch after the prompt buffers died with HIP 719. Once the prompt path's own buffers are in place the engine now tries one stream, fill and wait; if that fails it prints one line naming the chunk, the cache and the 16384 alternative and ends with exit code 2, before any prompt is read (HIP builds, integrated GPU, own buffers, chunk above 16384 only; the 16384 default, CUDA and Metal are untouched; STRATA_PF_OWN_PROBE_OFF=1 skips it). The failed context cannot be reused, so the engine cannot step down in the same process.

The #1630 warning about a named chunk of 8192 or more no longer prints on gfx1151 (it said a bare --prefill auto stops at 6144, stale since 0.2.8); it is printed once the device is known. STRATA_TRACE marks around the prompt path and the buffer size of the chunk.

setup: the bench tip for --prefill auto:32768 is not said on an APU (apu_hides_chunk_tip). The tuner's prefill_32k arm needs a discrete GPU. The app's saferStart does not retry a start that ended with the chunk refusal (the same chunk would fail again, and a memory setting is not the cause). Tests for all three.
2026-11-03 19:11:00 +01:00
Niko1221 a1c7f05059 serve: an explicit --draft dflash2 with --batch N turns the batch slots' DFlash2 drafts on for one card (STRATA_BATCH_DF2=0 still turns them off; --draft auto and MTP unchanged) 2026-11-03 17:54:13 +01:00
Niko1221 081edf2d9b Merge f-0212-df2choice (final 181b668e5) into f-0212-int 2026-11-03 17:54:12 +01:00
Niko1221 181b668e5a app: a named DFlash2 with a number of requests passes Expert mode's batch-mtp wish on as in 0.2.10, so the Metal fallback to the MTP keeps its --batch-mtp
The server drops --batch-mtp when DFlash2 runs and keeps it when the MTP runs; a test pins the Metal fallback arguments equal to the MTP's.
2026-11-03 17:52:57 +01:00
Niko1221 ae5e73de20 drafting: a DFlash2 the person chose by name keeps the number of requests they chose (DFlash2 with N requests); Auto and the MTP unchanged
- drafting.blocked_by(named=True): only --pipeline-windows still blocks; Metal keeps the old rule (no per-request drafters there)
- apply/launcher/plan pass the choice; the all-cards split engine runs --draft dflash2 --draft-model PACK only for DFlash2 by name AND --split-engine on
- the split engine's own DFlash2 start line counts as "DFlash2 on"; a split run config with --draft dflash2 is left as it is
- app Drafting hint and Terminal --drafting help say it (owner-approved wording, English only)
- tests: terminal drafting/single/split-parallel, split engine, serve parallel, node parallel-default
2026-11-03 16:28:14 +01:00
Niko1221 938b76eb3e gfx12 prompt staging ring (a1e6ef324): only on a gfx12 card. The branch was keyed on the matrix-core attention being active, which the Strix Halo (gfx1151) also has on by default on Windows, so it took the 256 / 192 slot ring that was measured on an R9700 only. gfx1151 keeps the 96-slot ring of 0.2.10; gfx12 is unchanged. 2026-11-03 16:13:46 +01:00
Niko1221 75bbf42912 docs: the split engine's opt-in DFlash2 draft source (engine flag only; Auto keeps the MTP draft layer) 2026-11-03 14:55:40 +01:00
Niko1221 c65b9f8be8 Merge branch 'f-0212-int' into f-0212-splitdf2 2026-11-03 13:57:10 +01:00
Niko1221 0748e92701 split: DFlash2 rounds follow the draft layer's length rule, lanes with no planned drafts bank their taps (FNX_DF2_DEFER, FNX_DF2_REFRESH), the batch planner prices a drafter round per lane that drafts (RowOffer::first_ms), FNX_DF2_TRACE, FNX_SPECULATE=0 reference 2026-11-03 13:57:09 +01:00
Niko1221 f6cea71a78 Merge f-0212-tailskip (final) into f-0212-int 2026-11-03 12:56:48 +01:00
Niko1221 2d8ead05b8 Merge f-0212-parallel (final) into f-0212-int 2026-11-03 12:56:48 +01:00
Niko1221 212d15ba88 Merge f-0212-nvsingle (final 52ac4e733) into f-0212-int 2026-11-03 12:22:49 +01:00
Niko1221 52ac4e7337 NVIDIA: STRATA_MMQ_KQ=1 is on by default for compute capability 8.0 and up (owner yes 11-03): K-quant experts (UD-Q4/Q5/Q6_K_XL) read the prompt on the MMQ kernels
kNvKqDefaultOn is true; STRATA_MMQ_KQ=0, STRATA_NV_KQ_DEFAULT=0 and STRATA_DEFAULTS=026 turn it off, a STRATA_MMQ_KQ the user sets is kept, Volta / Turing / Pascal keep the FP16 prompt path. A pack without K-quant experts never reads the variable.
Start check (RTX PRO 6000, UD-Q4_K_XL, no STRATA variable set): the start prints the K-quant MMQ line and loads libstrata_kq; 4K prompt GPU timeline 1,047 ms (dequant 7.3%) against 1,500 ms (dequant 25.1%) with STRATA_MMQ_KQ=0, prompt 2,854 vs 2,233 tok/s (one request each).
2026-11-03 12:20:34 +01:00
Niko1221 c850b979b5 split: DFlash2 as the draft source of the all-cards split engine (--draft dflash2 --draft-model PACK, FNX_DRAFT=dflash2). One drafter a lane on card 0 through df2_bridge (src/core/dflash2.cpp with bind() compiled out); the window graphs tap layers 5/15/25/35/45 on card 0 (df2_tap_gr), the prompt path taps each card's own rows into a host ring per lane; Session and BatchDecoder take the drafts after the commit. Greedy 6.4K and 27K answers identical to no drafter and to the draft layer (UD-Q4, 2 x A6000) 2026-11-03 12:16:09 +01:00
Niko1221 95cb87d427 Merge f-0212-multispill (final 31a735153) into f-0212-int 2026-11-03 12:07:25 +01:00
Niko1221 c2de8af1c4 setup: the route-tail skip stays on for Unsloth files on Linux gfx12 (R9700, RX 9000)
Owner 11-03 ("3 Yes"). Since 0.1.44 setup wrote STRATA_ROUTE_TAIL_SKIP=0 into the run config of every Unsloth file whose experts are all in RAM. On Linux gfx1200 / gfx1201 it no longer
does: the engine's own default (rank 7, applied only while the variable is unset, so =0 still turns it off) runs. UD-Q4_K_XL on one R9700: decode +6.2% (2 rounds x 8 classes, interleaved) and a
median +15.6% over the 61 suite tasks; the suite scores 56/61 against 55, 57 and 56 for the default arms; start line "STRATA_ROUTE_TAIL_SKIP=7 is ON (default)". Teacher-forced decode-window
KL against no skip 0.0773 (noise floor 0.00000, top-1 91.1%; agent segment 0.20) on a static, low-hit-rate cache, an upper bound for real use. Windows and other cards keep the 0.
unsloth_default_env takes the card's arch (default "": the old behaviour); test_tail_skip_stays_on_for_linux_gfx12 pins the rule.
2026-11-03 11:57:52 +01:00
Niko1221 dfc675848f Merge f-0212-amdsplit (final 9149835c6) into f-0212-int 2026-11-03 11:55:52 +01:00
Niko1221 31a7351534 Layer split: price DFlash2's head, logits and staging into the last stage's room 2026-11-03 11:51:52 +01:00
Niko1221 ee0b367fb7 Layer split walk: early trim for every card, stage late-allocation reserve, fewer-cards step, min card 3 GiB; docs 2026-11-03 11:48:08 +01:00
Niko1221 c2335eebba Merge f-0211-nvdual (final f95411b84) into f-0211-int 2026-11-03 11:41:43 +01:00
Niko1221 9149835c6a Auto starts the split engine on 4 x R9700 for the IQ3_S set (and the other i-quant / Q2_0 expert types). Owner's yes 11-03. With the shared expert fused into the routed launches the split beats the layer split on IQ3_S: 1 user 186.8 against 104.2 tok/s, 8 users 364.6 against 320.7 through launch; 61-task suite 57/61 against 54/61; first-token KL vs the layer split 0.041. 2 and 3 cards unchanged; --split-engine off keeps the layer split 2026-11-03 11:39:16 +01:00
Niko1221 62853e78de FP16 weight cache on by default when the engine runs batch slots on Linux CUDA (3 GiB kept free); gemm_w16_parity test
gemm_dequant_side_parity failed with the cache because its synthetic scratch (10.5 M elements) is smaller than four of its weights, so uncached they take the sliced path and cached one full-N GEMM. The engine's scratch is 32 M elements and every dense projection fits it. gemm_w16_parity runs the engine's scratch, shapes and types, cache off against on, bitwise. STRATA_PF_W16_CACHE_MIB=0 turns it off.
2026-11-03 11:29:44 +01:00
Niko1221 f95411b843 split: the int8 prompt attention is on for the IQ3_S-type files and off for the others (UD-Q4_K_XL: 57/61 with it off, 55/61 with it on, agent-A8 and A10), like GDN_TC; the variable overrides 2026-11-03 11:22:25 +01:00
Niko1221 e2f7c47447 NVIDIA: STRATA_MMQ_KQ=1 as a candidate default for K-quant experts (UD-Q4/Q5/Q6_K_XL prompt on the MMQ kernels), behind STRATA_NV_KQ_DEFAULT=1 until the owner says yes
UD-Q4_K_XL's Q4_K gate/up and Q5_1 down experts read the prompt through the FP16 dequant path plus cuBLAS on CUDA (STRATA_PREFILL_TIMING, 8K prompt on an A6000: dequant 21%, gemm gate/up 24%, gemm down 13% of the GPU timeline).
With STRATA_MMQ_KQ=1 (libstrata_kq): RTX PRO 6000 (sm_120) prompt 4K +37.7%, 8K +25.8%, 20K +34.3%, 32K +32.6% (n 4 per cell, 2 rounds, all won); A6000 (sm_86) 4K +1.7%, 8K +9.0%, 20K +8.1%, 32K +11.2%. First-token KL against the FP16 path over 24 prompts of 2K: 0.0243 (PRO 6000) and 0.0351 (A6000), top-1 23/24 and 22/24.
nv_kq_default_env / apply_nv_kq_default: compute capability 8.0 and up (lowest card decides), off switches STRATA_MMQ_KQ=0, STRATA_NV_KQ_DEFAULT=0, STRATA_DEFAULTS=026; kNvKqDefaultOn stays false, so nothing changes by default.
2026-11-03 10:18:17 +01:00
Niko1221 e318910206 FP16 weight cache: STRATA_PF_W16_CACHE_MIB=auto sizes it by the card (3 GiB kept free); tester switch, the default stays off 2026-11-03 10:16:40 +01:00
Niko1221 fc5dac23c5 Layer split spill walk: stage-weight trim for short cards, drafter drop and head on host per card, KV window and fewer-cards steps; launcher retry without a drafter and the engine's own reason as the last line 2026-11-03 10:02:18 +01:00
Niko1221 3671e77acd split: the shared expert rides in the routed gate|up launch for every expert type, not only Q4_K (FNX_SH_FUSE=0 turns it off). The IQ and the other layers ran it as three launches on a second stream with an event fork and join, 48 layers a window; k_moe_gu_sd carries its swiglu and down as extra rows of blocks like k_moe_gu_q4k does, with the same lanes a row and summation order. Greedy answers (short, 6.5K, 26K) identical for IQ3_S and UD-Q4; IQ3_S 4 x R9700 1 user 115 -> 203 tok/s, 8 users 211 -> 351; UD-Q4 179 -> 186 and 367 -> 385 (n=1 pair) 2026-11-03 09:57:55 +01:00
Niko1221 22f4dbe0e1 split: the NVIDIA prompt path takes 8192 rows x 4 chunks only if the free VRAM after the weights holds it beside the K/V pool (UD-Q4_K_XL on 2 x 48 GB did not start); else 8192 x 2, 4096 x 4 or 4096 x 2 2026-11-03 09:46:00 +01:00
Niko1221 55a2bcd474 Merge f-0211-nvdual into f-0211-int
Conflict in src/split/src/prefill.cu (pf_ring and the ring wiring): kept both sides. The HIP all-to-all small-chunk path stays, guarded by !own_out (the own-rows output is the NVIDIA hyper-connection split's), and the ring runs with nvdual's own_out argument; in the next-card wiring the HIP a2a box/flag copies and the hyper-connection split's input pointers are both kept.
2026-11-03 08:46:45 +01:00
Niko1221 07e1508893 Merge f-0211-hybrid into f-0211-int 2026-11-03 08:46:30 +01:00
Niko1221 2613b4de5a Merge f-0211-r9700one into f-0211-int 2026-11-03 08:46:29 +01:00
Niko1221 66cfc4bcdb Merge f-0211-r9700split into f-0211-int 2026-11-03 08:46:29 +01:00
Niko1221 d2dc05557e split: gdn_tc_set_default and its flag live in namespace fnx (link) 2026-11-03 08:41:39 +01:00
Niko1221 0ffbf096f7 split: g_gdn_tc_default is defined where the scan reads it (kernels part 4), before its use 2026-11-03 08:37:41 +01:00
Niko1221 fcd1493c1a split: gdn_tc_set_default is called from the Engine constructor (engine.cu includes kernels.hpp), so the HIP build compiles; GDN_TC stays NVIDIA-only 2026-11-03 08:34:32 +01:00
Niko1221 c2d44e93ac gfx12: K-quant experts read the prompt on MMQ by default; bare --prefill auto takes 32768-token chunks
Owner 11-03 ("do all 5"), two prompt defaults for Linux gfx12 (R9700), both changing rounding slightly and both with their off switch.

STRATA_MMQ_KQ=1 joins the gfx12 value-changing defaults (gfx12_extra.hpp, applied by apply_arch_defaults, so a raw engine gets it too; Linux only like the
other approved gfx12 prompt defaults; STRATA_MMQ_KQ=0, STRATA_GFX12_DEFAULTS=0 or STRATA_DEFAULTS=026 turn it off, a value the user sets wins). The experts of
Unsloth's UD-Q4/Q5/Q6_K_XL files (Q4_K, Q5_K, Q5_1, Q6_K) then run their prompt products on llama.cpp's MMQ kernels instead of being dequantized to FP16 and
multiplied by hipBLASLt (UD-Q4_K_XL ran 25K dequant launches and ~50K small Tensile GEMMs per 8K prompt). R9700, same job, interleaved: prompt 4K +10.3%,
8K +14.3%, 32K +16.8%; first-token KL against the FP16 path 0.031 over 24 prompts of ~2K (agent turns 0.065), top-1 23/24. A pack without K-quant experts never
reads the variable. The fused variant (STRATA_PF_FUSED_KQ) stays a flag.

The gfx12 ceiling of a bare --prefill auto goes from 16384 to 32768 (gfx12_chunk.hpp, pure functions with tests). A 20K prompt is one chunk instead of
16384 + a 3.6K tail, and every chunk of 1,024 tokens or more streams all the experts the card does not hold. R9700, IQ3_S, 3 rounds interleaved: 20K +12.1%,
64K +2.6%, 32K +1.3%, 8K +0.9%; UD-Q4_K_XL 20K +26%; first-token KL against 16384 at most 5e-5. The lent slots are taken only for what a prompt needs and are
refilled after it; the RAM guard (#2090) now steps 32768 -> 16384 -> 8192 until the extra loan fits. STRATA_GFX12_CHUNK=16 keeps 16384, =0 keeps 8192.

gfx12_extra_test covers both (gfx only, Linux, off switches, user value wins, the ceiling and its step-down); docs/DETAILS.md lists them.
2026-11-03 08:32:12 +01:00
Niko1221 d00d937b89 split: Pf::down_lt is declared outside the NVIDIA-only cublasLt block, so the hyper-connection checks in prefill.cu compile on HIP (HIP never sets it) 2026-11-03 08:30:58 +01:00
Niko1221 213f1a3905 Engine: the auto CPU/PCIe miss share follows the CPU pool by default on a discrete card: ceiling 0.95, factor k falling from 1.85 at 30 GB/s of expert bytes to 0.8 at 12, and a CUDA layer split refines its stage shares (STRATA_PCIE_AUTO_SPLIT=0, STRATA_PCIE_K_RULE=0 and STRATA_PCIE_SHARE_MAX=0.60 restore 0.2.10; integrated GPUs, HIP layer splits and STRATA_DEFAULTS=026 keep it). Pure rule in pcie_share_rule.hpp with a test. 2026-11-03 08:27:33 +01:00
Niko1221 6983b09bba split (owner 11-03, do all 5): FNX_PF_ROWS 8192, FNX_PF_HCSPLIT mode 2 and FNX_PFX_ATTN_I8 on by default on NVIDIA; FNX_PFX_GDN_TC off for the IQ3_S-type files (decided in the engine from the file's routed-expert types), on for the others; passes limited to 8192 rows while other requests decode; tests 2026-11-03 08:26:30 +01:00
Niko1221 38c08443e0 split: a short last chunk is a pass of its own only after at least 3 whole chunks (with fewer the pass is unsplit anyway and the chunks hide each other's sums) 2026-11-03 06:33:56 +01:00
Niko1221 37e4fcf9ac split: the hyper-connection split needs at least 3 chunks in a pass (2 or 1 chunk: slower than the plain read) 2026-11-03 06:17:40 +01:00
Niko1221 cde3399b31 split: with the hyper-connection split a short last chunk is a pass of its own (prompts under 16K were not split at all: the tail kept the pass unsplit) 2026-11-03 05:37:45 +01:00
Niko1221 a1e6ef3246 gfx12 prompt staging ring: 256 / 192 slots below 12K-token chunks instead of the fixed 96
The Linux gfx12 matrix-core prompt path hard-set the streamed-expert ring to 96 slots (measured before the fused IQ3_S kernels). A layer's streamed
experts want a deeper ring at small and middle chunks. R9700 (gfx1201), IQ3_S, 3 rounds x 2 reps interleaved, prompt tok/s against ring 96: ring 256
4K +12.5%, 8K +2.4%, 2K +1.4%, 32K -0.4%; ring 192 4K +11.8%, 8K +4.4%, 32K -1.3%. New default build against the old one: 2K +2.3%, 4K +16.1%,
8K +2.7% (3/3 rounds each), 20K and 32K +0.1%, decode unchanged. UD-Q4_K_XL (MMQ): ring 192 4K +6.9%, 8K +9.5%, 20K +4.0%.
The ring is staging depth only: first-token full-vocabulary logits are bit-identical (md5) for ring 96 / 192 / 384 on 6 prompts of 8K to 64K; greedy
128-token hashes with static residency are identical on UD-Q4 (12/12) and, on IQ3_S, the few divergent requests occur in ring-96 engines as often
as in ring-192 engines (the known HIP timing divergence of an engine's first requests).
Chunks of 12,288 tokens and more keep 96. The ring is capped at ~800 MiB of blobs. STRATA_PREFILL_RING=N and STRATA_GFX12_RING=0 restore the old value.
Only the Linux gfx12 branch (matrix-core prompt attention active) changes; gfx1151 and Windows keep their rule.
2026-11-03 05:09:40 +01:00
Niko1221 e364620c56 Engine: two tester switches for the auto CPU/PCIe miss share: STRATA_PCIE_SHARE_MAX (ceiling, default 0.60) and STRATA_PCIE_K_RULE=1 (pool factor k falls from 1.85 at 30 GB/s of expert bytes to 0.8 at 12). Defaults unchanged. 2026-11-03 04:46:13 +01:00
Niko1221 400e26e3a8 split: fix the two log lines of the hcsplit decision 2026-11-03 04:20:27 +01:00
Niko1221 0ed3a193ac tests: split hcsplit pictures 2026-11-03 04:10:15 +01:00
Niko1221 9f891886d6 split: the hyper-connection split (exact mode) is the default on two NVIDIA cards when the calibration finds the algorithm; 4 chunks a pass unless other requests decode; tests 2026-11-03 04:09:53 +01:00
Niko1221 73dabf548d 0.2.10.1 App: Auto uses the all-cards split engine wherever Control says the Terminal does
Host: draftingPlan/draftingFetch no longer pass --no-split (only when told noSplit, after the draft layer for the split could not be downloaded); the start asks Control
(splitEngine) to turn the run config into the split engine's before the profile is saved, the second try after a failed start is never split, the load waits at least 10 minutes
for a split start, and the Details line says what runs (or that the server started the layer split). A failed MTP layer fetch ends in one plain note and the 0.2.10 layer split
plan. Screen: the split word counts with two or more cards; the download note and the Drafting hint say Auto is the split with the MTP draft layer (text only, two new strings in nine languages).
Tests: tests/split-app.test.mjs (fake Control, 13 tests); drafting.test.mjs and ui-r3.test.mjs updated.
2026-11-03 04:06:55 +01:00
Niko1221 3f15274e8f 0.2.10.1 Control: the app starts the all-cards split engine like the Terminal
`strata split-engine apply --config IN --out OUT` turns the run config the app built into the split engine's (same steps as strata launch: rule, strata-split-N beside the
engine, draft layer on disk, lanes 1..8 from "parallel"); nothing is written on a no. Shared helpers in split_engine.py (exe_finder, draft_layer_present, apply_run_config,
split_plan_line), used by the launcher too. `strata drafting plan` asks the rule with the choice in hand (a saved DFlash2 under an Auto on the screen is Auto), says split for
an explicit MTP as the Terminal does, and uses the Terminal's plan line; expected() refuses a conversation memory other than int8 as the start does. --no-split stays.
Tests: AppPlanEqualsTerminalPlan, AppStartsTheSplit replace AppDoesNotSplit; docs/SPLIT_ENGINE.md describes the app.
2026-11-03 04:06:55 +01:00
Niko1221 c984ab42a0 split: FNX_PF_HCSPLIT=1 splits only passes of whole 4096-row chunks and uses the calibrated algorithm only at 4096 and 2048 rows (exact mode); =2 any chunk of 512 rows or more 2026-11-03 03:52:10 +01:00
Niko1221 e8570ac616 split: FNX_PF_HCSPLIT runs the hc down GEMM through cublasLt with an algorithm calibrated at start to give the same rows at 2048 and 4096 rows (and cublasGemmEx's at 4096) 2026-11-03 03:42:59 +01:00
Niko1221 8088d056f0 split HIP: chunks of up to 256 rows are summed all-to-all instead of round the ring (FNX_PF_A2A_ROWS, with FNX_PF_PEER). Each card stores its f32 addends into the other cards' mailboxes, raises a flag per peer, and sums the chunk itself in the ring's own order and roundings: bit-identical to the ring, one cross-card latency instead of three hops and the gather. 29-row prompt pass 212 -> 139 ms, 6-row pass 206 -> 145 ms (RCCL: 156 / 149); greedy answers (short, 6.5K, 26K) identical to the ring and to RCCL 2026-11-03 03:21:46 +01:00
Niko1221 deebb73a65 split HIP: the prompt chunk sums through the peers' device memory (FNX_PF_PEER, default on when the peer push is on and RCCL is not in use). Receive slots, gather rows and flags live in fine-grained device memory of the receiving card; the sender's kernels store into it. Same sum kernels as the host exchange: greedy answers at 6.5K / 26K / 103K tokens identical to the host sums, 103K prompt 33.4 s -> 30.4 s 2026-11-03 02:52:12 +01:00
Niko1221 750cbf0dbd split HIP: i-quant decode dots (IQ3_S, IQ3_XXS, IQ2_S) with a cheaper sign expansion, the same integers as ggml's (0 of 25M random dots differ, tools/iqdot_test.cu; IQ3_S greedy 26K-token answer identical to the base build) 2026-11-03 02:52:12 +01:00
Niko1221 697fec9d06 split: FNX_PF_HCSPLIT_FULL=2 debug 2026-11-03 02:19:42 +01:00
Niko1221 8969119a42 split: FNX_ONEHOP=2 (screen): the one-hop window sum on two cards 2026-11-03 02:15:50 +01:00
Niko1221 a2aa1f3122 split: FNX_PF_HCSPLIT_FULL debug switch 2026-11-03 02:11:05 +01:00
Niko1221 d716ccfca6 split: FNX_PF_HCSPLIT swap on its own stream, FNX_PF_HCSPLIT_F16 (wip) 2026-11-03 02:05:20 +01:00
Niko1221 838b2e0de6 split: FNX_PFX_ATTN_I8 default back to off (agent-A10 failed 3 of 3 suite runs with it on); FNX_PFX_GDN_TC stays on; FNX_PF_HCSPLIT (screen, wip) 2026-11-03 02:00:38 +01:00
Niko1221 8fbf6b1902 Split engine on AMD: the default lane count is 8 (was 6). A plain launch on 4 R9700 ran 8 users in two waves (255 tok/s aggregate); with 8 lanes they run in one (350). The K/V pool and buffers of 8 lanes at 128K context leave 7.2 GiB free on each card. NVIDIA keeps 6; a number the person chose stays. 2026-11-03 01:35:59 +01:00
Niko1221 1bf5e13e7f split: the INT8 prompt attention (FNX_PFX_ATTN_I8) and the tensor-core GDN scan (FNX_PFX_GDN_TC) are on by default on NVIDIA sm_80+ (the owner's yes, 0.2.10.1); =0 turns each off; HIP keeps the old kernels 2026-11-03 01:16:47 +01:00
Niko1221 082af9c19a Split: tensor-core chunk scan for the prompt GDN behind FNX_PFX_GDN_TC (default off)
Strata's gdn_tc.cuh scan (mma.m16n8k16 f16 hi/lo split, state in registers, 2048-token super-blocks, prep kernel per value head) ported
into the split engine's prompt GDN (gdn_tc_prompt.cuh, part 4, CUDA sm_80+; HIP keeps the sub-chunk kernels). FNX_PFX_GDN_TC=1 runs it in
place of k_gdn_ch_prep / k_gdn_ch_rec when the card has the scratch for it. split_gdn_tc_test checks outputs and end state against the
sub-chunk kernels on random inputs (one card slice, C 33..32768) and times both. FNX_GDN_TIME=1 prints the prompt GDN's GPU time per card.
2026-11-03 01:16:47 +01:00
Niko1221 4a49d25975 split: FNX_PFX_ATTN_I8=1 (screen): the prompt attention on INT8 tensor cores, Strata's prompt_attn_i8v3 adapted to the split's K/V pages (scale per 32 values), card partials and cell lists (off by default) 2026-11-03 01:16:47 +01:00
Niko1221 ffa80d7313 App: a layer split runs DFlash2 alone, one request at a time. chooseParallel kept the app's default of 4 slots on two or more cards, so the Python plan (drafting.blocked_by) refused DFlash2 for slots > 1, the MTP layer was not on the disk (a DFlash2 install leaves it out) and the start ran with no drafter. The one-request rule for DFlash2 no longer asks for one card; a number the person chose still stays, and the MTP keeps its slots. Test: two cards with DFlash2 give one slot and the note, two cards with the MTP keep more than one. 2026-11-02 23:37:27 +01:00
Niko1221 974074c549 0.2.10: the all-cards split engine is Terminal-only; the app's drafting plan and fetch ask without it; the split's lanes are cut to 1..8
App: `strata drafting plan/fetch` gain --no-split, and every drafting call the host makes (control-runtime.mjs draftingPlan, draftingFetch) carries it, so Auto in the app on
2 NVIDIA / 4 R9700 cards is the layer split with DFlash2 alone, no draft layer is planned or fetched for the split, and the Models screen label and fit say DFlash2 again
(capabilities.dflash2Split stays false). The Terminal's plan is unchanged (split + MTP). The gguf_dir fix of installMtpLayer and the explicit-MTP path stay.
Lanes: split_config cuts the lanes to 1..8 (the engine refuses "--batch must be 1..8"): the AMD four-card default PARALLEL 32, a layer config's own --batch (IQ3_S on 4 R9700
gave 32 and fell back to the layer split) and a profile parallel above 8; serve/server.py parallel_args' split branch cuts the same.
Tests: tests/terminal/test_split_parallel.py (AppDoesNotSplit, LaneClamp), apps/host/tests/drafting.test.mjs.
2026-11-02 22:45:11 +01:00
Niko1221 3482b3dff8 Merge f-0210-ds-mac-vision: the Mac DeepSeek picture tower (engine commit f457ed7b9) 2026-11-02 22:04:03 +01:00
Niko1221 4045621eba docs/SPLIT_ENGINE.md describes what 0.2.10 does: Auto starts the all-cards split on 2 RTX A6000 or RTX PRO 6000 cards and on 4 R9700 cards with a UD-Q4_K_XL-type file, the MTP draft layer is fetched for it, --split-engine off turns it off, other AMD cards are opt-in with the self-test line 2026-11-02 22:04:03 +01:00
Niko1221 904b2a723f App: the MTP draft layer fetch names the model's own files folder (--native), so a plain Auto load never downloads the model again
Found live on the AppImage (srv118): after an app install from the person's own GGUF folder, a plain Auto load on two cards (the plan says split + MTP, the layer is missing) ran installMtpLayer, which asked the installer for the model by name without gguf_dir; the installer started downloading the 111 GB model into the app's data folder (stopped after 3.5 GB). installMtpLayer now passes gguf_dir = the folder of the run config's --native. Test: apps/host/tests/drafting.test.mjs.
2026-11-02 21:05:56 +01:00
Niko1221 b1eee01074 test: the Mac vision setup-order window allows the download-total correction between the engine line and step 5 2026-11-02 21:04:16 +01:00
Niko1221 f457ed7b98 Mac DeepSeek vision tower: the GEMM choice is made by the engine at the first picture (Metal 4 present, the tensor-op library compiles, a small self-test equals the plain kernel), logged once; otherwise the plain kernel. mtl::Context::compile_try (compile that returns instead of exiting) 2026-11-02 20:52:11 +01:00
Niko1221 c5ab5d4003 Merge f-0291-rc1-ds-vision-reserve: DeepSeek pictures on a discrete card get --reserve-mib 900 (lazy tower stays) 2026-11-02 20:26:53 +01:00
Niko1221 0659c12582 --reserve-mib 900 with pictures on a discrete card (36 experts, 1.2% of the resident ones, for a worst-case margin of ~10 MiB at the default 600) 2026-11-02 20:24:20 +01:00
Niko1221 e6c0cd41d3 test: the Strix stand-in card has no unified_gb (that is a Mac card) 2026-11-02 20:09:42 +01:00
Niko1221 434c6221b6 Setup: a picture file the person already has is used, not fetched again; the Mac's download total drops the picture file once the engine says no
The Qwen family's picture file is also looked for in the folder above --gguf-dir and in that folder's models/ (pictures.places() already did);
Qwen3.6 links another size's copy of its picture file instead of fetching it again; on a Mac, when the installed DeepSeek engine reads no pictures,
the download total printed before the engine step is corrected (nothing is fetched before that point); the early Coder builds' temporary-repository
picture file (972,207,968 B, the same tensors) is accepted as the shared one (DSV41_ALT_FILES).  test_dsv41_mac_vision's step(5) search starts at
the engine line (it found an earlier family's step 5).
2026-11-02 20:03:42 +01:00
Niko1221 006f0ff258 The Coder's Mac measured line names the memory it was measured with (M5 Max, 128 GB): a 64 GB Mac streams part of the experts from the SSD 2026-11-02 20:01:19 +01:00
Niko1221 45ae55c038 test: the Strix stand-in card carries the uma mark setup gives it 2026-11-02 20:00:50 +01:00
Niko1221 82a028f96e The app and the launcher follow the plan: a plain Auto load fetches the MTP draft layer when the split will engage, the Models screen and the fit say split + MTP, and the summary says what runs after a split fallback
App (apps/host): a plain Auto load (no choice carried, none saved but Auto) fetches the draft layer when `strata drafting plan` says mtp_needed and split,
the same as the Terminal; a Load that carries a choice is as before. probeDrafting reads the plan's split word (capabilities.dflash2Split); the install's drafter
(resolveDrafter) takes it too, so a new install on 2 cards leaves DFlash2 out and gets the MTP layer. drafting.ts / native-settings.mjs
resolveDrafter(choice, family, served, split): Auto is the MTP where the split takes the start, a DFlash2 the person chose stays; NativeModels.tsx passes it, so the
label and the fit use the MTP. `strata drafting plan` gains "split".
Launcher: when the server starts the normal layer split because the all-cards split engine would not come up (metrics engine.split_fallback), the final summary
replaces "Using the all-cards split engine on N cards" with "The all-cards split engine could not start (<the engine's own reason>); running the normal layer split."
Tests: apps/host/tests/drafting.test.mjs; tests/terminal/test_split_parallel.py (PlanTellsTheApp, SummaryTellsWhatRuns).
2026-11-02 19:59:57 +01:00
Niko1221 607d015590 CANDIDATE (not sent): DeepSeek with pictures on a card with memory of its own starts the engine with --reserve-mib 1100 (provisional) so the first picture's hipBLASLt start and the tower's buffers have room; Mac, unified-memory APUs and pictures off unchanged 2026-11-02 19:57:37 +01:00
Niko1221 e483f7835c Mac DeepSeek vision tower: GEMM on the Metal 4 tensor ops (BF16 operands read in place, FP32 accumulation; 798x448 in 93 ms, 1344x1260 in 1.2 s, was 2.6 s and 29 s), DS4_VISION_NO_MPP=1 for the plain kernel, sequential-FMA test GEMM (DS4_VISION_REF_GEMM) 2026-11-02 19:42:56 +01:00
Niko1221 63422c2443 Split engine as the automatic default really works on a fresh install: Auto that picks the split fetches the MTP draft layer, and a missing draft layer is a quiet layer split
The split runs the model's draft layer (--mtp) and exits at start without it ("sfnx: --native and --mtp are required"). The plan and the fetch for Auto
resolved to DFlash2 (drafter 1.2 GB, no draft layer), so on 2 x sm_86/sm_120 or 4 x gfx1201 the split was chosen at start, failed, and the server fell back
to the layer split after "Using the all-cards split engine on 2 cards".

(B) strata_control/split_engine.expected(): the rule apply_to_config uses, asked before anything is on disk (model assumed when its file is not there yet).
Runner.split_expected / resolved_drafting and drafting_inputs: Auto where the split will take the start is the MTP, so download_plan and the fetch include the
draft layer (5.2 GB) and not DFlash2's drafter, setup gets --mtp on without --drafter dflash2, and the plan says "Drafting: MTP (the all-cards split engine uses
the MTP draft layer, 5.2 GB)". The app's `strata drafting plan/fetch` (drafting_cmd._inputs) follows the same rule. A person's own DFlash2 / off, --split-engine
off, one card, other cards, three cards keep what they had.
(A) Runner.apply_split_engine: when the --mtp folder is missing or empty at start the split is not picked (Auto: silently, the layer split with DFlash2 starts as
the plan said; "on": one line saying to add --drafting mtp). _auto_gates() is the Auto-only part of the rule, shared by apply_to_config and expected().
Tests: tests/terminal/test_split_parallel.py (Terminal and app plan paths, AMD 4 x gfx1201 and 2, one card, sm_89, own DFlash2, off, missing draft layer).
2026-11-02 19:41:45 +01:00
Niko1221 742863adf3 Revert "Split engine as the automatic default really works on a fresh install: Auto that picks the split fetches the MTP draft layer, and a missing draft layer is a quiet layer split"
This reverts commit 770a9b8dc8.
2026-11-02 19:41:42 +01:00
Niko1221 0bab1534be test: the old-launcher test pins the product version 0.2.10 (it still said 0.2.9) 2026-11-02 19:39:43 +01:00
Niko1221 45c71cf9c3 Merge f-0210-draftsw-dl: switching the model resets Drafting to Auto (Terminal and app); the Q4_0 selector pack is converted only for an engine that carries it 2026-11-02 19:37:43 +01:00
Niko1221 fca8b48c00 Drafting follows the model: the Terminal goes back to Auto when the model changes to one without a Drafting choice (DeepSeek, Coder, Swift, NVFP4) and a launch writes none into a DeepSeek run config; the app's Models page resets to Auto for a model that is not installed; the Q4_0-selector pack is made only where the installed engine reads it (never for the SYCL engine); tests 2026-11-02 19:37:22 +01:00
Niko1221 2e00d75f99 Merge f-0291-rc1-coder-final: test fix for the Mac vision setup-order test 2026-11-02 19:36:46 +01:00
Niko1221 fbc5c8e1d9 test fix 2026-11-02 19:34:02 +01:00
Niko1221 200f639432 test: the setup-order check looks for the step 5 after the engine line 2026-11-02 19:34:01 +01:00
Niko1221 1f7640a8e5 serve: take SPLIT-HIP's server key fix as amended (cf9406ea9) over the earlier 6174eed4a 2026-11-02 19:31:56 +01:00
Niko1221 379e893f84 Merge f-0291-rc1-split-keys: the server stops sending pen_from to the split engine; logprobs/logit_bias answer 400 naming --split-engine off 2026-11-02 19:24:55 +01:00
Niko1221 6174eed4ac serve: never send the all-cards split engine a GEN key its parser refuses: pen_from is dropped for it (its penalty window is the last penalty_last_n tokens of the whole context, so a continued prompt gives the same window), logprobs and logit_bias get a 400 naming --split-engine off; a test pins the set of keys the server can send against sfnx.cpp's parse_gen 2026-11-02 19:23:52 +01:00
Niko1221 6456e3f242 Merge f-0291-rc1-split-draft: Auto picks the MTP draft layer when the all-cards split engine will engage; quiet fallback when --mtp is missing 2026-11-02 19:21:24 +01:00
Niko1221 770a9b8dc8 Split engine as the automatic default really works on a fresh install: Auto that picks the split fetches the MTP draft layer, and a missing draft layer is a quiet layer split
The split runs the model's draft layer (--mtp) and exits at start without it ("sfnx: --native and --mtp are required"). The plan and the fetch for Auto
resolved to DFlash2 (drafter 1.2 GB, no draft layer), so on 2 x sm_86/sm_120 or 4 x gfx1201 the split was chosen at start, failed, and the server fell back
to the layer split after "Using the all-cards split engine on 2 cards".

(B) strata_control/split_engine.expected(): the rule apply_to_config uses, asked before anything is on disk (model assumed when its file is not there yet).
Runner.split_expected / resolved_drafting and drafting_inputs: Auto where the split will take the start is the MTP, so download_plan and the fetch include the
draft layer (5.2 GB) and not DFlash2's drafter, setup gets --mtp on without --drafter dflash2, and the plan says "Drafting: MTP (the all-cards split engine uses
the MTP draft layer, 5.2 GB)". The app's `strata drafting plan/fetch` (drafting_cmd._inputs) follows the same rule. A person's own DFlash2 / off, --split-engine
off, one card, other cards, three cards keep what they had.
(A) Runner.apply_split_engine: when the --mtp folder is missing or empty at start the split is not picked (Auto: silently, the layer split with DFlash2 starts as
the plan said; "on": one line saying to add --drafting mtp). _auto_gates() is the Auto-only part of the rule, shared by apply_to_config and expected().
Tests: tests/terminal/test_split_parallel.py (Terminal and app plan paths, AMD 4 x gfx1201 and 2, one card, sm_89, own DFlash2, off, missing draft layer).
2026-11-02 19:21:07 +01:00
Niko1221 6ca50487ef Mac DeepSeek: the vision tower on Metal (kernels, BF16 GEMM, host side), capability marker STRATA_DS4_METAL_VISION_V1; credit to antirez/ds4 (MIT, 890898cf) 2026-11-02 19:16:42 +01:00
Niko1221 079d7a6244 Merge f-0291-rc1-mac-vision-marker: the Coder's own picture file accepted beside the shards; DeepSeek's Mac pictures follow the installed engine (STRATA_DS4_METAL_VISION_V1) 2026-11-02 19:13:25 +01:00
Niko1221 541eb00c91 DeepSeek's pictures on a Mac follow the installed engine: on exactly when the strata-dsv41 program carries STRATA_DS4_METAL_VISION_V1 (read, never started, cached per path; no engine yet: on, and setup looks again once it is fetched). Setup settles it after the engine step (no marker: the picture file is not fetched, pack and engine text only, with the plain line), the Mac model rows and the install tool ask the same function, the Coder's Mac line and docs/DEEPSEEK_MAC.md follow. The static DSV41_MAC_VISION is gone; other platforms are unchanged 2026-11-02 19:05:35 +01:00
Niko1221 43bc45f893 Merge f-0291-rc1-split-parallel: a parallel override on the split's run config is its lane count, not a conflict 2026-11-02 19:04:58 +01:00
Niko1221 cd35829bb8 Split engine and parallel: no collision on the first 2-card Auto start; --parallel N and Concurrent requests set the split's lanes
The split's run config always carries --batch N. ensure_profile still wrote the launch profile's parallel=<card fit> (4 on 32 GB or more) beside
it, and strata serve --profile (cli._effective) refuses a parallel override next to --batch: every 2-card NVIDIA start with the split on Auto
stopped with "parallel override conflicts with --batch/--slots in run config args" before the engine ran. Same for the AMD four-card default.

- runner.ensure_profile: no parallel override when the run config already names its slots (run_config_owns_slots)
- runner.apply_split_engine: the person's --parallel N is the split's lane count (cfg["parallel"] for the rule; taken out again on a refusal)
- cli._effective: a parallel override on the split's run config (split_engine_note) is not a conflict (the app's Concurrent requests); other configs keep the check
- server.parallel_args: "parallel" on the split's run config sets its --batch
- tests/terminal/test_split_parallel.py: Terminal path, app path, AMD four-card, lanes from the flag, the check kept for other configs
2026-11-02 19:04:34 +01:00
Niko1221 0e3b06f03f The Coder's own picture file (mmproj-DeepSeek-V4.1-Flash-1017-BF16.gguf, beside the shards or in --model-path) is found as the picture file with its own pin, so setup does not fetch mmproj-BF16.gguf a second time and the plan agrees (DSV41_ALT_NAMES; DF2-STRIX 2026-11-02) 2026-11-02 19:03:29 +01:00
Niko1221 ed619c8341 Merge f-0210-draftsw-split: the layer-split gate lift (DFlash2 alone on several cards) 2026-11-02 18:57:13 +01:00
Niko1221 d040f0b660 Auto and an explicit DFlash2 run DFlash2 alone on a layer split over several cards, with the same arguments as one card: blocked_by no longer counts a layer split (several requests at once and --pipeline-windows still keep the MTP); the all-cards split engine still runs its MTP; comments and help text follow; tests for layer split x CUDA/HIP x choices, split engine, slots 2026-11-02 18:56:47 +01:00
Niko1221 65d0b031d6 Merge f-0291-rc1-coder-hf: Coder HF layout (subdir IQ2_XXS) and the Mac/Strix plan words 2026-11-02 18:30:56 +01:00
Niko1221 50af790125 The Coder's Mac plan words, menu text and docs say what was measured (M5 Max 19-22 tok/s with the published 0.2.9 Metal engine, Ryzen AI Max about 20) instead of 'not run yet' 2026-11-02 18:26:19 +01:00
Niko1221 89515548bd The ISTA-DAS Coder's setup entry names its folder (subdir IQ2_XXS from coder-set.json): --model-path at the Hugging Face layout (IQ2_XXS/ plus the Engram and the picture file at the top) is found by setup's step 5 as the launch plan already found it. Before: the plan said 'used where they are' and step 5 said shard 1 is missing (DF2-STRIX, Strix, 0.2.10 Terminal) 2026-11-02 18:26:19 +01:00
Niko1221 5514024c47 Terminal Drafting step: Auto overrides a choice the app saved in the model's file (written as auto, not dropped); tests lock in one drafter per run for every choice on cuda/hip/metal/sycl, old saved values, and the server and Terminal paths 2026-11-02 18:26:17 +01:00
Niko1221 9bc2a65c0a Merge f-0291-rc1-dl-plan: the launch plan counts the shared DeepSeek files setup still fetches 2026-11-02 18:12:03 +01:00
Niko1221 b1043775d9 DeepSeek with your own shards: the launch plan counts the shared files setup still fetches (the Engram, the DSpark drafter, the picture file), with the same search setup makes (setup.dsv41_shared_missing). Before: the plan said 'Nothing to download' and setup then fetched the 8 GB drafter, even with --dspark off (found on Strix, 2026-11-02; the drafter is part of the model's prepared data, so --dspark off does not skip it) 2026-11-02 18:09:02 +01:00
Niko1221 c75ab76550 HIP package: SPLIT_DIR ships the strata-split-N-amd binaries beside strata 2026-11-02 17:05:08 +01:00
Niko1221 95901fe8a4 Merge f-0291-split-hip e3563ff3d: AMD split final candidate (batched draft layer, RCCL sums, strata-split-2/3/4-amd) 2026-11-02 17:03:21 +01:00
Niko1221 e3563ff3dd split HIP: the RCCL default stays quiet when the peer push is off 2026-11-02 16:46:12 +01:00
Niko1221 d889208a0e split HIP: batched draft layer and RCCL prompt sums on by default (AMD); NVIDIA unchanged 2026-11-02 16:45:58 +01:00
Niko1221 8dd8f11a81 split: FNX_ADMIT_INTERLEAVE default off (no measured gain) 2026-11-02 16:24:46 +01:00
Niko1221 9ecdfa0df7 split: batched draft layer covers sampled lanes and draft penalties (per-lane penalize_row and candidates) 2026-11-02 16:23:05 +01:00
Niko1221 7ccfdb833c split: FNX_BATCH_DRAFT=1 (screen): the draft layer's steps of all the lanes in one window (Engine::draft_lanes, BatchDecoder::draft_batched) 2026-11-02 16:17:59 +01:00
Niko1221 dc54f94ad9 split HIP: FNX_PRECAP=1 and FNX_CAPTURE_AFTER=2 are the defaults on AMD (NVIDIA unchanged) 2026-11-02 16:12:45 +01:00
Niko1221 c3c76f0ad0 Merge f-0291-spill: fit_start for discrete AMD cards (Python only) 2026-11-02 15:54:47 +01:00
Niko1221 076a195b79 fit_start: discrete HIP cards get the window walk, with a base measured on an R9700
fit_start treats a discrete AMD card (backend hip; an APU gives no card memory and is left alone) like CUDA: the KV window walk, the working-cache threshold and
single requests priced from a measured base. The HIP base is 3900 MiB, measured on an R9700 (gfx1201) held by a memory hog: the engine's free VRAM before the expert cache is
the squeeze minus 5045 MiB from 7000 to 5200 MiB (CUDA: minus 6324), so a HIP start holds about 1.28 GiB less. DFlash2 alone at 128K with setup's window of 32768:
7942 MiB 787 slots 864 MiB free 48-52 tok/s; 7000 439 slots; 6400 273 slots (loan keeps 16); 5800 head on the host (31 tok/s); 5200 and 4600 refused with the plain line.
An 8 GB AMD card therefore starts as set up; a 6 GB one walks the window.
2026-11-02 15:54:12 +01:00
Niko1221 06698c6746 DS Coder points at the official repository ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-Coder-GGUF, revision 0b1d5da5, every file's size and LFS sha256 from the Hub: five shards in IQ2_XXS/, the Engram at the root is the shared Engram (same name, size and SHA-256 as the unpruned release's converted one, used where it is, on the SSD), the repository's mmproj is accepted beside the shared one; tests pin the Engram rule 2026-11-02 15:40:48 +01:00
Niko1221 7b57fbaacd split: FNX_CAPTURE_AFTER=N (N >= 2, default 0 = off): a batched window whose layout has no graph runs eagerly (kernels launched one by one, its own lane tables, no capture) until the layout has been seen N times, then it is captured; counters for held, captured and eager windows (by rows too) in FNX_BATCH_STATS 2026-11-02 15:32:14 +01:00
Niko1221 09c76c323d split: FNX_PRECAP=1 keeps the batched window graphs by layout (up to FNX_PRECAP_GRAPHS=64, least recently used out) and pre-captures the uniform layouts at the start, charges the planner the measured capture cost; capture counters in the batch stats (FNX_BATCH_STATS) and FNX_PRECAP_TRACE 2026-11-02 15:32:14 +01:00
Niko1221 9d03dba69b Spill gate: the output head's host fallback (native_head.cpp) passes the Metal build flag too, so it is refused on Metal like the rest of the spill logic (it asked spill_allowed(false, integrated) and the Metal shim reports integrated 0) 2026-11-02 15:31:31 +01:00
Niko1221 3096e0d0af split: FNX_BATCH_STATS window-cycle phases (between windows, verify, draft) 2026-11-02 15:24:02 +01:00
Niko1221 9d1ee9b191 split HIP: auto on AMD only for the UD-Q4 expert family (IQ3_S set stays on the layer split; on still runs it) 2026-11-02 15:22:21 +01:00
Niko1221 7bf446b528 rc1 merge spill (2e7db84bc) 2026-11-02 15:19:15 +01:00
Niko1221 df8c28e879 pin_coder.py reads one folder level of a repository (the official Coder repository keeps its model shards in IQ2_XXS/ and the Engram and picture file at the root): subdir, engram_path, mmproj_path 2026-11-02 15:19:14 +01:00
Niko1221 2e7db84bc0 Spill gated to discrete GPUs; KV window floor 4096 walked by fit_start; output head to pinned host memory as the last step
The draft-layer drop, the 16-slot prompt-path loan, the own-buffer fallback and the head-to-host move run only on a discrete GPU (strata::spill_allowed:
not Metal, not an integrated GPU such as Strix Halo), with a CHECK for the gate in arch_defaults_test. qsa_kv_resident_min is 4096 (20480 on a Metal build;
STRATA_KV_RESIDENT_MIN overrides): windows of 4096 and 8192 gave the same tokens as 20480 with the cache fixed. fit_start walks the window 20480 / 8192 / 4096 only as
far as the card needs. When every step leaves the cache no slot, the output head (Q6_K, 0.41 GiB) moves to mapped pinned host memory (about 3x slower decode).
Measured on the 8 GB mimic at 128K: DFlash2 alone 391 MiB free, 63-70 tok/s, 32K prompt 19.8 s; MTP asked: layer dropped, 555 MiB free, 61-71 tok/s, 32K prompt 13.4 s;
2 requests 277 MiB free, 46 tok/s per user; 4 requests 171 MiB free, 15 tok/s per user. 12 GB cells unchanged.
2026-11-02 15:17:02 +01:00
Niko1221 a4b4e06fa7 split HIP: auto on four cards only (AUTO_SPLIT_AMD_CARD_COUNTS = (4,)); on still runs 2 and 3 2026-11-02 15:12:40 +01:00
Niko1221 f31abf7bd2 Version 0.2.10 / engine 0.1.56 follow-through: test_tui2_023_versions and AGENTS.md follow the bump; the '0.2.9.1' tags that meant this release now read 0.2.10; the test comment names 0.2.9.1 as the unpublished DeepSeek hotfix label 2026-11-02 14:54:37 +01:00
Niko1221 aa471de5d6 rc1 merge version bump: product 0.2.10, engine 0.1.56 (be7d719e0 on 72a0cc1d5) 2026-11-02 14:53:54 +01:00
Niko1221 be7d719e0a Product version 0.2.10 (option B): strata_cli, apps/host, apps/desktop, Cargo, the version test, the bug-report template 2026-11-02 14:46:13 +01:00
Niko1221 72a0cc1d5e Engine component version 0.1.56 (CMake, Metal, packaging defaults, RAM_KEEP_MIN_ENGINE and its test) 2026-11-02 14:45:39 +01:00
Niko1221 984f977d66 split: FNX_ADMIT_INTERLEAVE defaults on for AMD only (NVIDIA split frozen for 0.2.9) 2026-11-02 14:43:58 +01:00
Niko1221 9666d1e760 split HIP: FNX_HC_GRID and FNX_RING_GRID (screen switches) 2026-11-02 14:42:49 +01:00
Niko1221 da20fc301d split: FNX_ADMIT_INTERLEAVE (default 1): one decode window between two admissions; AMD auto card-count constant; tests 2026-11-02 14:41:28 +01:00
Niko1221 b90ee03815 rc1 merge dl-fast (21d53d981) 2026-11-02 14:20:00 +01:00
Niko1221 6527e33c7c rc1 merge metal-selq4 (371a28ed1) 2026-11-02 14:17:01 +01:00
Niko1221 ecf7424b53 8 GB NVIDIA starts at 128K: the prompt-path loan may leave 16 cache slots, the draft layer is dropped when it cannot fit
A card whose expert cache stays below the 128 slots a prompt-path loan keeps behind used to be squeezed (reserve lowered to 100 MiB free, then the first long
prompt aborted in an mmq allocation) or gave the prompt path 600 MiB of its own buffers and stopped. The loan now leaves 16 slots, the auto lend cap is lifted
for such a card, and every 128-slot floor in the lend, request-time lend and VRAM-shrink paths follows it (g_lend_keep). When the draft layer, the head and a
working cache with room left do not fit in the free VRAM before the layer loads, the start runs without the layer and says so (STRATA_MTP_KEEP=1 keeps it).
An out-of-memory in the layer or head load with --batch names --batch and --max-context. Measured on an 8 GB mimic at 128K with --kv-resident 20480: DFlash2
alone 391 MiB free, 69-73 tok/s, a 32K-token prompt in 19.8 s; MTP asked: layer dropped, 557 MiB free, 62-70 tok/s, 32K prompt in 13.4 s. 12 GB unchanged.
2026-11-02 14:13:19 +01:00
Niko1221 21d53d9812 docs: the hf command as the manual way to fetch model files (DETAILS.md)
A short section next to the Linux setup: what Strata's own downloader does, the measured speeds (anonymous, 16-24 MB/s whatever the method), and the manual route
with `hf download` and `--gguf-dir`, which setup already supports (checks the SHA-256, never deletes a file in your own folder). No code change.
2026-11-02 14:05:10 +01:00
Niko1221 d78aa598c6 test_drafting_selector_q4: the corruption test lets the file clock move on (Linux ticks in ~4 ms steps, a same-size rewrite right away kept the marker's mtime_ns) 2026-11-02 14:02:29 +01:00
Niko1221 58169905bb rc1 merge df2-vocab (1063d6fa4) 2026-11-02 13:56:44 +01:00
Niko1221 4a6fd9b0e1 rc1 merge df2-sycl Arc engine-side sizing (42a9c25aa) 2026-11-02 13:51:31 +01:00
Niko1221 42a9c25aa5 SYCL: a start short of VRAM runs slower instead of stopping. Requests at once (--batch N) and a long context no longer end in a refusal on an 8 GB A750: when the expert cache sizing leaves no working cache (or only one sized for a prompt loan that cannot happen), the prompt path's own chunk goes down (to 256), then a reserve that was asked for (to 300 MiB); a context that does not fit in VRAM turns KV streaming on by itself (the largest resident window that fits, 20480 at the least); the person's own --prefill N and --kv-resident N are kept. Only a start with no slot even then is refused, with one plain line saying what is taken and what to lower, and an out-of-memory during the load ends the same way. short_cache.hpp holds the arithmetic, short_cache_test (CPU) the A750 figures from the engine logs 2026-11-02 13:50:34 +01:00
Niko1221 1063d6fa48 DFlash2-only installs use the draft-vocabulary subset the person chose in setup: the choice (en / fr / cyrillic) is copied beside the DFlash2 pack, refreshed when it changes and on update and start, kept by the drafter's fetch; tests for DFlash2-only and MTP-folder installs 2026-11-02 13:37:26 +01:00
Niko1221 6467ec76aa cuda_spill_guard table: gr_down_v3_kernel on sm_120 (8 B, df2-more's register-blocked instance) and DFlash2's rope_table_inv_kernel (32 B) measured on l620 2026-11-02 13:28:35 +01:00
Niko1221 892cdbad7f rc1 merge df2-strix-pf (b1d9ebb4c) 2026-11-02 13:27:17 +01:00
Niko1221 9ed67ba318 split HIP: FNX_PF_RCCL_F32=1 (prompt sums in f32 through RCCL, screen) 2026-11-02 13:22:33 +01:00
Niko1221 7a6e77a625 fit_start never changes --max-context or the requests at once: it moves work off the GPU
The context ladder, the fewer-requests step and the one-request-at-a-time fallback of the earlier fit are gone. A start short of VRAM keeps its
context and its number of requests and is moved off the GPU by the backend's knobs (CUDA: the KV window down to the engine's 20480; Arc: the prompt
chunk, then the reserve, then the window), with one plain note. CUDA single requests are priced from the measured base per drafter (DFlash2 alone,
DFlash2 with the MTP layer, MTP, none). Measured on the 8 GB mimic: DFlash2 alone starts at 128K with --kv-resident 20480 and not with 32768.
2026-11-02 13:21:43 +01:00
Niko1221 418982072b tests: AMD architecture sources pinned (self-test before load, code objects, package script, issue template) 2026-11-02 13:06:04 +01:00
Niko1221 e3ca02ec09 split HIP CMake: STRATA_SPLIT_HIP_ARCHS names the split's code objects (gfx11-generic is not in the main backend's arch list); amd_split_report issue template 2026-11-02 13:02:58 +01:00
Niko1221 a0247cdee8 split HIP: gfx11-generic code object for RDNA3 (self-test passes on a 780M), self-test tolerance for the WMMA accumulate, p2006 self-test scripts 2026-11-02 13:01:04 +01:00
Niko1221 6dd725980e model_pins guard: the credit link to the community DFlash2 GGUF in a comment and a docstring is not a model host the runtime reaches 2026-11-02 12:55:28 +01:00
Niko1221 837909fdd1 test_home_hermetic: raw docstring 2026-11-02 12:49:36 +01:00
Niko1221 f3a43da42a rc1 merge fit8 (fd818c7bd) 2026-11-02 12:49:29 +01:00
Niko1221 e4a68ec6fe Launch tests are hermetic on the per-user settings: Home() sets APPDATA and XDG_CONFIG_HOME to its temp folder and drops a developer's STRATA_SETTINGS_FILE / STRATA_CONNECTION_DIR; test_home_hermetic.py fails if a settings, profile or connection location is outside it 2026-11-02 12:47:04 +01:00
Niko1221 fd818c7bd2 fit_start: CUDA Flash-Next request-slot figures (8 GB mimic), fewer requests before one at a time, plain hint on a slot-session OOM
BASE_SLOTTED_MIB gains (cuda, flash-next) 4990 from the 12 GB and 8 GB CUDA cells; a CUDA start with request slots needs a working cache (263 slots) not the one-slot refusal limit; the one-request fallback keeps the largest context a single request runs (16K on 8 GB); plan() tries fewer requests (down to 2) before one at a time; the draft-layer and head loads name --batch/--max-context when they run out of memory.
2026-11-02 12:45:08 +01:00
Niko1221 6cafbe2c74 Split: ATTN_DMA stays off by default on HIP (FNX_PFX_ATTN_DMA=1 opts in), on by default on NVIDIA as in e074b8e53 2026-11-02 12:41:58 +01:00
Niko1221 e52ae14ab0 rc1 merge split up to e074b8e53 (ATTN_DMA) 2026-11-02 12:41:33 +01:00
Niko1221 5f82379cf6 NVIDIA split engine compiles again after split-hip: FNX_HIP is defined only by the HIP build (prefill.cu used it as an expression) and k_pfr_cmp read host memory with HIP intrinsics (NVIDIA uses __ldcv) 2026-11-02 12:39:13 +01:00
Niko1221 83cadd32fe split HIP: gfx11/gfx12 WMMA paths, one binary with code objects for gfx1100-1103 gfx1200 gfx1201, kernel self-test on cards not run (hip/selftest*), exe lookup strata-split-N-amd, experimental AMD only on explicit on, packaging for N=2,3,4 2026-11-02 12:39:12 +01:00
Niko1221 950038a0a8 NVIDIA split engine compiles again after split-hip: FNX_HIP is defined only by the HIP build (prefill.cu used it as an expression) and k_pfr_cmp read host memory with HIP intrinsics (NVIDIA uses __ldcv) 2026-11-02 12:31:38 +01:00
Niko1221 b0bbba5f8b packaging: build_split_hip.sh builds N = 2, 3, 4 2026-11-02 12:27:12 +01:00
Niko1221 202bb94ae6 split HIP: AMD gfx1201 starts the split by default (AUTO_SPLIT_AMD_ARCHS), off by key, preset or STRATA_SPLIT_ENGINE=0; tests and docs 2026-11-02 12:27:01 +01:00
Niko1221 3227bd2187 rc1 merge split-hip (2c1771dce): AMD split, opt-in 2026-11-02 12:23:11 +01:00
Niko1221 09859d0bfe rc1 merge df2-sycl tip (8cb6e82de: vram_fit calibration) 2026-11-02 12:19:54 +01:00
Niko1221 8cb6e82dea vram_fit: a streamed slot session at 128K is 748 MiB, as DF2-MEM measured on the CUDA engine (0.73 GiB with 32768 resident; 0.56 GiB at 32K): the per-cell extra a streamed session keeps is 1400 bytes, not the 380 derived from the code 2026-11-02 12:19:36 +01:00
Niko1221 4d27740022 rc1 merge df2-sycl vram_fit fix (e4be9e50c) 2026-11-02 12:19:13 +01:00
Niko1221 a47e5634ee Node tests that read source follow the q4sel code: the capability line carries dflash2Selq4, and the version list's cache uses fitDrafter 2026-11-02 12:18:47 +01:00
Niko1221 e4be9e50c1 vram_fit: request slots no longer lower a 12 GB card's 128K. A slot's session counts only the KV the engine keeps in VRAM (--kv-resident N: max(N, 20480) cells, not the whole context: 621 MiB instead of 1930 at 128K with 32768 resident), the threshold is the engine's own minimum (the cache keeps a slot) instead of 128 slots, and a card with no measured base for request slots (CUDA / HIP: only the Arc A750's Flash-Next base is measured) is left to the engine as before 2a1042877. The 8 GB Arc that the engine refused (--batch 2 + MTP, 1024 MiB reserve, 8192) still gets a smaller context. Tests for both 2026-11-02 12:18:45 +01:00
Niko1221 523b78e215 SYCL links again after df2-q4sel: df2_select takes the new cb_q4 argument of the shared header and refuses Q4_0 selector codebooks (SYCL stays on the bf16 pack) 2026-11-02 12:15:05 +01:00
Niko1221 2c1771dce2 Merge f-0291-split (up to ea2aed12c: auto/on/off switch, drafting fix, i-quant sets, PF_JOB) into f-0291-split-hip; AMD opt-in rule (AUTO_SPLIT_AMD_ARCHS), strata-split-N-gfx1201 lookup, packaging/build_split_hip.sh, AMD tests 2026-11-02 12:13:42 +01:00
Niko1221 db4b445ba1 rc1 merge df2-sycl vram_fit slot-session term (2a1042877): serve/server.py engine_args conflicted; kept the announce gate of the drafting line (gui-fb3/q4sel) and added fit_start, whose note prints only when announce is on 2026-11-02 12:05:06 +01:00
Niko1221 3dbd3b0538 rc1 merge df2-q4sel pack-aware VRAM (d4c8ca1f6) 2026-11-02 12:04:40 +01:00
Niko1221 d4c8ca1f65 DFlash2 graphics memory follows the pack in use: 0.72 GB (715 MiB) with the Q4_0 selector, 0.89 GB (890 MiB) otherwise; a SYCL build says it cannot read the Q4_0 selector
drafting.DRAFTER_MIB gains dflash2_selq4 (715); drafting.vram_gb(data_dir, engine) gives the true figure (an engine program that carries 'selector codebooks: Q4_0 rows' and a Q4_0 pack that setup / Load converts), strata drafting plan reports vram_gb and selq4 and its text says the same number, Control passes capabilities.dflash2Selq4 to the page and the fit (apps/host/src/lib/native-fit.ts DFLASH2_SELQ4_VRAM_GB, apps/host/server/native-parallel.mjs) uses it; apps/host/tests/drafting.test.mjs reads DRAFTER_MIB from drafting.py and fails when a copy differs.

Marker per backend: CUDA and HIP builds carry the text (src/core/dflash2.cpp); the SYCL drafter (sycl/src/core/dflash2.cpp, bf16 select kernel) does not read Q4_0 rows, its built binary (P1006 rc1sycl) has no such text so it stays on the bf16 pack, and its loader now refuses a Q4_0 codebook with a plain message instead of tiling it as a matrix. Metal: DF2-METAL's f-0291-metal-selq4. The idea of the Q4_0 selector is nazeshinjite's (nazeshinjite/Qwen3.8-Flash-Next-DFlash2-GGUF).
2026-11-02 12:03:30 +01:00
Niko1221 242549e432 split-hip: line endings back to LF in the files my edits had turned to CRLF (diff against f-0291-split is now the real change) 2026-11-02 12:00:59 +01:00
Niko1221 2a1042877d vram_fit: request slots ("Concurrent requests" 2 or more) take their sessions out of the expert cache's room. Each slot is a session the size of the main one on every backend (the engine's 0.23 GiB at 8K int8); the Flash-Next figures measured on the 8 GB A750 (base 4500 MiB, 2.22 MiB per cache slot) are used for these starts only. A start the engine would refuse gets a smaller context (32K, 16K, 8K, 4K, 2K) for that start, or runs one request at a time, with the fit note; the person's own --max-context is kept, and one-card-less, layer-split, DeepSeek and Metal starts are untouched (fit_start, called from serve/server.py engine_args). Tests for an 8 GB Arc at 8K with 2 requests, a 24 GB CUDA card, an explicit context. verify_refusal_test is registered with add_test 2026-11-02 11:59:43 +01:00
Niko1221 371a28ed11 metal DFlash2 reads the Q4_0 selector codebooks (df2_select_q4; the pack loader keeps them raw rows) and prints 'selector codebooks: Q4_0 rows', the text strata_control/drafting.py looks for in an engine before it uses the converted pack; STRATA_DF2_HEAD=native runs the head over the target's whole head and ignores the draft vocabulary 2026-11-02 11:56:44 +01:00
Niko1221 296e6780fa split HIP: RCCL self-test kernels static (two files include the header) 2026-11-02 11:56:17 +01:00
Niko1221 f93c5019f9 rc1 merge df2-q4sel (ebac5686f, incl. df2-comm select kernel) 2026-11-02 11:56:06 +01:00
Niko1221 e074b8e530 split: the attention partials are copied into device memory by the copy engine by default (FNX_PFX_ATTN_DMA=0 turns it off)
IQ3_S on 2 RTX A6000, 32K and 62.9K prompts, four pairs against the same build without it: 3.3 to 5.6% faster. Only the memory path changes: the merge reads
device memory instead of the mapped host buffer, so the logits are unchanged (checked on the 53-prompt set).
2026-11-02 11:55:39 +01:00
Niko1221 a134650e27 rc1 merge df2-sycl Arc start-crash fix (ab7baa5b1) 2026-11-02 11:52:35 +01:00
Niko1221 71120303b1 split HIP: peer push on by default (probe and host fallback stay); RCCL for the prompt sums behind FNX_PF_RCCL (dlopen, self-test, one-line fallback) with a policy test 2026-11-02 11:51:56 +01:00
Niko1221 ab7baa5b17 SYCL: a start refused for lack of VRAM ends with one plain line and a clean exit. A verifier whose init stopped before it made its own queues holds the process's default queue as cs_, sh_cs_ and copy_, and its destructor destroyed that queue; the next verifier's wait died in urQueueFinish (SIGSEGV on an 8 GB A750 with --batch 2 + MTP, where the expert cache gets 0 slots and the verify window refuses to start). The destructor now leaves the default queue alone. The refusal names what is taken and what to lower (--batch / parallel, --max-context, --vram-reserve-mib). verify_refusal_test: two refused verifiers, then the default queue must still run 2026-11-02 11:48:45 +01:00
Niko1221 ebac5686f9 DFlash2 pack with the Q4_0 selector, made on the person's computer at setup / Load from the checked Q4_0 pack and used when it verifies
The selector's two codebooks as Q4_0 rows take 174 MiB (about 88 expert slots) less graphics memory than bf16, with the same drafts: the committed text was byte-identical to the bf16 pack's in 40 of 40 requests (greedy and sampled, everything resident), draft time and acceptance equal, and ahead 1.7-2.2% where the cache is short (RESULTS-DF2MEM.md section 15).

The idea and the bytes are nazeshinjite's: the community GGUF nazeshinjite/Qwen3.8-Flash-Next-DFlash2-GGUF (Apache-2.0) stores the two tables as Q4_0, and tools/dflash2_pack.py --q4_0-selector / --convert-selector writes exactly those bytes (selector_predecessor sha256 55f70049...ffd28, selector_successor 3b596669...a72d2 over the tensor bytes, equal to the GGUF's).

drafting_fetch.convert_selector (called by setup, strata drafting fetch and Load, never by a start or a serve; no network; about 120 MB of memory) checks the Q4_0 pack against PACK_SHA256, converts it in-process and checks the result against CONVERTED_BYTES / CONVERTED_SHA256 (339694592 bytes, 4a42b982...3ecd); anything else is deleted and the bf16-selector pack keeps running. drafting.pack_path returns the converted pack only when it verifies (a marker beside it keeps the hash check off the start path) and the engine program carries the text 'selector codebooks: Q4_0 rows'; a missing, short or corrupt file, an engine without the text or an engine that is not known gets the Q4_0 pack. Includes f-0291-df2-comm (Q4_0 rows in the select kernel, STRATA_DF2_HEAD=native) merged into rc1.
2026-11-02 11:46:16 +01:00
Niko1221 f32c3c4614 rc1 merge df2-sycl slot-penalty port (e8a6afd5f) 2026-11-02 11:40:57 +01:00
Niko1221 4043138e1e split: FNX_PFX_ATTN_DMA=1 (screen): the other cards' attention partials are copied into device memory by the copy engine as their flags arrive, so the merge kernel reads device memory instead of the mapped host buffer (off by default) 2026-11-02 11:39:22 +01:00
Niko1221 f4561cbe02 Merge f-0291-df2-comm (Q4_0 selector codebooks in the select kernel and the packer, STRATA_DF2_HEAD=native) into rc1; the engine says 'selector codebooks: Q4_0 rows' when it loads such a pack 2026-11-02 11:38:53 +01:00
Niko1221 52e791e097 rc1 merge df2-unsloth (d55138e95) 2026-11-02 11:38:53 +01:00
Niko1221 d55138e95a Auto picks DFlash2 for Unsloth's quantizations of Qwen3.8-Flash-Next
DF2-METAL found that SUPPORTED_FAMILIES and the model-name pattern left Unsloth out, so UD-Q4_K_XL and UD-IQ4_XS (the same Qwen3.8-Flash-Next weights, and the case the
DFlash2 numbers on Strix, R9700 and M5 Max were measured on) started the MTP everywhere.
- strata_control/drafting.py: "unsloth" is a supported family; the name pattern also matches qwen3.8-flash-next-unsloth-<model> (what setup.py and metal/setup_mac.py write:
  f"{family name}-{model.lower()}"). Swift (UkisAI's fine-tune), Coder (ISTA-DASLab's coding version) and NVFP4 stay on the MTP. The stale "Unsloth" in the "another model"
  comment is gone.
- The app's two copies of the rule follow (apps/host/server/native-settings.mjs, src/lib/drafting.ts): the Drafting dropdown and the memory estimate treat Unsloth like Qwen.
- Tests: Unsloth names on cuda, hip, metal and sycl resolve to DFlash2 alone with the vocabulary; Swift, Coder and NVFP4 names stay MTP with their arguments untouched; the names
  are read from setup.py and metal/setup_mac.py; the terminal flow gets the Drafting step for Unsloth; tests/drafting.test.mjs.
2026-11-02 11:28:06 +01:00
Niko1221 a2123a73a0 split-hip: fix the log lines of the peer decision 2026-11-02 11:21:18 +01:00
Niko1221 bbfa7c85cf split-hip: FNX_PEER (AMD): peer push of the window sums and the attention merge into the other cards' device memory, behind a start probe of every directed pair (stores, full read-back compare, flag round trips) with the host exchange as the fallback for the whole run; FNX_PEER=0 forces the host exchange; CPU policy test; NoPeerAccess now allows HIP peer calls in the one probed file only 2026-11-02 11:19:35 +01:00
Niko1221 b1d9ebb4c2 gfx1151 Windows: keep the 16,384-token prompt chunk; do not step it down by the unified pool's "free" figure (STRATA_PF_OWN_CHECK=1 restores the check)
On a Strix Halo the prompt path keeps its own buffers at the 16,384-token chunk (prefill_own_keep_chunk) and the cache sizing already reserved them. The
step-down before the prompt path init read cudaMemGetInfo after the pinned RAM complement (which the unified pool counts twice): 860 MiB "free" in the MTP
engine (no chunk fits, the chunk stays, prompts read at 955 tok/s) but 1.7 GiB in the DFlash2-alone engine (no MTP layer, a slightly larger cache), where
1,536 fit: chunk cut to 1,536, prompts at 303 tok/s (UD-Q4_K_XL, 30K prompt 98.9 s against 31.4 s; 8K 28.3 s against 10.8 s). A real allocation failure still
steps the chunk down (the init loop). Only reached when prefill_own_keep_chunk is set: Windows HIP gfx1151 with a bare --prefill auto.
2026-11-02 11:16:47 +01:00
Niko1221 3342e06444 rc1 merge rc1-metal (d0bc95ec5): draft-vocab resolver 2026-11-02 11:13:22 +01:00
Niko1221 ea2aed12cd split: build option STRATA_SPLIT_PF_JOB (64 default, 128): rows of one expert per prompt-path job, so a weight tile is dequantized once for twice the tokens
Default unchanged (64). The expert prompt kernel's token-tile count follows PF_JOB (it was fixed at four).
2026-11-02 11:13:09 +01:00
Niko1221 568127b7a0 Terminal Overview: the ASCII look has no U+00B7 (a redirected stdout in cp852, cp874, cp932 or ascii raised UnicodeEncodeError); test_speed_average decodes node's output as UTF-8 instead of the locale code page 2026-11-02 11:13:00 +01:00
Niko1221 c64702b724 rc1 merge gui-fb3 (6acd50efe) 2026-11-02 11:07:44 +01:00
Niko1221 33bbd39084 Golden setup test: the vision reader's thread count is mocked (6, the 12-thread baseline PC), so the baseline no longer depends on the runner's CPU count (it failed 10 cases x2 on any Linux runner with more than 12 threads, v0.2.9 included) 2026-11-02 11:07:43 +01:00
Niko1221 d0bc95ec5e DFlash2 alone on Metal starts again without <mtp>/draft_vocab.bin: the packaged default subset (data/draft_vocab.bin) is used, as before the merge; CUDA, HIP and SYCL keep the error; a file named with --mtp-draft-vocab that is missing is still an error (draft_vocab_test) 2026-11-02 11:06:29 +01:00
Niko1221 6acd50efe4 GUI-FB3 item 4: the speed cards show the run average when idle
The Monitor tab (serve/web/app.js), the API monitor page (monitor.js, monitor.html) and the Terminal Overview (strata_cli/overview.py) show the token-weighted
average of the run instead of the last request's speed when the server is idle; live values are unchanged. Ported by hand onto rc1 from the community patch
by keinebenutzer: https://github.com/keinebenutzer/STRATA-average-patch (MIT, Copyright (c) 2026 keinebenutzer), made against v0.1.55 c62d5e3a. Read in full
(no network, telemetry, eval or markup in what it adds); none of its files was run.
Tests: serve/test_speed_average.py (the page code run in node, display-only guard), tests/terminal/test_overview.py (edge cases, fallback).
2026-11-02 11:00:20 +01:00
Niko1221 abd9b4581d rc1 merge split switch (15fb6ffe3): strata_cli/launch/cmd.py conflicted at --drafting help text; df2-primary's help kept, SFNX's --split-engine argument kept 2026-11-02 10:59:45 +01:00
Niko1221 7d4fcefe98 Setup golden baseline re-recorded for the approved 128K default: only --max-context, --kv-resident and --resident-budget-gib moved (12 of 25 configs) 2026-11-02 10:59:20 +01:00
Niko1221 15fb6ffe3f split: Auto drafting on a setup the split accepts runs the split with its MTP; DFlash2 or off chosen by the person keeps the layer split
split_config writes "drafting": "mtp" (what the split engine runs), so serve/server.py's drafter step, which resolves Auto to DFlash2 where the
engine has it, cannot add DFlash2 flags to the split engine's arguments; the fallback keeps the person's own drafting word (or none: Auto), so the
layer split that starts instead gets DFlash2 as usual. The rule refuses (quietly in auto) for an explicit dflash2 or off, and for Auto on a card
where the split has not been measured to decode faster than the layer split with DFlash2. Tests: all four cases, the card rule, the server's
drafter step on a split config.
2026-11-02 10:53:28 +01:00
Niko1221 e8a6afd5f6 SYCL: --batch slots honour the request's repeat / frequency / presence penalties: each penalised slot's reply tail is staged before its window (set_slot_history), sample_rows builds the per-row history with slot_penalty_rows (the CUDA/HIP/Metal logic of 2678c9732), pen_from names where a continued request's reply began; a plain greedy slot still keeps the window's argmax token. logit_bias stays refused on SYCL. slot_history_test (CPU) added to the SYCL targets 2026-11-02 10:48:40 +01:00
Niko1221 4a93c5428c GUI-FB3 items 1 and 2: no Recommended badge in the version list; DFlash2 follows what the installed engine takes
1 The version dropdown no longer shows the "Recommended" tag (the order and the Downloaded tag stay).
2 Gyro-S with Auto sent --mtp-draft-vocab FILE and no --mtp; engine 0.1.55.2 only takes --mtp DIR for the vocabulary and ended with exit code 2.
  (a) strata_control/drafting.py reads the engine program (never starts it) for the start message that says what it takes: the vocabulary file alone, --mtp DIR
      (the MTP layer still not loaded), or no --draft-fallback at all (then no DFlash2 flag is sent). serve/server.py hands the program to the rule.
  (b) An Auto start that picked DFlash2 and ended before it was ready is started once more with the MTP, in one quiet line ("Drafting: MTP (DFlash2 didn't start)",
      nine languages); a DFlash2 the person named fails with the engine's reason.
  (c) tests: tests/terminal/test_drafting.py (new class), serve/test_drafting_retry.py (a fake engine process), version-picker test.
2026-11-02 10:45:18 +01:00
Niko1221 c0380157df Tests that pinned old values follow the merged decisions: native-fit (MTP layer 1.08 GB), tune-outcome (calm running step, GUI feedback 2 item 3), setup tests (128K default context on every card; the picture-file host in strata_control/pictures.py is behind acquisition.require) 2026-11-02 10:45:03 +01:00
Niko1221 ac1965edea split: the all-cards split engine is automatic by default, with an off switch
strata launch --split-engine auto|on|off (settings field, saved in the preset and written to the launch config, so an off survives updates);
STRATA_SPLIT_ENGINE=0/1 for testers. auto (the default) uses the split engine where the rule says yes and says nothing where it says no; on says
why it cannot run; off never. The rule also leaves the layer split for choices the split engine does not honour (DFlash2, custom engine arguments,
replicas, a conversation memory format other than int8). The engine is looked up per card count and architecture (strata-split-N-smCC, then
strata-split-N); sm_120 joins sm_86 as a tested architecture. Tests: modes, auto/on/off, silent refusals, the flag, a saved off.
2026-11-02 10:44:43 +01:00
Niko1221 ec1b3b273b split-hip: FNX_DUMP_FIRST_LOGITS (first-token logits per request, for KL between the engines) 2026-11-02 10:36:45 +01:00
Niko1221 168325418a Metal builds again on the merged DFlash2 line: dflash2.inc follows the drafter header of the ring/mgpu branches (cur_.hist for hist_, bind with head_device, reset/init_ring_log/copy_log_state_from/ring byte counts, plain answers for the batch-slot clone/split calls), and Metal never asks for DFlash2 batch slots 2026-11-02 10:19:23 +01:00
Niko1221 575caededc split: the fallback line carries the engine's own reason and keeps its log; FNX_DUMP_FIRST_LOGITS diagnostic
serve/server.py: when the split engine fails to start, the one line now says why in the engine's words (its 'sfnx: failed:' line) and the
split engine's log is kept beside the normal engine's (it wrote the same file). src/split/src/session.cpp: FNX_DUMP_FIRST_LOGITS=PATH writes
each request's first-token full-vocabulary logits to PATH.<n>, for comparing the prompt path with another engine's.
2026-11-02 10:15:30 +01:00
Niko1221 fe57a0cb52 rc1 merge split (1ba231217) 2026-11-02 09:58:44 +01:00
Niko1221 df642fbf77 rc1 merge pf-small-df2-split (dcad09339): generate.cpp conflicted at the DFlash2 serve on_taps line; kept the layer-split install and added the branch's df2_tap_dump (env-gated diagnostic) to the single-card lambda 2026-11-02 09:58:43 +01:00
Niko1221 ee21447ead rc1 merge df2-mgpu 3e7b8343b + HIP GEMM table (b353ce7d4): generate.cpp conflicted in 2 hunks, both sides kept 2026-11-02 09:58:02 +01:00
Niko1221 3dfb046bf1 rc1 merge df2-metal (a9df88afc): window_rule.hpp and its test (add/add) and generate.cpp (8 hunks) conflicted; mainline draft_fallback/draft_on/lookup_default kept, Metal's defer_df2 call taken 2026-11-02 09:57:22 +01:00
Niko1221 b5abd7792f rc1 merge df2-sycl (903badd90): window_rule.hpp/window_rule_test.cpp add/add (kept the mainline draft_on rule, added SYCL's lookup-skip helpers), drafting.py CONF_STEPS (kept the engine-slot steps, added BACKEND_CONF sycl 0.6), SYCL generate.cpp sets wr.draft_on 2026-11-02 09:56:28 +01:00
Niko1221 f7e24459b7 rc1 merge df2-ring (09ec63b88) 2026-11-02 09:54:45 +01:00
Niko1221 d86c6eeee0 rc1 merge df2-confsteps (6140ce1f3): strata_control/drafting.py and tests/terminal/test_drafting_hardware.py conflicted; confsteps' measured slot arithmetic taken (SLOT_BASE_MIB 3800 pays for DFlash2, no DRAFTER_MIB subtraction, MAX_SLOTS), the gui-fb2 side kept elsewhere 2026-11-02 09:54:45 +01:00
Niko1221 a2f602398f rc1 merge gui-fb2 (35d4781f1): two files conflicted (apps/host/server/native-control.mjs, apps/host/src/components/NativeModels.tsx), resolved at token level; images/visionPrepared take both the DeepSeek tower check and picturesMissing 2026-11-02 09:52:17 +01:00
Niko1221 f81f1326f4 rc1 merge vis-ram (e5de74282) 2026-11-02 09:51:08 +01:00
Niko1221 4fb068a8c1 rc1 merge ds-coder (d6a5a7837) 2026-11-02 09:51:07 +01:00
Niko1221 a1cc285fa1 rc1 merge tui-update (3165c2e8a) 2026-11-02 09:51:07 +01:00
Niko1221 0116b0049a rc1 merge slot-penalty (62e861c99) 2026-11-02 09:51:03 +01:00
Niko1221 299fb68eab rc1 merge arc-uc (512333833) 2026-11-02 09:51:02 +01:00
Niko1221 71e26e68f6 rc1 merge cfg-paths (d0d31a419) 2026-11-02 09:51:02 +01:00
Niko1221 8f9e705c67 rc1 merge ds-rx7000 (d12b0508a) 2026-11-02 09:51:02 +01:00
Niko1221 8394297b26 RAM_KEEP_MIN_ENGINE is 0.1.55.1, the engine version of this update (ram-keep); its test follows 2026-11-02 09:51:02 +01:00
Niko1221 b353ce7d4b DFlash2 Q4_0 GEMM on gfx12: variant 26 (two warps a block, two rows a warp, unroll 1) above 640 rows, 31 (one warp a row, unroll 1) at 640 rows and below; bench variants 36-45; STRATA_DF2_GEMM4=N (N>=10) forces a variant
dflash2_gemm_bench q4 on an R9700 (gfx1201, p620, 8 columns, cold weights, bitwise vs variant 1 in every cell): gate+up 56 -> 44.5 us, down 39 -> 23.4, qkv 26.8 -> 21.4, o 23.6 -> 14.9, fc 61.5 -> 35.2, kv 15.2 -> 10.0, akp/mkp 12.1 -> 6.3, selector 11.1 -> 5.2.
Serve A/B (128K, 8 classes, 3 rounds x 2 reps): drafter 2.5-3.3 -> 2.2-2.8 ms a round, +1.0% tok/s over the old variant (n 6 per class). CLI greedy ids identical across old / new. Other cards keep 17. gfx1151 and gfx1100 compile.
2026-11-02 09:50:36 +01:00
Niko1221 3e7b8343b2 DFlash2 batch slot drafts: on by default only on a layer split (the measured case), one card stays opt-in 2026-11-02 09:50:35 +01:00
Niko1221 80e7c27eb7 rc1 merge kv128 (ee455af7f) 2026-11-02 09:50:22 +01:00
Niko1221 96b29327ff rc1 merge ram-keep (4adc047dc) 2026-11-02 09:50:21 +01:00
Niko1221 79a5ad0429 rc1 merge ple128 (78316c8df) 2026-11-02 09:50:21 +01:00
Niko1221 903badd906 SYCL DFlash2: the drafter's work buffers, K/V rings and rebuild scratch come from one device slab (55 allocations down to 5). On the Arc A-series the host cost of a launch grows with the live allocations; the fresh 4K prompt gap to MTP goes from 4.6 s to about 0.6 s (the prompt context update). Ring log unchanged, ring test: 0 K/V rows differ 2026-11-02 09:48:31 +01:00
Niko1221 1ba2312176 split: refuse a model whose routed experts it has no kernel for at once, in one plain line
Before this, a start with such a file (UD-Q6_K_XL) read 55 GB of embeddings and spent more than 25 minutes loading before it gave up, so the
server's fallback to the normal layer split came very late. The check runs when the file is opened, before anything is read or placed.
2026-11-02 09:29:42 +01:00
Niko1221 db865e9b3d split-hip: the stream flags are one-thread kernels by default (the runtime's native stream memory ops made the prompt path's text vary from start to start with the exchange overlapped: 1 distinct output of 5 fresh starts at 6.5K / 26K / 103K now) 2026-11-02 09:28:01 +01:00
Niko1221 7da32daf97 SYCL, Arc A-series (i915): the ~300 native dense matrices come from first-fit 256 MiB blocks, the emptiest block trimmed to its used size; a launch costs the host time in proportion to the live device allocations there (12 us with none, 131 us with 300). A750, 4K-token prompt 112.6 -> 185.6 tok/s (3 interleaved rounds), decode unchanged, --pcie-frac 0 logits and output identical. STRATA_DENSE_SLAB_MIB overrides, 0 = off; other drivers unchanged 2026-11-02 08:59:43 +01:00
Niko1221 b19ca19ac4 SYCL: STRATA_UC_PROBE also prints the live device allocations, GPU-stamped kernel times and the host launch cost, at nine points of start-up (test only, does nothing when unset) 2026-11-02 08:59:43 +01:00
Niko1221 3f455f9a68 SYCL: test-only in-process kernel timer (STRATA_UC_PROBE) for the uncached-host-memory slowdown on Arc Alchemist 2026-11-02 08:59:42 +01:00
Niko1221 8ec2c8a918 DFlash2: Q4_0 selector codebooks (--q4_0-selector, dtype q4_0 rows in block order, dequantized in the select kernel) and STRATA_DF2_HEAD=native (the draft head runs on the target's own head, no pruned copy) 2026-11-02 08:59:20 +01:00
Niko1221 aaa7a8e494 split: the selection rule accepts the i-quant and Q2_0 expert sets the engine now runs
EXPERT_TYPES gains IQ2_S, IQ3_XXS, IQ3_S, IQ4_XS, IQ4_NL and Q2_0; a set with such experts may have embeddings, shared experts and an output
head in any type the loader converts to Q8_0; the memory estimate counts the Q2_0 to Q4_0 repack twice; the per-layer embedding table type is
checked. Tests for the supported sets (the IQ3_S set, UD-IQ4_XS), the plain refusal lines and the scan.
2026-11-02 08:41:19 +01:00
Niko1221 48d05d0735 split-hip: one-hop decode sum and attention merge for NCARD > 2, per-row batch stats, determinism and handshake test tools 2026-11-02 08:33:10 +01:00
Niko1221 d12b0508a5 Tests for the RX 7000 DeepSeek package: each card gets its family's package, an unnamed card is not guessed, the not-tested line in the Terminal and setup 2026-11-02 08:21:52 +01:00
Niko1221 dcad09339e Merge branch 'f-0291-pf-small-df2' into f-0291-pf-small-df2-split
# Conflicts:
#	src/core/arch_defaults_test.cpp
2026-11-02 08:17:43 +01:00
Niko1221 3eb55adffd A CUDA layer split reads prompts chunk-major by default: the layer-major walk is off unless STRATA_PREFILL_LAYER_MAJOR=1 names it
On 2x RTX A6000 (IQ3_S, MTP, three residency levels) a 32K prompt reads 37-55% sooner chunk-major and an 8K prompt is level (DF2-MGPU, boston); the default had been on for a layer split since 0.2.7 from a 2x A4000 measurement with streamed experts only. Owner's yes 2026-11-02. STRATA_PREFILL_LAYER_MAJOR set by hand still wins both ways; a single card, HIP and the small-card rule (which sets the variable itself) are unchanged.
2026-11-02 08:11:10 +01:00
Niko1221 c14eb72d1c DeepSeek for RX 7000 on Linux: build script for the separate rx7900 DeepSeek package (gfx1100 gfx1101 gfx1102), the not-tested line in the Terminal, the app and setup 2026-11-02 08:11:08 +01:00
Niko1221 62e861c995 A request that continues an earlier leg (prompt + what it generated: solo -> batch slot, slot -> solo) names where its reply began (pen_from), so the penalty window keeps the earlier reply tokens 2026-11-02 08:10:06 +01:00
Niko1221 d0d31a4191 A saved run config follows the update: paths into a removed Control, engine or DeepSeek version folder are pointed at the current one (#4891)
Setup writes absolute paths into the run config (cwd and --expert-profile into the Control bundle that ran it, the picture reader and library folders into
an engine package, the DeepSeek package). An update keeps the new Control/engine and the one before, so the second update after setup removed the folder
the config names: the model showed as not installed and a start failed ("Qwen breaks each time Strata is updated").

strata_control/cfg_paths.py is the one resolver: a string that names a file in components/control/<ver>, components/engine/<ver>-<kind> or
engine-dsv41/<ver>-<kind> whose version folder is gone is mapped to the same file in the active Control / the newest engine of its own kind. Paths the
person typed, packs, models, output paths (--expert-profile-save, log) and folders that still exist are left alone.
- profiles.read_run_config applies it in memory, so the doctor, the model list, the launch plan and the host see the current folders.
- runtime.start writes it once into every saved run config, one line in the host log, keeps a caller's expected source digest valid, and stops with the
  file named when it exists in no current version.
Tests: tests/terminal/test_cfg_paths.py (real update_control twice, real runtime.start for the Terminal and the app, DeepSeek); 9 of 11 fail without the wiring.
2026-11-02 08:08:13 +01:00
Niko1221 b1f4c92961 Arc: DFlash2 alone is the default drafter on the SYCL engine
strata_control/drafting.py: "sycl" joins DFLASH2_BACKENDS, so Auto picks DFlash2 (Q4_0 pack, no MTP layer, --draft-fallback none) on an Intel engine.
BACKEND_CONF = {"sycl": 0.6} replaces the CUDA slot steps for Intel (A750, 298 expert slots, serve sweeps: 0.6 was the best of 0.35 / 0.6 / 0.8); a --draft-conf
of the person's own still wins. An Arc A-series card (A3xx / A5xx / A7xx, told by its Windows adapter name or its Linux PCI id) counts as tested; Arc B-series,
Arc Pro and integrated graphics keep "has not been tested on your graphics card yet".

sycl engine: --mtp-draft-vocab is accepted in place of --mtp for the draft vocabulary (the app passes no --mtp with DFlash2); STRATA_DF2_TIMING also prints the
request-end context update and each prompt context update, STRATA_DF2_NO_PROMPT_CTX (measurement only) skips the prompt's context update.
dflash2_bench: a ctx mode that times the prompt's context update.

Measured on the A750 (IQ3_XXS, --prompt-cache 6, 5 rounds): DFlash2 alone is 2.6% behind MTP in decode tokens per second (95% interval -4.8 to -0.4) and 1.1%
behind over time; a fresh 4K prompt reads 4.6 s slower. Tests: tests/terminal/test_drafting.py, test_drafting_hardware.py, test_issues_022.py (117 pass).
2026-11-02 08:08:04 +01:00
Niko1221 4adc047dc8 Arena under a hard memory limit also needs its headroom: a limit just above the arena no longer kills the engine
On l620 a 49 GiB cgroup limit and the 46.84 GiB arena: the engine's check (arena + blob <= limit) passed, the arena is pinned and never given back, the
engine's own memory (about 3 GiB) came on top, and the cgroup OOM killer ended the engine at the start (journal: "A process of this unit has been
killed by the OOM killer", twice). With the fallback allowed the arena now needs arena + blob + max(RAM to keep, the fallback headroom) under the
hard limit (cgroup, container, job object, --memory-limit-mib), else the RAM copy runs. Without a fallback (hand-set --ple-io ram, shared arena file,
STRATA_ARENA_RAM_FALLBACK=0) the comparison is unchanged. arena_hard_limit_need in ple_residency.hpp, unit cases for a limit just above the arena.
2026-11-02 08:02:49 +01:00
Niko1221 b509b2fef5 Metal: a request's logit bias reached no sampler (set_logit_bias / set_slot_logit_bias returned early because the Metal window leaves the last-stage layer end at -1), and the window's bias buffer stayed at its 16-byte stand-in 2026-11-02 07:47:29 +01:00
Niko1221 78316c8df4 prefill: the batched PLE post-ops default off for an engine built with CUDA 12.8 on a compute capability 12.0 card (one log line); the variable still wins
An engine built with CUDA 12.8.61 faults in the prompt path on an RTX PRO 6000 (compute capability 12.0, driver 580) with the batched PLE post-ops on, which is the
default: "an illegal memory access" at every prompt length from 94 tokens, first error in the native PLE postops launch (CUDA_LAUNCH_BLOCKING), and the isolated
verify_batch_parity test faults on native_ple_postops_batch_snap with 4 tokens. STRATA_PLE_BATCH=0 runs. The same source built with CUDA 13.0 (the released
cuda13 package) runs the default at 94 to 20K tokens, where the batch is ~23% faster prompts, so only the 12.8 toolchain changes: ple_batch_default(cudart_version, hip,
major, minor) is false only for CUDART_VERSION 12080..12089 on cc 12.0 (not HIP, not another card, not another toolkit), ple_batch_resolve lets STRATA_PLE_BATCH win in both
directions, and prefill.cpp says once when it turned the batch off. Known issue #220 / #224; CMake already warned for sm_120 builds with CUDA < 13.0. Unit test in
arch_defaults_test; docs/OLDER_GPUS.md.
2026-11-02 07:39:06 +01:00
Niko1221 d8867ef616 Host: the RAM rule depends on the engine in use: STRATA_RAM_KEEP_GIB for engines that know it, the 0.2.9 rule for older ones
"Leave 20% for my work" reads the version of the engine the Engine tab selected (snapshot engine version, or the folder named in the run config's exe).
An engine of RAM_KEEP_MIN_ENGINE (0.1.56) or newer gets the env STRATA_RAM_KEEP_GIB = clamp(ceil(0.2 x RAM GB), 5, 8). An older engine, or one whose
version cannot be read, gets the rule of 0.2.9 unchanged: --resident-budget-gib from the 80% rule and a STRATA_RESIDENT_HEADROOM_GIB of 20%; neither
is ever given an argument it does not know. Switching engines between loads swaps the rule; full performance removes either. Tests at 24, 32, 64 and 128 GB
for both kinds of engine, with the preference on and off.
2026-11-02 07:34:37 +01:00
Niko1221 35d4781f1c Tests follow the GUI feedback 2 changes: the picture choice rule moved to pictures-choice.ts, the status line is flex, the new catalog is known to plain-errors 2026-11-02 07:32:59 +01:00
Niko1221 53a48dbe5f DeepSeek pack check: the reason is never empty, a pack that no longer fits its files is rebuilt once, and a card the Linux package has no code for is said
HF discussion 30 ("the model's pack is not complete ()", 7900 XTX, files converted by ISTA's script):
- dsv41_pack_info: an engine that prints nothing is reported by how it ended (signal 11, exit code N), never as an empty string.
- The engine's file check is repeated in Python (dsv41_pack_problems); a pack that does not fit the model files (missing or short file, a file replaced
  since the pack was made: pack-source.json size and time, unreadable description) is rebuilt once with one line; when only the engine's own check gave
  no answer the pack is kept, one warning carries the reason and the test question decides. The failure line is one line and names the file (the
  launcher shows only the first [X] line).
- Both layouts whole (what ISTA's converter leaves) use the six-shard files; without --gguf-dir six-shard files in setup's own folder are seen when all five are there.
- The 0.2.9 Linux DeepSeek package holds code for gfx1200/gfx1201 only: hip_archs_from_pci is a third probe for the card; an unnamed card gets one plain line,
  a known card without code is told so (install, install_package), setup warns for its own card pick and reads BUILD.json archs beside a hand-made engine.
Tests: tools/test_setup_dsv41.py (PackCheck, CardsAndLayout, CardGate), tests/terminal/test_dsv41_engine.py (HipCards).
2026-11-02 07:31:16 +01:00
Niko1221 96b009d2ef verify.hpp: set_slot_sampling comment no longer says penalties are skipped in batch windows 2026-11-02 07:26:09 +01:00
Niko1221 541a2fac2d Slot penalties and logit bias in batch windows: shared slot_row_draw for CUDA/HIP/Metal, server accepts logit_bias with --batch, docs updated 2026-11-02 07:15:58 +01:00
Niko1221 e5de74282e AMD: the Qwen on-demand picture reader works on HIP (reader with its CPU reader beside it, one GPU) and releases its VRAM when idle, but is off by default (VISION_ON_DEMAND_AMD_BY_DEFAULT): no shrinking expert cache on HIP, so 7,959 slots against 7,943 for +5.4 s on the first picture; docs 2026-11-02 07:13:18 +01:00
Niko1221 067771d800 Merge branch 'f-0291-metal-sample' into f-0291-slot-penalty 2026-11-02 07:12:54 +01:00
Niko1221 2678c9732e --parallel slots honour the request's repeat / frequency / presence penalties and logit bias (CUDA, HIP): each penalised slot's reply tail is staged before its window (set_slot_history), the verifier builds the per-row history (slot_penalty_rows: the tail plus the slot's tokens in the window up to the row) and draws the row with sample_tokens; the slot's own logit bias array (set_slot_logit_bias); a logit_bias request is no longer refused with --batch; plain greedy slots keep the window's argmax. CPU test slot_history_test 2026-11-02 07:12:53 +01:00
Niko1221 56f93d8ee0 Docs: the choice is now called Leave room for my work 2026-11-02 07:12:20 +01:00
Niko1221 ef7668e596 GUI feedback 2, items 1-8: plain Read pictures, white tick, calm progress card, steady checkbox, ring before the chat name, status line without overlap, compact file list
1 Read pictures is a plain checkbox; no hint, no download button (Load does it); a failed picture download keeps one line under the box.
2 A checkbox tick is white in light mode (dark mode unchanged).
3 The progress card shows the step, the numbers and a thin bar; Stop is a text button; no "buttons wait" sentence; reduced motion honoured.
4 The Read pictures choice is kept per version and no longer re-derived from host reports while a job runs; the reader probe keeps its last answer.
5 No separator under Read pictures.
6 Running chat: an SVG ring before the chat's name.
7 The chat's time and token count are a wrapping flex row without fixed widths.
8 A run with many files is one block: delivered-to folder, working folder, and a count that opens the list.
Also: "Leave 20% for my work" is now "Leave room for my work" (hint: keeps 5-8 GB free), nine languages (gui-<lang>.json).
2026-11-02 07:08:18 +01:00
Niko1221 1bdda21126 Metal --parallel slots honour their request's sampling (temperature, top_p, top_k, min_p, seed; penalties are not applied in batch windows, as on CUDA): each slot's row of a batch window is drawn again from the window's logits with its own params, Philox(seed, position) 2026-11-02 07:04:27 +01:00
Niko1221 d292672844 DFlash2 batch slot drafts on by default (STRATA_BATCH_DF2=0 opts out) where the slots run in one serial group 2026-11-02 06:55:08 +01:00
Niko1221 c683fbf0ef DFlash2: prompt taps in the layer-major prefill (single stage, one prompt); STRATA_DF2_TAP_DUMP research dump
lm_prepare no longer declines for the DFlash2 taps on one stage. lm_run takes a layer's completed output when the next layer starts (the hyper-connection write of its FFN half), puts each sub-chunk's rows at positions >= tap_first_ in the staging, and hands the super-chunk's rows to on_taps in position order when all its layers are done. A layer split and a multi-prompt read still decline. Untested on a GPU at this commit: the equivalence test and the A/B are running.
2026-11-02 06:54:11 +01:00
Niko1221 6140ce1f3b DFlash2 confidence steps from the DFlash2-alone sweep, in the engine's slots (12,000+: 0.35, 7,500+: 0.5, 3,000+: 0.7, below: 0.8)
Measured with no MTP layer loaded against the MTP at equal VRAM (l620 mimic, IQ3_S, chat with a rewind plus four cold prompts, 3 paired rounds): best confidence 0.8 at 1.4K and 3.3K engine slots, 0.65 at 6.6K, 0.5 at 9.2K, 0.5 and 0.35 level at 15.7K; 0.35 loses to the MTP up to 6.6K slots. An explicit --expert-cache N is a budget of largest blobs, about 1.3 N engine slots on IQ3_S, now converted before the steps apply. The slot estimate no longer deducts the drafter from a baseline that already holds the MTP layer, because the DFlash2 is the only drafter.
2026-11-02 06:52:59 +01:00
Niko1221 b49e1ffd6b Host: "Leave 20% for my work" passes the RAM to keep as the env STRATA_RAM_KEEP_GIB, not the flag
An engine older than --ram-keep-gib (0.1.52, 0.1.54, 0.1.55, which a person can choose in the Engine tab) stops with "unknown argument";
it ignores an environment variable. The engine still takes the flag for CLI users (the flag wins over the env). Full performance removes the env;
a STRATA_RAM_KEEP_GIB, --ram-keep-gib or RAM mode of the person wins. New host test: an older engine gets no argument it does not know.
2026-11-02 06:51:33 +01:00
Niko1221 ee108afef5 fused_gr: the shared-memory opt-in of the v3 down kernels is set once per DEVICE, not once per process (a layer split on two sm_120 cards answered 0 tokens)
gr_down_pf_launch and gr_down_rb_launch set cudaFuncAttributeMaxDynamicSharedMemorySize inside a function-local `static const bool once` lambda. The attribute belongs to
the device: only the first card to launch got it, and on the second card of a layer split the launch of a window of 5 or more rows (51-82 KB of dynamic shared memory
against the 48 KB default) was rejected with "invalid argument", which inside the graph capture surfaced as "fused_gr_read_multi v3: operation failed due to a previous error
during capture" and an engine that answered every request with no tokens. Reproduced on 2 x RTX PRO 6000 (CUDA 12.8): the int3 head with the default mask fails at the
6-token window capture; STRATA_GR_V3=0 and one card work. set_attr_once_per_device (one flag per device, under a mutex) replaces both lambdas; no kernel, launch geometry or
arithmetic changes. Every other cudaFuncSetAttribute site of the tree was already per device (listed in phaseA-tests/blackwell/RESULTS-DF2NV.md section 9).
tools/test_func_attr_scope.py (pytest, CPU) fails if cudaFuncSetAttribute / hipFuncSetAttribute appears inside a function-local static lambda or a std::call_once.
2026-11-02 06:47:11 +01:00
Niko1221 b537586e6b One RAM-to-keep-free setting: --ram-keep-gib W covers the arena and the RAM-copy fallback; the app passes it for "Leave 20% for my work"
Engine: --ram-keep-gib W (default 0; STRATA_RAM_KEEP_GIB the same). The expert arena is used only if W GiB stay available after it;
otherwise the RAM copy of the hottest experts leaves max(W, the safety headroom) free, and STRATA_RESIDENT_HEADROOM_GIB still wins when set.
The safety headroom is 8 GiB on Windows (the page trim of the copy needs the room) and 4 GiB elsewhere; with W = 0 nothing else changes.
The decision lives in ple_residency.hpp (ram_keep_plan and its parts) and is covered by ple_residency_test over free RAM x W x arena size.
The explicit modes (--resident-budget-gib, --resident-experts) leave max(W, their headroom).

Host: profile 'work' passes --ram-keep-gib W, W = clamp(ceil(0.2 x RAM GB), 5, 8), instead of the 80% budget and the 20% env headroom; the engine
decides from the RAM free at its start. A flag or env of the person wins; full performance passes nothing and takes the earlier version's settings out.
resource-ram.test.mjs covers 24, 32, 64 and 128 GB with the preference on and off. Docs in DETAILS.md.
2026-11-02 06:44:09 +01:00
Niko1221 fd59c9c39b Small-card prompt path: with one CUDA card, a native pack and a default prompt chunk below 8,192 tokens, prompts read with the unfused experts and, from two chunks, the layer-major walk
The default path on a small expert cache streams every expert again for every 512-5,888-token chunk. On an RTX PRO 5000 held to 1.3K / 1.6K / 1.9K engine slots the unfused experts plus layer-major read a 20K / 32K prompt -44% / -50%, -41% / -42%, -14% / -12% sooner (a real 12 GB 5070: -26% at 20K); from a default chunk of 8,192 (2.3K slots) the default path wins and the change costs +1..+25%, so the rule keys on that chunk (strata::small_card_prompt_path, unit-tested at its boundaries). Output changes at rounding level (first-token KL against the FP16 prompt path 0.012-0.018 for every path including the default, top-1 47 of 48), owner's yes 2026-11-02. STRATA_PF_FUSED / STRATA_PREFILL_LAYER_MAJOR set by hand win, STRATA_PF_SMALL=0 turns the rule off; a layer split, HIP, a non-native pack and STRATA_DEFAULTS=026 keep the default.
2026-11-02 06:40:26 +01:00
Niko1221 4d4a59642b Merge f-0291-df2-primary into f-0291-df2-sycl (primary's side on the duplicated cherry-pick hunks of src/program/generate.cpp) 2026-11-02 06:34:26 +01:00
Niko1221 fc453c3d82 generate.cpp: batch DFlash2 confidence cut: real \0 in the env tests 2026-11-02 06:29:57 +01:00
Niko1221 fd3839172c split: tile exactness test and fnx_run (teacher-forced logits) as optional build targets
split_moe_tile_test runs the prompt kernel's weight tiles on a GPU for one expert of each routed-expert type and tests/split/tile_check.py compares them with gguf-py. split_fnx_run is sFNX's logits tool on the same engine objects.
2026-11-02 06:27:49 +01:00
Niko1221 73a470dafc DFlash2 batch slots: per-slot draft rows cut where the selector's probability product falls below STRATA_BATCH_DF2_CONF (default --draft-conf or 0.35), STRATA_BATCH_DF2_MAXX caps rows 2026-11-02 06:27:03 +01:00
Niko1221 5123338339 SYCL: STRATA_UC_PROBE also prints the live device allocations, GPU-stamped kernel times and the host launch cost, at nine points of start-up (test only, does nothing when unset) 2026-11-02 06:06:07 +01:00
Niko1221 f9e14bdf8e SYCL, Arc A-series (i915): the ~300 native dense matrices come from first-fit 256 MiB blocks, the emptiest block trimmed to its used size; a launch costs the host time in proportion to the live device allocations there (12 us with none, 131 us with 300). A750, 4K-token prompt 112.6 -> 185.6 tok/s (3 interleaved rounds), decode unchanged, --pcie-frac 0 logits and output identical. STRATA_DENSE_SLAB_MIB overrides, 0 = off; other drivers unchanged 2026-11-02 06:06:07 +01:00
Niko1221 90cb0c0f5a DFlash2 serve: the end-of-request context update reads the gathered taps (a layer split's drafter is on another card than stage 0's tap buffer) 2026-11-02 06:03:41 +01:00
Niko1221 09ec63b884 DFlash2: STRATA_DF2_END_UPDATE=0 turns the end-of-request context update off (diagnostics) 2026-11-02 06:03:00 +01:00
Niko1221 7b5d1a38da DF2-SYCL ring test: a conversation that resumes at 0 stashes the old lineage as a branch (the check expected a cold start) 2026-11-02 06:01:41 +01:00
Niko1221 2d5a577037 Merge branch 'f-0291-df2-ring' into f-0291-df2-mgpu 2026-11-02 05:59:43 +01:00
Niko1221 4e7866f91b DFlash2 ring log: off until init_ring_log() has allocated the staging (a clone that skips alloc() copied into a null buffer)
log_on_ defaulted to true and alloc() allocated the pinned staging; a drafter that never ran alloc() (the --parallel slots' clone_alloc) therefore ran the
context update's two D2H copies into a null pointer: an illegal memory access on its first draft. The log is now off by default, init_ring_log() (called
by alloc(), callable by a clone's own allocation on its device) allocates the staging and turns it on, and a drafter without it keeps the pre-log rule.
2026-11-02 05:57:13 +01:00
Niko1221 5a73435898 Metal honours the request's sampling: temperature, top_p, top_k, min_p, repetition / frequency / presence penalties, logit bias and seed on the head rows of every window (Window::sample_rows over lm_, set by Verifier::run through window_set_sampling); greedy requests keep the in-graph argmax 2026-11-02 05:55:39 +01:00
Niko1221 d59ab12fc5 metal: the sampler chain on the GPU (sample.metal samp_part + samp_pick: bias, penalties, top_k, top_p, min_p, temperature, Philox draw at (seed, position)) and sample_chain_parity (greedy == argmax, 1800 rows against the CPU chain with 0 mismatches, determinism, chi-square) 2026-11-02 05:52:27 +01:00
Niko1221 d0dcf65435 generate.cpp: fix two broken string literals in the batch DFlash2 path 2026-11-02 05:42:10 +01:00
Niko1221 29d55bd718 DFlash2 batch slots: deferred context when a round has no spare window row (taps parked on the host, replayed on catch-up), draft graphs of all slots launched then finished together, ring log merged 2026-11-02 05:40:37 +01:00
Niko1221 2f608aac30 Merge f-0291-df2-ring (DFlash2 ring log) into f-0291-df2-mgpu; copy_state_from carries the lineage log 2026-11-02 05:32:32 +01:00
Niko1221 1b25b55444 DF2-SYCL: ring log (port of f-0291-df2-ring): host log of the quantized hidden rows, tag shadow, rebuild and stash, request-end context update, dflash2_ring_test 2026-11-02 05:05:11 +01:00
Niko1221 28db88e56f DF2-SYCL serve: note the batched prompt rows in the drafter's token history (live continuations were taken for a foreign conversation) 2026-11-02 05:00:17 +01:00
Niko1221 e8abcadeef split-hip: chunked GDN prompt kernel in 58 KB of LDS (16 state columns, 128 threads, same sums), DMA receive/gather as an opt-in with its self-check (FNX_PF_DMA=1, FNX_PF_DMA_CHECK=1), batch stats (FNX_BATCH_STATS=1) 2026-11-02 04:57:44 +01:00
Niko1221 a9df88afc9 metal DFlash2: the lineage row log and the ring rebuild for a restored conversation: the half hidden_norm row of every context position is logged (10.6 MiB a lineage, STRATA_DF2_LOG_MIB stashes 4); a request that restores a stashed lineage rebuilds the ring rows of its window with the same projections, bitwise equal to the original update (STRATA_DF2_CHECK_REBUILD=1); the drafter's history is the whole prompt 2026-11-02 04:43:36 +01:00
Niko1221 e04ee998f2 DeepSeek picture tower in RAM between pictures on AMD too (R9700: tower-in-RAM 419 vs 386 experts at a 12 GB-like reserve, same tok/s; lender read the no-room picture right), docs 2026-11-02 04:40:39 +01:00
Niko1221 ac23de4c4d A failed MTP download says one short line; tests 2026-11-02 04:32:18 +01:00
Niko1221 72cf167e3b DFlash2 ring log: update the last window's rows when a request ends, quiet small gaps, copy_log_state_from, stats env
A request that ends on EOS or max_new skips the draft that would have made the last accepted window's context rows, so every turn left a hole of a + 1
positions (5 a turn, accumulating) that a later turn resuming after it found blank and the log could not rebuild. The serve loop now updates those rows
(one synchronous context update, ~0.4 ms) before it finishes the request: a four-turn agent loop plus a parked return now rebuilds with 0 rows missing.
The cold line prints only when 64 or more rows of the window are missing. copy_log_state_from(src) moves a lineage's log, tag shadow and epoch with a request
between two drafters (solo <-> a --parallel slot). STRATA_DF2_RING_STATS=1 prints the live log, the stash and each rebuild per request.
2026-11-02 04:18:30 +01:00
Niko1221 d826b02d5b DeepSeek picture tower: when the card has no free graphics memory for a picture turn, the experts' slots lend the room (the prompt path's own lend: the last slots of device 0's tail region, read back from the model files after the turn), so the picture is read instead of refused. Boston A6000 with 59 MiB free: the picture is answered right, 35 experts lent and handed back in 117 ms (12 GB-like reserve: 31 experts, 79 ms) 2026-11-02 04:11:42 +01:00
Niko1221 b1de678f33 DF2-SYCL: --draft-conf defaults to 0.6 on the Arc build 2026-11-02 04:11:39 +01:00
Niko1221 96f5227763 DFlash2 ring log: rebuild the draft ring's window from logged context rows after a restore, a rewind or a repeated prefix
The draft ring has 2,064 slots (position mod 2064). A request that resumes below the highest position written (a repeated or rewound prefix, a
parked conversation brought back) found the part of its first draft's window [n - 2047, resume) overwritten by later positions or never written,
and the drafter drafted poorly until it refilled: on an RTX PRO 6000 with the prompt cache on, DFlash2 alone was +14% over MTP on cold and live
turns but +3% on restored prompts and -2% on an exact repeat (acceptance 0.63 and 0.59 against 0.79 and 0.70 with the cache off). The repeat case
printed no warning at all, because the token history was equal.

The K/V of the five layers is a function of one row per position, the quantized hidden_norm output (2560 int8 codes and 80 block scales, 2.9 KB;
the K/V is 40 KB and the taps 51 KB). Every context update now copies that row into a chunked, copy-on-write host log of the lineage (a
pinned staging copy inside the captured graph, moved to the log after the stream sync). begin_request rebuilds, with the same kernels on the
same bytes, every slot of the window below `resume` whose tag does not match (a host shadow of the tags says which): the rebuilt K/V is bitwise
the original update's (dflash2_ring_test: rewind, parked, branch, evicted; 578 rows in 4.0 ms, a whole window of 2042 rows in 12.7 ms).
A lineage replaced by another is moved, not copied, into a stash (STRATA_DF2_LOG_MIB, default 256, oldest evicted); a request whose prefix matches a
stashed lineage swaps it back in; a rewind or branch keeps the old branch as a copy-on-write stash entry. STRATA_DF2_RING_LOG=0 restores the old
behaviour. Rows never logged are reported (cold) instead of silently blank.
2026-11-02 03:55:35 +01:00
Niko1221 5074caee06 DF2-SYCL: defaults to the SIMD16 pair GEMM (v41, v40 for small matrices) and the 256-work-item attention at 64-key chunks 2026-11-02 03:40:14 +01:00
Niko1221 6de7c8e315 DFlash2: the MTP chain's cost window and q lists apply only to candidates the MTP drew
With DFlash2 as the drafter the MTP stays loaded as the fallback (use_mtp stays true), and three decisions tested use_mtp where they meant
"the MTP chain drew this round's candidates":
- STRATA_SPEC_COST=1 (a GB10 default): T = clamp(chain_rows, 1, S_mtp), but chain_rows is only written by the MTP chain callback, so with DFlash2
  and no --draft-conf the window stayed at one row (CLI and serve; code_py 175 vs 258 tok/s on an RTX PRO 6000, serve 166-230 vs 283-397).
- STRATA_SPEC_PROB=1, sampled requests: ver.set_spec_q(mtp.spec_q()) judged DFlash2's deterministic candidates against the MTP's stale q lists
  (CLI: 107 of 137 windows), and serve's gated path was not declined for DFlash2.
- the lookup choice read mtp.prob() for the same reason.
spec/window_rule.hpp holds the window rule and the q rule, both loops call it. The default path is unchanged: window_rule_test compares the new
rule with a copy of the old code over 9216 inputs (only the DFlash2 + cost rule + no --draft-conf cases differ). The CLI stats also print the
window histogram for any rule that sets the width and, when STRATA_SPEC_PROB judged windows, their count and source.
2026-11-02 03:36:50 +01:00
Niko1221 1acdf93401 DF2-SYCL: attention chunk 64 (33 chunks), SIMD16 pair Q4_0 GEMM variants 40-43 2026-11-02 03:32:30 +01:00
Niko1221 f3face9402 DF2-SYCL: lookup-first rule (c2b9218c7's window_rule.hpp, CLI and serve loops hand-ported), STRATA_DF2_LOOKUP_SKIP default 0 2026-11-02 03:27:11 +01:00
Niko1221 ee455af7fe Setup streams the KV cache from 64K up on a PC with room for the experts plus 10 GB, not only for the model's all-in-RAM need (KV-128)
The rule asked ram_gb + KV + 1 (64.8 GB for IQ3_S, a 64 GB PC reads 63.1), which left a small card with the KV in VRAM and 720 expert slots at 128K. It is now the smaller of that and arena_gb + LOW_RAM_HEADROOM_GB + KV (62.1 GB for IQ3_S; the KV's RAM comes out of the engine's RAM copy or file cache, not on top). It never asks more than before; --kv-streaming on|off and WSL are unchanged; the size stays 32768.

Measured (phaseA-tests/gh-sweep-1031/KV-128.md): IQ3_S, 12 GB mimic on l620 under a 45 GiB cap, 756 -> 1,267 slots, 32K prompt 21-34 s -> 12-17 s, 118K prompt 76-122 s -> 45-63 s, decode flat to +10%, engine RAM unchanged; on the owner's RTX 5070 (single runs) 924 -> 1,585 slots, 32K prompt 130 -> 79 s, RAM copy 30.8 -> 28.9 GiB, no paging. Decode there is within noise.

Tests: need numbers, never-more-than-before over every model/context/KV, the rule at the edges and for WSL, setup writing --kv-resident on a 63.1 GB PC with a 12 GB card and not at 61.9 GB or at 32K. The golden baseline gets --kv-resident 32768 for the 64 GB / 32 GB card IQ3_S and recommended entries. Setup and tuning tests on boston: same 24 failures as f-0291-int3 (not touched), 7 more pass.
2026-11-02 03:01:20 +01:00
Niko1221 f47df12624 metal DFlash2: the attention floor follows the conversation (kept for a request that goes on from the same history, lowered when a request restarts earlier, set to the resume point for a restarted lineage); it stayed at the last restore's resume point and starved every shorter prompt of context rows 2026-11-02 02:52:21 +01:00
Niko1221 17c6c4cd80 split: the prompt ring's receive and gather steps move data to and from device memory by DMA on the copy engines (the add and output kernels read device memory, not host memory over PCIe) 2026-11-02 02:36:08 +01:00
Niko1221 77b4bc42dc split-hip: direct all-gather (every card stores its finished part into all peers' gather buffers), answers bit-identical to the ring; run scripts 2026-11-02 02:36:07 +01:00
Niko1221 cc2dc69184 DF2-SYCL: STRATA_DUMP_WINDOW_TOP2 debug hook 2026-11-02 02:30:50 +01:00
Niko1221 07fb0e7113 Arena RAM fallback: budget form with an 8 GiB headroom, and the Windows copy trims the mapped pages
The fallback's RAM copy (previous commit) now uses the --resident-budget-gib form: the budget is what is available minus the headroom
(default 8 GiB, STRATA_RESIDENT_HEADROOM_GIB changes it), so the experts outside the copy are read unbuffered instead of through the mapped
view whose pages grow the working set until available RAM is near zero.  On Windows the complement copy releases the mapped pages to the
standby list every 4 layers (the VirtualUnlock call that already ran once after the copy): available RAM fell to 0.9 GiB for ~10 s during the copy
on a 64 GB PC with 44.6 GiB available.  No data changes.
Measured on the RTX 5070 PC (128K, 44.6 GiB available): lowest available RAM 4.05 GiB, 2,000-token answer 25.2 tok/s (9.4-10.1 without the fix).
2026-11-02 02:15:46 +01:00
Niko1221 9a35255008 split-hip: side-stream sync copies for HIP graph capture, row-by-row GDN default on HIP, mlock warning 2026-11-02 02:01:42 +01:00
Niko1221 e47ad761fd Docs: Qwen picture reader on demand (DETAILS vision table, vision_reader note) 2026-11-02 02:01:12 +01:00
Niko1221 79dc463760 Picture reader in RAM: Qwen on-demand reader is the NVIDIA one-GPU default (vision.elastic false keeps it on the card; setup's 700 MiB reserve becomes 2048 for it), no repeated VRAM asks to an engine that cannot shrink, 1,536 MiB room check before the card is tried; DeepSeek tower: a failed take no longer leaves a sticky CUDA error, AMD keeps the tower on the card (--vision-lazy asks); docs 2026-11-02 02:00:38 +01:00
Niko1221 86cbd533a4 metal DFlash2: the producers write the half GEMM operand themselves (df2_rms_h, df2_conv_h, df2_swiglu_h, df2_attn_merge_h; STRATA_DF2_FUSE_H=0 restores the 37 df2_f16 dispatches): drafter 2.12 -> 2.08 ms GPU, output ids identical; STRATA_DF2_STATS prints a periodic drafter line (serve leaves through exit()) 2026-11-02 02:00:01 +01:00
Niko1221 3f3b6895e1 Doctor: the MTP folder is optional when the drafter is DFlash2; ui-r3 follows the drafter-aware wording 2026-11-02 01:48:35 +01:00
Niko1221 13edeb7f89 One drafter per run: DFlash2 is the primary drafter where the engine has it (CUDA, HIP, Metal), alone and with --draft-fallback none; the MTP is chosen alone with no DFlash2 option
Auto and the default pick DFlash2 with the Q4_0 pack for Qwen3.8-Flash-Next and Gyro; Intel SYCL stays MTP (one line, DFLASH2_BACKENDS). The GUI dropdown, TUI, strata launch, setup (--drafter), serve/server.py, the host's Load and the drafting command agree. A new install fetches only the selected drafter, a switch fetches the other once (never at start), the draft vocabulary comes with the DFlash2 pack. A layer split or several requests load only the MTP and say so in one line; no drafter on disk says so too. Fit, plan and slot estimates use the chosen drafter (0.89 GB DFlash2, 1.08 GB MTP).
2026-11-02 01:31:26 +01:00
Niko1221 3d08a095ff split: routed expert kernels for the IQ3_S set (first version, to be gated)
Decode: ggml's vec_dot for IQ4_XS, IQ3_S, IQ3_XXS and IQ2_S added to the mmvq expert launchers beside IQ4_NL and Q4_0.
Prompt: weight tiles for Q4_0, IQ4_NL, IQ4_XS, IQ3_S, IQ3_XXS and IQ2_S in moe_mma_iq.cuh (ggml's dequantization rounded once to f16).
Loader: Strata's Q2_0 down experts are repacked to Q4_0 blocks (lossless, twice the bytes).
2026-11-02 01:30:07 +01:00
Niko1221 43543e9b07 Arena RAM fallback keeps a RAM copy of the hottest experts (--resident-experts form)
When the expert arena does not fit the available RAM (#4846), the run read every expert through the plain file source with no RAM copy:
9-10 tok/s on the owner's 5070 PC (44.6 GiB available for a 46.84 GiB arena). The same engine with a RAM copy of the hottest experts
(--resident-budget-gib 40 / --resident-experts) gave 24-31 tok/s at the same RAM. The fallback now sets the --resident-experts flags
(resident_cpu_experts, resident_pin, resident_soft, 4 GiB headroom) when a static expert profile is set; if the file cannot be opened the
flags are cleared with the rest of the fallback. STRATA_ARENA_FALLBACK_RESIDENT=0 keeps the plain file source.
Not built, not run: the patch is untested.
2026-11-02 01:28:07 +01:00
Niko1221 058f65322b split-hip: an mlock that the user's RLIMIT_MEMLOCK refuses is a warning on HIP (the table stays in the page cache), not a failed start 2026-11-02 01:23:48 +01:00
Niko1221 84f16f7a27 split: load-time conversion of the dense and small tensors of the IQ3_S set
Adds the IQ2_S, IQ3_XXS, IQ3_S, IQ4_XS and Q2_0 types to the GGUF type table and a
loader step that, only when a file has routed experts in IQ2_S, IQ3_XXS, IQ3_S or Q2_0,
dequantizes its dense matrices, shared experts, projections and embeddings (K-quants,
IQ4_XS, IQ4_NL, BF16, F16) and writes them again as Q8_0, and the routers, injections
and SSM gates as F32. Other files are not touched. The routed IQ expert kernels are not
there yet, so the set still falls back.

Checked against gguf-py on the ISTA IQ3_S file: 762 tensors converted, the sampled ones
all within the Q8_0 bound (F32 targets exact), every source type covered.
2026-11-02 01:23:25 +01:00
Niko1221 a6fa0fc869 DF2-SYCL: DFlash2 in the serve loop (taps, begin_request, window rule, draft after commit) 2026-11-02 01:18:15 +01:00
Niko1221 46f84d4646 split-hip: native stream memory ops by default (13 us cross-card trip on R9700), the rule accepts the F32 router gate beside the Q8_0 shared expert 2026-11-02 01:14:25 +01:00
Niko1221 c2b9218c76 DFlash2: the lookup first when the suffix match is long (spec/window_rule.hpp skip_draft_for_lookup, STRATA_DF2_LOOKUP_SKIP=N, default 8 on Metal, 0 elsewhere): the round's DFlash2 draft is deferred to the start of the next round; the policy takes the lookup (the committed rows only update the drafter's ring, no draft round) or the draft is made then from the same taps. CLI and serve loops. Based on 34c0ab47b 2026-11-02 01:13:55 +01:00
Niko1221 2fb096b938 Gate check: a Qwen serve engine must answer requests that make prompt lookup propose long windows (the Metal 12-row refusal went unseen because no gate request repeated text) 2026-11-02 01:12:02 +01:00
Niko1221 2f13eb5224 Merge branch 'f-0291-df2-mem' into f-0291-df2-primary 2026-11-02 01:09:39 +01:00
Niko1221 cd9c7e7b57 Merge branch 'f-0291-df2-mem' into f-0291-df2-mgpu 2026-11-02 01:09:03 +01:00
Niko1221 96f0307aa0 --draft dflash2 without the MTP layer: refuse a missing draft vocabulary file instead of running the drafter's head over the whole vocabulary 2026-11-02 01:06:15 +01:00
Niko1221 7e25613634 split-hip: stream memory ops as one-thread kernels by default on HIP (FNX_HIP_MEMOP=native for the runtime's) 2026-11-02 01:03:36 +01:00
Niko1221 f70368b996 DFlash2 on a layer split and in batch slots: stage-wise taps gathered onto the drafter's card (cross-device events), per-stage prompt taps, per-slot drafters sharing the pack and head, spare-row slot windows, solo<->slot ring handoff 2026-11-02 00:59:54 +01:00
Niko1221 415b43f95e metal DFlash2: with no MTP loaded the draft vocabulary falls back to the repository's draft_vocab.bin like the MTP loader (it bound the whole 248,320-row head: +372 MiB and +0.7 ms per round) 2026-11-02 00:47:43 +01:00
Niko1221 285fa48b94 lookup_default.hpp is included on every backend (it sat inside the HIP-only include block) 2026-11-02 00:47:32 +01:00
Niko1221 5a258235c6 split-hip: the launcher rule knows two makes (NVIDIA sm_86 x2, AMD gfx1201): per-make tested sets, KFD-topology card probe for AMD 2026-11-02 00:46:15 +01:00
Niko1221 cf73cc4cb5 DF2-SYCL engine integration, first cut: SYCL Dflash2Drafter, verifier and prompt taps, --draft dflash2 in the CLI loop (no MTP loaded) 2026-11-02 00:45:06 +01:00
Niko1221 fea8a3e633 Metal keeps --lookup-max-t off by default: its windows hold 8 rows and a 12-row lookup window ended the first Qwen serve request (metal window: window size out of range); the default rule is a header with a test 2026-11-02 00:43:09 +01:00
Niko1221 12c2d740c6 split: the prompt ring's receive and gather steps move data to and from device memory by DMA on the copy engines (the add and output kernels read device memory, not host memory over PCIe) 2026-11-02 00:40:54 +01:00
Niko1221 909ab78af8 Metal serve: no default 12-row lookup window (the Metal window holds 8 rows; with the MTP the first suffix-lookup window ended the request with 'metal window: window size out of range') 2026-11-02 00:40:48 +01:00
Niko1221 857d0e3fe9 split-hip: CMake HIP target (strata-split-N for gfx1201 with -DSTRATA_ENABLE_HIP=ON), card check by gfx name 2026-11-02 00:40:32 +01:00
Niko1221 ea2b0d9c63 split-hip: first HIP gfx12 compile of the split engine: gfx12 WMMA expert prompt kernel (replaces mma.m8n8k4), nvcuda::wmma shim for attention and the QSA scores, driver stream-memory-op shim, HIP stubs for the shared-memory holder 2026-11-02 00:37:55 +01:00
Niko1221 857e516621 DFlash2: --draft-fallback none (no MTP layer loaded, DFlash2 the only drafter, an error at start if it cannot run) and the DFlash2 draft head shared with the MTP's byte-identical subset (STRATA_DF2_SHARE_HEAD_OFF=1 keeps its own copy)
Per-run source choice, no mid-run fallback. With the MTP loaded the DFlash2 gathered a second copy of the 106299-row draft head (212.9 MiB on Flash-Next); it now uses the MTP's when the type, the vocabulary ids and the layout match, and the expert cache is sized without the copy. The cache sizing also prices the DFlash2 head from the vocabulary file when there is no MTP.
2026-11-02 00:35:05 +01:00
Niko1221 a6fd5cfe42 DFlash2: the MTP chain's cost window and q lists apply only to candidates the MTP drew
With DFlash2 as the drafter the MTP stays loaded as the fallback (use_mtp stays true), and three decisions tested use_mtp where they meant
"the MTP chain drew this round's candidates":
- STRATA_SPEC_COST=1 (a GB10 default): T = clamp(chain_rows, 1, S_mtp), but chain_rows is only written by the MTP chain callback, so with DFlash2
  and no --draft-conf the window stayed at one row (CLI and serve; code_py 175 vs 258 tok/s on an RTX PRO 6000, serve 166-230 vs 283-397).
- STRATA_SPEC_PROB=1, sampled requests: ver.set_spec_q(mtp.spec_q()) judged DFlash2's deterministic candidates against the MTP's stale q lists
  (CLI: 107 of 137 windows), and serve's gated path was not declined for DFlash2.
- the lookup choice read mtp.prob() for the same reason.
spec/window_rule.hpp holds the window rule and the q rule, both loops call it. The default path is unchanged: window_rule_test compares the new
rule with a copy of the old code over 9216 inputs (only the DFlash2 + cost rule + no --draft-conf cases differ). The CLI stats also print the
window histogram for any rule that sets the width and, when STRATA_SPEC_PROB judged windows, their count and source.
2026-11-02 00:34:36 +01:00
Niko1221 dc9b2a407c DF2-SYCL: attention variant 2 (256 work-items), DF-04 prefix test, replay script 2026-11-02 00:34:36 +01:00
Niko1221 9d642b9eb9 split: the expert prompt kernel on Ampere's m16n8k16 tensor-core instruction for sm_80 and later (the Volta m8n8k4 version stays for older cards); first version, to be gated for exactness and speed 2026-11-02 00:33:26 +01:00
Niko1221 1c2aba90fa Merge branch 'f-0291-df2-mem' into f-0291-df2-primary 2026-11-02 00:32:35 +01:00
Niko1221 962816b409 Merge branch 'f-0291-df2-mem' into f-0291-df2-mgpu 2026-11-02 00:32:35 +01:00
Niko1221 5a048c6c09 --draft dflash2 loads no MTP layer unless --draft-fallback mtp is given (the DFlash2 and the MTP layer are never loaded together by default); --draft auto still loads both 2026-11-02 00:32:01 +01:00
Niko1221 626f9ef009 DFlash2: --draft-fallback none (no MTP layer loaded, DFlash2 the only drafter, an error at start if it cannot run) and the DFlash2 draft head shared with the MTP's byte-identical subset (STRATA_DF2_SHARE_HEAD_OFF=1 keeps its own copy)
Per-run source choice, no mid-run fallback. With the MTP loaded the DFlash2 gathered a second copy of the 106299-row draft head (212.9 MiB on Flash-Next); it now uses the MTP's when the type, the vocabulary ids and the layout match, and the expert cache is sized without the copy. The cache sizing also prices the DFlash2 head from the vocabulary file when there is no MTP.
2026-11-02 00:31:09 +01:00
Niko1221 681e509516 DFlash2: the MTP chain's cost window and q lists apply only to candidates the MTP drew
With DFlash2 as the drafter the MTP stays loaded as the fallback (use_mtp stays true), and three decisions tested use_mtp where they meant
"the MTP chain drew this round's candidates":
- STRATA_SPEC_COST=1 (a GB10 default): T = clamp(chain_rows, 1, S_mtp), but chain_rows is only written by the MTP chain callback, so with DFlash2
  and no --draft-conf the window stayed at one row (CLI and serve; code_py 175 vs 258 tok/s on an RTX PRO 6000, serve 166-230 vs 283-397).
- STRATA_SPEC_PROB=1, sampled requests: ver.set_spec_q(mtp.spec_q()) judged DFlash2's deterministic candidates against the MTP's stale q lists
  (CLI: 107 of 137 windows), and serve's gated path was not declined for DFlash2.
- the lookup choice read mtp.prob() for the same reason.
spec/window_rule.hpp holds the window rule and the q rule, both loops call it. The default path is unchanged: window_rule_test compares the new
rule with a copy of the old code over 9216 inputs (only the DFlash2 + cost rule + no --draft-conf cases differ). The CLI stats also print the
window histogram for any rule that sets the width and, when STRATA_SPEC_PROB judged windows, their count and source.
2026-11-02 00:30:13 +01:00
Niko1221 7dea06f75d Merge f-0291-df2-nv-a (DFlash2 source ownership) into f-0291-df2-mgpu; the window rule reads draft_on for min-p so DFlash2 alone keeps --spec-min-p 2026-11-02 00:30:10 +01:00
Niko1221 0c87b96f8c Merge f-0291-df2-mem (--draft-fallback none, DFlash2 head sharing) into the DFlash2-primary branch 2026-11-02 00:30:07 +01:00
Niko1221 5eb33fe4f3 Merge branch 'f-0291-df2-conf' into f-0291-df2-primary 2026-11-02 00:29:38 +01:00
Niko1221 166dcf0b11 Qwen picture reader on demand by default on an NVIDIA card with one GPU: the first picture starts it (the expert cache gives its room), 120 s without one stops it; it needs no config key (vision.elastic false or vram_elastic false keeps it on the card). The on-demand reader has the same program without --gpu as its CPU reader, so a picture is read whatever the card has left (found on boston: reserve 700 and no CPU reader gave a 503 on every picture) 2026-11-02 00:18:52 +01:00
Niko1221 bb2649ee8b SYCL: test-only in-process kernel timer (STRATA_UC_PROBE) for the uncached-host-memory slowdown on Arc Alchemist 2026-11-01 23:57:37 +01:00
Niko1221 1ada0f6202 SYCL DFlash2 Q4_0 drafter kernels (active-column GEMM, host rope tables) and a drafter-only bench for Intel Arc 2026-11-01 23:54:08 +01:00
Niko1221 09c407d8f4 DFlash2: --draft-fallback none (no MTP layer loaded, DFlash2 the only drafter, an error at start if it cannot run) and the DFlash2 draft head shared with the MTP's byte-identical subset (STRATA_DF2_SHARE_HEAD_OFF=1 keeps its own copy)
Per-run source choice, no mid-run fallback. With the MTP loaded the DFlash2 gathered a second copy of the 106299-row draft head (212.9 MiB on Flash-Next); it now uses the MTP's when the type, the vocabulary ids and the layout match, and the expert cache is sized without the copy. The cache sizing also prices the DFlash2 head from the vocabulary file when there is no MTP.
2026-11-01 23:51:17 +01:00
Niko1221 8bccfe281e docs: SPLIT_ENGINE.md with the copy-engine ring's numbers, the parallel table and the suite result 2026-11-01 23:46:34 +01:00
Niko1221 d4ab9edc3f split: the prompt path's chunk sums use the copy engines through host memory by default (suite 56/61 against 55/61 on the GPU-thread ring, parallel loads equal or better, prompts 17-20% faster); FNX_PF_RING_SM=1 keeps the GPU-thread ring 2026-11-01 23:44:53 +01:00
Niko1221 34c0ab47b2 DFlash2: the MTP chain's cost window and q lists apply only to candidates the MTP drew
With DFlash2 as the drafter the MTP stays loaded as the fallback (use_mtp stays true), and three decisions tested use_mtp where they meant
"the MTP chain drew this round's candidates":
- STRATA_SPEC_COST=1 (a GB10 default): T = clamp(chain_rows, 1, S_mtp), but chain_rows is only written by the MTP chain callback, so with DFlash2
  and no --draft-conf the window stayed at one row (CLI and serve; code_py 175 vs 258 tok/s on an RTX PRO 6000, serve 166-230 vs 283-397).
- STRATA_SPEC_PROB=1, sampled requests: ver.set_spec_q(mtp.spec_q()) judged DFlash2's deterministic candidates against the MTP's stale q lists
  (CLI: 107 of 137 windows), and serve's gated path was not declined for DFlash2.
- the lookup choice read mtp.prob() for the same reason.
spec/window_rule.hpp holds the window rule and the q rule, both loops call it. The default path is unchanged: window_rule_test compares the new
rule with a copy of the old code over 9216 inputs (only the DFlash2 + cost rule + no --draft-conf cases differ). The CLI stats also print the
window histogram for any rule that sets the width and, when STRATA_SPEC_PROB judged windows, their count and source.
2026-11-01 23:41:43 +01:00
Niko1221 2ad5867efb DeepSeek serve: test-only STRATA_TEST_NAN_STOP_AT=<n> takes the NaN stop once on the n-th request (does nothing when unset), to run the post-NaN path on a real engine 2026-11-01 23:32:18 +01:00
Niko1221 0941f1fe83 metal: DFlash2 Q4_0 drafter on the Metal window: fragment-order Q4_0 repack at load, df2_mma4s matrix-unit GEMMs, Q8_0 draft head on the matrix units (df2_mma8), DF-04 candidate cap (set_max_drafts), begin_request/note_rows/prepare, attention floor for a restarted lineage; builds on f-0291-df2-q4 2026-11-01 23:17:17 +01:00
Niko1221 d6a5a7837e README: the ISTA-DAS Coder row 2026-11-01 23:15:09 +01:00
Niko1221 37e7b738f5 metal: DFlash2 Q8_0 GEMM kernel over a repacked pack (df2_mm: 12.6 ms a round against 36), Q8_0 draft head, STRATA_DF2_DUMP and tools/dflash2/check_metal_dump.py (final rows cos 0.9999 against the torch port)
# Conflicts:
#	metal/src/engine/msl/dflash2.metal
2026-11-01 23:12:16 +01:00
Niko1221 1d36e65a14 metal: DFlash2 drafter on the Metal window (taps in the decode window and the prompt chunks, Q8_0 pack through the engine's GEMVs, windowed attention, top-16 selector); same Dflash2Drafter API as the CUDA build 2026-11-01 23:12:07 +01:00
Niko1221 5bb21e170a metal: df2_mma8 (draft head Q8_0 rows on the matrix units), stubs_df2.cpp from the first Metal port 2026-11-01 23:11:58 +01:00
Niko1221 488b67a1a9 metal: df2_mma4 / df2_mma4s, Q4_0 in fragment order on the simdgroup matrix units (split-K variant), df2_bench modes for it, soak and head cap; drafter round 3.0 ms GPU / 3.3 ms wall (was 7.1 / 7.4) 2026-11-01 23:08:27 +01:00
Niko1221 29e8a47ac9 DFlash2 confidence follows the expert slots the card holds after the drafter (9000+: 0.35, 3000+: 0.7, below: 0.8; the person's own --expert-cache N or --draft-conf wins); gfx1201 is a tested card (+7.2% short, +1.35% long), not a slower one 2026-11-01 22:55:53 +01:00
Niko1221 9f0794e993 Qwen picture reader on demand without the shrinking cache: a GPU reader that does not start (no room, card busy) is the CPU reader for that start and the next start tries the card again; a cache that cannot shrink and under elastic_mib free VRAM goes straight to the CPU reader; a CPU reader makes the cache grow back at once; pictures already encoded start no reader; --vram-elastic on a driver without virtual memory management starts as one block with a log line 2026-11-01 22:34:36 +01:00
Niko1221 21df2aa30d DeepSeek picture tower: in RAM between pictures. With --serve on a discrete GPU the tower is read into pinned host memory at the load and takes its ~320 MiB of graphics memory only for a picture turn (out of what --reserve-mib keeps free; kept on the card when the reserve is below its size + 192 MiB, on a GPU that shares the PC's memory, on Metal, or with --vision-resident). The expert split then counts the tower's MiB as experts' room. A card that has no room at the turn gets 3 retries, then the request's ERR names --vision-resident 2026-11-01 22:34:35 +01:00
Niko1221 11e02ea876 ISTA-DAS Coder: measured numbers in the menu text and the docs (KL, perplexity, speed on R9700 and RTX PRO 6000) 2026-11-01 22:29:19 +01:00
Niko1221 4938fcc9f0 metal: df2_bench, the cost of one DFlash2 drafter round on Metal with the Q4_0 pack's shapes (small kernels from the first Metal port, df2_mm4 Q4_0 matrix kernel, engine gemv_mma over native Q4_0 blocks); micro-benchmark only, not in the engine 2026-11-01 22:22:47 +01:00
Niko1221 05e276aa8c ISTA-DAS Coder: dense weights and RAM classes from measured runs on r9700 (placement lines, memory-capped one-card decode), words and menu row of its own 2026-11-01 21:58:06 +01:00
Niko1221 505dd93ed6 Load pictures: one line instead of two, a toast when only the files were fetched, the page driver counts new dialogs only
- With a loaded model that runs without pictures (no internet, no room), the existing "Picture reading needs a few more files" offer says the one short line
  instead of a second warning beside it; the offer is hidden while a download or load runs.
- "Download picture-reading files" ends with a toast; a model that is loaded without pictures is told to load again.
- Nine-language catalog: three more sentences. UI preview fixture: context 128K with its marker; the Chrome driver reports dialogs that are new after the click.
2026-11-01 21:51:15 +01:00
Niko1221 9e10513ad2 Load with Read pictures on and no picture files: fetch only those files, keep the model's pack, draft layer and data folder, no installer run
A model that is downloaded, in its own GGUF folder, with Read pictures on (the default) and no picture file started an install job on Load:
`--data-dir <app models folder>\Strata-data --gguf-dir <own folder> --vision yes`. That re-checked the model files (finish marks written in the
person's own folder), prepared a second pack and downloaded the draft layer into the wrong data folder, and rewrote data_dir in app-settings.json.

- strata_control/pictures.py (new): finds the pinned picture file (size and SHA-256; beside the model, one folder up, in the model's data folder) or
  downloads it once into <data folder>/models, shared; the data folder is the one the model's run config names (packs/<tag>, mtp/rt). Works out the run
  config's vision section for the engine in use with the same rule as setup (CPU reader under 24 GB, GPU reader from 24 GB, AMD/Intel Arc/Mac readers).
  No internet is one plain sentence after a short probe; a start never downloads (acquisition guard).
- native-control.mjs: Load (kind load, vision on) fetches the files through Control, writes the vision section, shows plain-word progress with a bar and
  loads. No internet, no room on the disk or a failed download: the model loads without pictures, nothing saved about pictures, one short line on the
  page. A restart or a tuning run never downloads. kind pictures does the same without a load (the "Download picture-reading files" button).
  A model whose only gap is a deleted picture file is no longer "missing files". installArgs: an installed model is prepared again in the folder its
  own run config names, never the app's models folder, and that folder is never written to the settings; the Drafting plan looks there too.
- Models page: the Load shortcut no longer needs the picture files prepared for the families Load can fetch for; the button and the line use the new kind.
- Tests: tests/terminal/test_pictures.py, apps/host/tests/load-pictures.test.mjs, UI preview fixture scenario, Chrome driver script, nine-language catalog.
2026-11-01 21:29:12 +01:00
Niko1221 f74bceeb6a split: the prompt path's finished part goes to the next card's gather buffer by a kernel, not a host-to-host cudaMemcpyAsync (which blocks the one thread that enqueues the cards in turn: the cause of the copy-engine ring stalling in the first read) 2026-11-01 21:25:01 +01:00
Niko1221 43c40ecb19 split: the launcher side: apply_to_config probes the cards (nvidia-smi) and RAM, reads the GGUF, and turns the run config into the split engine's with the normal one kept as fallback; strata launch calls it only when the config says split_engine true (or STRATA_SPLIT_ENGINE=1 for testers); tests 2026-11-01 21:05:45 +01:00
Niko1221 fcba411c41 split: packaging/build_split_cuda.sh (fails if the engine can call a peer function) and docs/SPLIT_ENGINE.md 2026-11-01 21:01:47 +01:00
Niko1221 ff913c6bf0 serve: a run config for the all-cards split engine starts the normal layer split, with one plain line, if the split engine refuses or fails to come up (start_engine_or_fallback, tests) 2026-11-01 20:58:08 +01:00
Niko1221 28f94d6890 split: the rule for when the all-cards split engine may run (off unless asked; Linux, 2-4 identical cards of a tested kind, UD-Q4_K_XL-type GGUF, every expert fits the cards, RAM free), its run config with the normal config kept as fallback, tests incl. no peer API in src/split; defaults: SM-ring prompt path, Strata's draft-layer files 2026-11-01 20:51:40 +01:00
Niko1221 9aea047eea Fix answers arriving one request late after an aborted request: no DONE after the NaN-stop ERR, server drops a trailing DONE, abort-sync test 2026-11-01 20:47:13 +01:00
Niko1221 333e113d53 split: host-memory exchange between the cards, no peer access anywhere: the ring, attention-exchange and prompt-path buffers live in page-locked mapped host memory; the peer probe, peer enabling, peer copies and the IOMMU/BAR1 start checks are gone; the 27B files are dropped; CMake option STRATA_BUILD_SPLIT (off by default) builds strata-split-N for N cards 2026-11-01 20:46:46 +01:00
Niko1221 b8668d0385 split: sFNX (johnfink8/sfnx f9ba1e9, MIT) as published: engine, engine process, CPU tests, two data files, LICENSE and NOTICE 2026-11-01 20:42:54 +01:00
Niko1221 c021f48d76 ISTA-DAS Coder: its own expert profile (519,422 prompt tokens of the calibration texts) and tests 2026-11-01 20:36:28 +01:00
Niko1221 93970e0a27 DFlash2 Q4_0 GEMM default: two warps a block, two rows a warp (variant 17); one warp a row (28) is best on a 188-SM card but 10-35% slower than 17 on l620's 110 SMs 2026-11-01 20:29:47 +01:00
Niko1221 16f513ad78 HIP: the PcieAuto start share from the pool size (06ebf2e4d, BW-MEASURE E26) is opt-in on AMD (STRATA_PCIE_START=new); CUDA keeps it by default. It flipped near-tie tokens in the first ~50 windows after a start on the R9700 for +-1%, the only default that made merged-with-the-other-defaults-off differ from v0.2.9 there (p620, G2: 21/45 -> 45/45 identical serve answers with the old start) 2026-11-01 19:49:00 +01:00
Niko1221 93e43af9e1 App: DeepSeek reads pictures by default (catalog images true, install keeps vision on, no reader device choice, a pack without the tower offers the missing shared file); load applies the --vision pair 2026-11-01 19:42:20 +01:00
Niko1221 adf7e54127 ISTA-DAS Coder: Strix and Spark note (sizes exact, speed not measured), app label test 2026-11-01 19:33:47 +01:00
Niko1221 4019121f24 DFlash2 Q4_0 GEMM: more rows-a-warp variants; default one warp a row (bench: down 18.7->10.3 us, fc 29.3->16.4, akp 8.2->4.1, o 12.4->8.2, qkv 11.9->9.5; 184/184 bitwise) 2026-11-01 19:28:37 +01:00
Niko1221 f5e2e7a86e comment 2026-11-01 19:24:07 +01:00
Niko1221 0c09456693 DFlash2 Q4_0 GEMM: default rows-a-warp table (bitwise: gemm bench 128/128 on srv118; down 18.7->12.2 us, o 12.4->8.2, fc 29.4->18.4, akp 8.2->4.1) 2026-11-01 19:24:02 +01:00
Niko1221 8dbdcf1b20 DeepSeek engine: an expert profile of another model's size is not read; the router tests run with 192 experts and poisoned padding (CUDA, Metal) 2026-11-01 19:18:48 +01:00
Niko1221 85dd085241 Add the ISTA-DAS Coder set (192 experts a layer): one JSON for the repository, setup recipe, fit numbers, menu rows, Mac note, tests 2026-11-01 19:18:48 +01:00
Niko1221 8752fc37ed Merge f-0291-int3 into f-0291-ds-coder (engine and DeepSeek plan numbers) 2026-11-01 18:49:22 +01:00
Niko1221 909aca3806 Merge branch 'f-0291-ui-final' into f-0291-ui-final2 2026-11-01 18:34:50 +01:00
Niko1221 f88fd15e8b Merge branch 'f-0291-agent-lost' into f-0291-ui-final2 2026-11-01 18:34:50 +01:00
Niko1221 d7d2ffab97 Take the rewritten agent-lost files (the cut sentence says the write did not happen and what the assistant is asked; the e2e script) 2026-11-01 18:34:50 +01:00
Niko1221 68baeabc54 Terminal and MCP agree with 128K: DeepSeek's setup default is 128K (a Mac keeps 8K), setup no longer calls the default untested, the pinned tests follow 2026-11-01 18:20:54 +01:00
Niko1221 601906b4de Merge f-0291-tui-update 3165c2e8a (its own wait for the stopped engine replaces the one found here) 2026-11-01 18:15:24 +01:00
Niko1221 d42341d797 A reply the length limit cuts inside a file write is no longer lost without a word: requests may produce half the window (compaction's reserve is unchanged), a cut reply is announced and the agent is asked to look at what is on disk and go on in smaller parts, and the file being written streams to the page (name, size, lines, tail) 2026-11-01 18:04:34 +01:00
Niko1221 de19caff76 The folder window no longer fails with an error when the Desktop folder is missing (it starts at This PC), and any failure of it now asks for the path instead of leaving no way to choose a folder (found in the review copy, whose profile has no Desktop folder) 2026-11-01 18:04:34 +01:00
Niko1221 6dbf88c784 Line endings of the files the agent-lost branch changed go back to LF (it saved whole files as CRLF; ui-do and anything that reads them by line broke); only its real changes remain 2026-11-01 17:59:32 +01:00
Niko1221 d97d130902 DFlash2 Q4_0 GEMM: rows-a-warp variants (gemm4_rows_kernel, bitwise the same results), STRATA_DF2_GEMM4, gemm bench q4 mode (auto table still the old kernel until measured) 2026-11-01 17:54:08 +01:00
Niko1221 31f0e8647c generate.cpp: fix the merged ERR line 2026-11-01 17:49:41 +01:00
Niko1221 32c37513e7 Merge f-0291-df2-option (Q4_0 drafter pack as shipped) into f-0291-df2-more 2026-11-01 17:47:30 +01:00
Niko1221 d29a6640e4 Chat: a file or command the model is writing right now is drawn from message.drafts through the open activity line: the file and its growing size, the last three lines when they read well, in a box that never changes height; drawn about three times a second, nothing animates 2026-11-01 17:44:39 +01:00
Niko1221 776fb9dbf5 Merge branch 'f-0291-agent-lost' into f-0291-ui-final2 2026-11-01 17:38:30 +01:00
Niko1221 944f06edba Merge branch 'f-0291-df2-option' into f-0291-ui-final2 2026-11-01 17:38:28 +01:00
Niko1221 bd7e44c90d 128K on every card, first-run setup and DeepSeek included: one rule (strata_control/default_context.py) for the app, the Terminal launch default, setup and the MCP install default; the tight-card fallback and DeepSeek's fixed 8K are gone, the inline warning stays 2026-11-01 17:35:17 +01:00
Niko1221 3165c2e8a5 After a model is stopped the update waits for its engine to let go; the start-again question follows what was changed 2026-11-01 17:15:20 +01:00
Niko1221 8550019663 A context picked after this update sticks, 32K included: saved with a marker (ui_context_chosen) by a load, an install or the first-run step; only an old unmarked 32K moves to 128K. The Tune dialog also saves no picture off when the engine has no reader 2026-11-01 17:13:27 +01:00
Niko1221 5ab569aa47 Tests that pinned the old words and places follow the new ones; a DeepSeek card keeps its own SSD line; the status line keeps its 24ch column 2026-11-01 17:02:14 +01:00
Niko1221 b541d7ad66 A reply the length limit cuts inside a file write is no longer lost without a word: requests may produce half the window (compaction's reserve is unchanged), a cut reply is announced and the agent is asked to go on in smaller parts, and the file being written streams to the page (name, size, lines, tail) 2026-11-01 16:56:39 +01:00
Niko1221 0038dbfd44 The folder window no longer fails with an error when the Desktop folder is missing (it starts at This PC), and any failure of it now asks for the path instead of leaving no way to choose a folder (found in the review copy, whose profile has no Desktop folder) 2026-11-01 16:56:39 +01:00
Niko1221 18aacf5dd0 Test: a load without a picture reader runs without pictures and saves no off 2026-11-01 16:55:06 +01:00
Niko1221 5d4183edfa Polish found in the preview: the SSD warning is said once (under the version picker), notes under controls keep their icon beside the words, the empty Subagents row is centred 2026-11-01 16:53:06 +01:00
Niko1221 01dd40aa8f Working ring: its turn is a loop in seconds, as the other loops (the audit wants tokens for transitions) 2026-11-01 16:51:14 +01:00
Niko1221 20c617a8ca Chat while it works: Stop and Send are one height so the box stops moving, one live cue per tool step, the caret follows text only, a slot for the host's live step and output on an open activity line, and the sidebar preview opens and closes in 460 ms 2026-11-01 16:48:58 +01:00
Niko1221 c738d5bf34 A chat that is working shows it on its sidebar row (a slow ring where the unread dot sits), and the model chip names the working chat while you are elsewhere 2026-11-01 16:48:53 +01:00
Niko1221 fbaff162bc DFlash2: graphics memory of the Q4_0 drafter is 0.8 GB (engine: 799 MiB) 2026-11-01 16:38:02 +01:00
Niko1221 8d6351f55f Load model: no pop-up. The press starts it; the plan is read first and a disk that is too small is said in the page and stops it; the download size sits beside the button and the DFlash2 drafter download is said in the page while it runs 2026-11-01 16:36:17 +01:00
Niko1221 04a007771c Tasks: the Board and Schedule help lines move into the header after the Tasks title, smaller, wrapping at narrow widths 2026-11-01 16:34:18 +01:00
Niko1221 e22a48d866 Subagents dropdown: with none, the title says No subagents recorded on the row with the X; no icon, no explanation, no empty body 2026-11-01 16:32:28 +01:00
Niko1221 e60e03762e Model files: the sentence is gone and Other local models... sits inside the component beside Storage settings 2026-11-01 16:32:05 +01:00
Niko1221 53f85fa815 Conversation context opens at 128K for every configuration (the model's largest size when under 128K); a tight card gets an inline warning, nothing is refused or lowered; DeepSeek keeps its checked 8K 2026-11-01 16:31:16 +01:00
Niko1221 943f29853a Version list: our recommended versions first and tagged Recommended, no Fits word anywhere, SSD streaming - will run slower for versions that do not fully fit; NVFP4 no longer says Doesn't fit (the fit follows graphics memory, RAM and SSD; Doesn't fit is left for no graphics card and under 16 GB) 2026-11-01 16:27:47 +01:00
Niko1221 0627b1ec63 This computer with a model loaded: only the free memory is left (no hardware line, name, Loaded, It takes about, numbers or SSD line); a version that fits says nothing 2026-11-01 16:25:17 +01:00
Niko1221 eb87544a58 Read pictures on by default for every model that can read them; a missing picture reader no longer saves an off, and the row has no sentence (tooltip only) 2026-11-01 16:24:20 +01:00
Niko1221 2b9c2980e2 DFlash2 drafter: the Q4_0 pack is made on the person's computer (522,458,112 bytes, pinned size and SHA-256), an existing Q8 pack keeps working 2026-11-01 16:22:06 +01:00
Niko1221 b2365cecf2 Models: no DFlash2 paragraph, no per-card line, no picture-reader sentence; the Drafting dropdown stays 2026-11-01 16:20:02 +01:00
Niko1221 811267e19f Terminal tests do not depend on the box's graphics card (the Drafting step changed the step count on Nvidia/AMD boxes); after a model is stopped for an engine update, wait briefly for its runtime host to release the engine lease (found on a real host) 2026-11-01 16:14:02 +01:00
Niko1221 27a3e056ab Int3 merge f-0291-df2-more (a84eebb51): bit-exact HC down kernel, default mask 133 on sm_120
# Conflicts:
#	src/program/generate.cpp
2026-11-01 16:11:56 +01:00
Niko1221 f7ada7119a Starting a model again waits at most what the runtime accepts (found by the real DeepSeek flow); flow fixes 2026-11-01 15:56:56 +01:00
Niko1221 3a3fd4391b Int3 merge f-0291-mac-q (bef28201f): FP16 default on resident IQ3_S and Q2_0 on Metal through setup's config, Coder on spec 3 2026-11-01 15:52:51 +01:00
Niko1221 bef28201ff Mac config: the fp16 decode (STRATA_METAL_F16=1) is the default of a resident IQ3_S or Q2_0 pack; the Coder joins --spec 3 --spec-min-p 0.5
Coder: expert-pruned 256-expert Coder, M5 Max, 5 interleaved pairs, identical greedy tokens in all 40 runs per class, over --spec 2: story -2.5%
(0/5), code +7.1%, JSON +9.3%, agent turn +12.5% (5/5 each). It has no Mac setup entry yet; the name is what a config for it would pass.

fp16 decode (PR 4807): setup writes STRATA_METAL_F16=1 into the config's env for a resident IQ3_S or Q2_0 pack (STRATA_METAL_F16=0 there turns
it off; streaming and the other sets are untouched). The owner's rule was an option unless it passes the 61-task quality suite: off / on / off
again on IQ3_S 54/61 each, on Q2_0 54/61 each, the same 7 tasks failing in all arms, no family drop, no new hallucination flag, 0 pass/fail
changes between two runs of the same arm; decode +14% (IQ3_S) and +15% (Q2_0) in the suite runs, top-1 96-97% and mean KL 0.009-0.016 against fp32
(phaseA-tests/mac-q/RESULTS.md sections 20, 22, 30).
2026-11-01 15:52:11 +01:00
Niko1221 95597c2dae Windows gfx1151: launch the verify window's graph from a helper thread (fixes the Q4_K_XL first-window start stall)
On a Strix Halo under Windows the first cudaGraphLaunch of a doorbell window sometimes never returned: the GPU had already rung layer 0
("GPU rang 1, flags served 0") while the main thread, which serves the doorbells, was still inside the launch call. HostGraphLaunch (the
CUDA instrumented-launch helper, STRATA_VERIFY_ASYNC_LAUNCH=1) is now compiled for the Windows HIP build too, and arch_defaults sets the
switch for Windows gfx1151 (launch_default_env; STRATA_GFX1151_DEFAULTS=0 or STRATA_VERIFY_ASYNC_LAUNCH=0 turns it off, a user setting is kept).

Measured on the strix box, UD-Q4_K_XL, default queues, 19,853 slots, interleaved within one job: old launch 14 hangs in 75 starts
(t2 4/20, t5 3/25, release build 7/30), helper-thread launch 0 in 75 (+ 8/8 on the final build); decode 48.2 vs 48.4 tok/s; output tokens
identical; IQ3_S 8K prompt 1,413-1,427 vs 1,419-1,423 tok/s with identical output; quality quick subset PASS, agent loops 2/2.

Also STRATA_LAUNCH_PROBE=1 (diagnostic): launch-call time, GPU doorbell state during the call, window graph node count.

(cherry picked from commit 93b9264e30054996d1cb9e89d27d211ed1156b15)
2026-11-01 15:38:54 +01:00
Niko1221 e394c48572 Engine update dialog: space above the progress bar; the failed-update offer shows the reason only under Details 2026-11-01 15:05:56 +01:00
Niko1221 a84eebb513 Remove STRATA_DF2_COUPLED (coupled sampled picks): at a fixed 8-row window it does not raise acceptance (tokens per round -1.7% short / -0.4% long, first-draft match 0.616 vs 0.629; draft temperature 0.6 / 1.5 / 2.5: 2.885 / 2.728 / 2.559 vs 2.868 tokens per round plain), the selector's argmax is the better draft 2026-11-01 15:02:51 +01:00
Niko1221 5f24b96dbd STRATA_GR_V3_PF default on a compute capability 12.0 card: 5 -> 133 (adds the bitwise down-kernel register blocking for windows of 5+ rows; serve A/B on 8 classes, 12 launches per arm: spec 6 +1.21%, spec 8 +1.4%, ids identical 96/96 each) 2026-11-01 15:01:30 +01:00
Niko1221 4deb89ef72 Revert "HC read v3 up kernel, ANOTHER SUMMATION ORDER (STRATA_GR_V3_PF bit 9, off by default, owner-approved item 10): a row per quarter warp, four rows side by side; gr_v3_screen reports its difference from the plain read"
This reverts commit ddd88c9f85.
2026-11-01 15:00:47 +01:00
Niko1221 a000bec3ce Update engine with a model loaded: warn naming it, stop it with the person's yes through strata update --stop-model, progress in words, offer to start it again with the same settings (also on the old engine after a failed update); nine languages 2026-11-01 14:57:08 +01:00
Niko1221 0ffb1d567c DFlash2 falling back to the MTP: the MTP runs its own window (--draft-mtp-spec), not DFlash2's 6, unless the person typed --spec 2026-11-01 14:50:42 +01:00
Niko1221 c70e049954 Default reading size 12 px and interface size 90%: a size the person chose is kept, a save that holds the old defaults moves once; Reset returns to the new ones 2026-11-01 14:50:42 +01:00
Niko1221 052c092856 Merge f-0291-tui-update (engine update beside a loaded model) into the app branch 2026-11-01 14:44:21 +01:00
Niko1221 ddd88c9f85 HC read v3 up kernel, ANOTHER SUMMATION ORDER (STRATA_GR_V3_PF bit 9, off by default, owner-approved item 10): a row per quarter warp, four rows side by side; gr_v3_screen reports its difference from the plain read 2026-11-01 14:29:22 +01:00
Niko1221 e5354698f9 gfx12: the 'matrix-core QSA scorer is on past 28,672 tokens' start line is not printed when STRATA_SELECT_WMMA=0 (the scorer is off then; the other variable of the pair was still being defaulted, which set the flag) (found by G2 on p620) 2026-11-01 14:27:48 +01:00
Niko1221 0e8e4d4c61 HC read v3: drop the up-kernel register blocking (slower: up 9.2 -> 10.6 us at T1, 18.8 -> 23.9 us at T8); the down kernel's register blocking stays: bit 7 = 2 rows a warp for windows of 5+ rows, bit 8 = 4 rows (bitwise; down 16.1/16.2/17.6/19.5 -> 13.3/14.1/14.7/15.6 us at T5-8) 2026-11-01 14:26:53 +01:00
Niko1221 f19d364637 locale-gaps test reads the df2 catalog 2026-11-01 14:22:22 +01:00
Niko1221 f8f4da07df gr_v3_screen: room for 16 variants 2026-11-01 14:20:52 +01:00
Niko1221 beb98760be Int3 merge f-0291-arc-a (a29977bd2): STRATA_SELECT_GEMM default on Battlemage only 2026-11-01 14:12:57 +01:00
Niko1221 26dc2a81c4 Docs: an engine is never updated by itself; flow script takes a prepared data folder 2026-11-01 14:11:12 +01:00
Niko1221 4ab81c7968 HC read v3 down kernel: register-blocked dot phase for 2 / 4 rows a warp (STRATA_GR_V3_PF bits 7 / 8, any window size; same dots, same order) 2026-11-01 14:07:07 +01:00
Niko1221 692b2573ea Int3 merge f-0291-ram-fallback (067b74338): #4846 falls back to --mmap-experts instead of refusing 2026-11-01 14:05:49 +01:00
Niko1221 686c0d42c7 HC read v3 up kernel: register-blocked dot phase (STRATA_GR_V3_PF bit 6, token loop outside the row loop, same dots in the same order); gr_v3_screen compares it bitwise 2026-11-01 14:05:41 +01:00
Niko1221 611ff41d41 DFlash2 option: reachability check through the download's own opener, one-request rule only when the drafter file is there, tests independent of the box's card 2026-11-01 14:00:01 +01:00
Niko1221 a29977bd2a SYCL: STRATA_SELECT_GEMM default (GEMM block scores from 2,048 blocks) only on a Battlemage card; =1 and =0 still decide on every card
The default changes the answers and gained +0.2% to +0.6% on an Arc A750 at 16K, 32K, 64K and 128K (3 runs per arm up to 64K, one pair at 128K; first-token KL 1e-9, same top-1 and tokens): its prompts are bound by expert streaming over PCIe. Under the 5% bar, so Alchemist and other devices keep the warp kernel unless STRATA_SELECT_GEMM=1. Battlemage unchanged (+19% to +28% at 100K to 128K on 2x Arc Pro B65).
2026-11-01 13:52:23 +01:00
Niko1221 e4d2bccd77 Flow scripts: a DeepSeek flow, the python for the throwaway feed 2026-11-01 13:44:52 +01:00
Niko1221 1ebae96167 DFlash2 chosen on purpose starts with one request at a time (request slots keep the MTP), Terminal Drafting step, plan line 2026-11-01 13:43:37 +01:00
Niko1221 067b743385 #4846: when RAM is still short at the expert arena, read the experts from the file like --mmap-experts
After the 20 s wait and the give-back of the PLE lock, a shortage no longer registers the arena anyway (a 60 GiB host stalled in reclaim until it was power-cycled). The run uses the --mmap-experts expert source, says so in one line with one next step, and the start never refuses. A hand-set --ple-io ram and --shared-expert-arena are kept as asked (warn only); a hard cgroup limit below the arena falls back instead of refusing; STRATA_ARENA_RAM_FALLBACK=0 is the old behaviour. If the file cannot be opened either, the arena loads the old way. Decision rule and tests in ple_residency.hpp / ple_residency_test.
2026-11-01 13:43:36 +01:00
Niko1221 e6ef7eed63 Tests follow the new rules: a model beside an update is refused before any download; the launch no longer upgrades DeepSeek; the monitor line 2026-11-01 13:36:10 +01:00
Niko1221 d615a9d643 DFlash2 as a real option: the dropdown only where there is a choice (not DeepSeek), --spec 6 --draft-conf 0.35 with it, only on CUDA and HIP engines, one plain line per card, no-internet message, verified files reused, SSD line label 2026-11-01 13:34:24 +01:00
Niko1221 3b1d9ae360 DF2-MORE: serve STRATA_SPEC_TRACE (per round window, accepted, selector probabilities, drafts, lookup draft, committed tokens; bench only) and STRATA_DF2_COUPLED, the selector's picks drawn with the target's Philox uniform for sampled requests (opt-in, verify unchanged) 2026-11-01 13:32:20 +01:00
Niko1221 687527d7a3 Int3 merge f-0291-ds-left final (ea2e791d8): screens say 5 experts on 3+ RDNA4 cards 2026-11-01 13:15:20 +01:00
Niko1221 ea2e791d8b Screens and app say 5 experts on several RDNA4 cards that hold half of the experts, as the engine does
The engine gives 5 experts per word on two or more RDNA4 cards that together hold at least half of the experts (4 x R9700), but strata launch --plan, the Speed screen and the app's Speed label called Full quality the default there.
dsv41_fit.rdna4_faster() applies the engine's rule with the plan's own numbers: one card the run uses (on Windows one AMD card): yes; several: yes when the plan puts at least half of the set's experts on them (3 or more R9700 for every official set; two hold 37-47%, so they keep 6). strata launch uses it with the chosen model; the model catalog carries rdna4_faster per DeepSeek size for the app (NativeSize, Speed label); --speed help and docs/MODELS.md say it. Other makes and older Radeons are unchanged.
2026-11-01 13:14:36 +01:00
Niko1221 dc96ad7033 Int3 merge f-0291-bw-draft (75d742778): DFlash2 drafter levers (bit-exact), Q4_0 pack path
# Conflicts:
#	src/kernels/cuda/dflash2.cu
2026-11-01 13:10:36 +01:00
Niko1221 89aa30476f Engine update beside a loaded model: never by itself, the person's yes first, stop cleanly, start again with the same settings; a quiet notice and a once-a-day check in the launch monitor 2026-11-01 13:09:21 +01:00
Niko1221 88a79a7efd FP16 weight cache (cda6463e8) OFF by default again, as in v0.2.9: gemm_dequant_side_parity fails with it on sm_86, sm_120 and sm_121 (the cached full-N GEMM rounds differently from the sliced form), so it is not bit-exact and has no 5%+/KL/owner yes. STRATA_PF_W16_CACHE_MIB=N still turns it on by hand. Flip back: set w16_budget_ = 16ll << 30 in the no-env branch of Gemm::native (gemm.cu) 2026-11-01 13:07:37 +01:00
Niko1221 0f7159ca35 Int3 merge f-0291-ds-left (386d2e441): #4677 stop bound, 5 experts by default on one RDNA4 card 2026-11-01 13:07:20 +01:00
Niko1221 4faa0cf0ee Models > This computer: free graphics memory and RAM from real readings, a loaded model changes the card, SSD only when it does not fit 2026-11-01 13:05:58 +01:00
Niko1221 d2c11867c0 Version list: only the chosen base model, recommendation and downloaded versions on top, the rest folded below, a Fits / Fits with SSD streaming word per row 2026-11-01 13:04:58 +01:00
Niko1221 386d2e4416 DeepSeek: 5 experts per word by default on a single RDNA4 card (R9700, RX 9000); --experts N still wins
One R9700 holds under a fifth of a 2.25 bpw set, so the CPU lane and the SSD set the speed, as on the 16 GiB cards and the multi-card RDNA4 rule that already take 5.
1 x R9700 on p620 (125 GB RAM), 2.25 bpw, frozen expert profile, 5 interleaved pairs, 5 against 6 experts: decode 15.24 against 13.08 tok/s (+16%, 5 of 5 pairs), a scripted agent loop -24% wall time (5 of 5), the long-read loop -12% (5 of 5). KL against the 6-expert run 0.030 (6 against 6: 0.008); against the original model 0.1106 against 0.1067-0.1071. quality027 quick: 18 of 19 with 5 experts and with 6, the same task flagged in both.
ds4_experts_rule.hpp: experts_per_word() takes single_rdna4 (the engine passes rdna4() with one device); DS4_DEFAULTS=026 gives the model's 6 back. The Terminal's Speed words (strata launch --plan and screens), the app's Speed label (arch added to RunCard) and docs/MODELS.md say the same. Several RDNA4 cards keep the existing rule (they must hold half of the experts); the screens still do not mirror that one.
2026-11-01 13:04:39 +01:00
Niko1221 142d5c6195 strata tune / tune recheck: the scratch folder the command makes for itself (strata-tune-*, strata-recheck-* in the temp folder) is removed when the run ends; --work-dir keeps its logs (G4 clean-machine finding: it was left behind) 2026-11-01 13:00:42 +01:00
Niko1221 c25a2de200 Sidebar preview: previewable pages open there first, and it comes in and goes out with motion 2026-11-01 12:57:56 +01:00
Niko1221 211ca210fb Code block preview is an icon button left of Copy, with an open and a close state (HTML and Markdown blocks) 2026-11-01 12:57:56 +01:00
Niko1221 b63a82e011 Code blocks: room before and after, a lighter 34px bar and tighter padding (12px floor kept) 2026-11-01 12:57:39 +01:00
Niko1221 e47848a319 Reasoning opens and closes smoothly: grows and fades, hover and press on the line, the rule belongs to the body 2026-11-01 12:57:39 +01:00
Niko1221 0a8499b0a3 Motion system: one press feedback and the reduced-motion gate for it (tokens in liquid.css section 17) 2026-11-01 12:57:39 +01:00
Niko1221 a44bcd4eb5 Copy: no Copied popover at the corner, the icon's check is the feedback (failures still say so) 2026-11-01 12:51:36 +01:00
Niko1221 a8e07c1b52 Version list: tags stay together on one line, a downloaded version shows its size without the word download 2026-11-01 12:51:20 +01:00
Niko1221 6f686536e5 Model versions list: each version once, plain names, small tags, details behind a toggle 2026-11-01 12:50:14 +01:00
Niko1221 f009913c6d Launch: the progress lines name a DeepSeek set as the screen does (ISTA-DAS 2.25 bpw, not 'deepseek41 IST-DAS-2.25bpw'), and the DeepSeek engine package is said to be about 430 MB, not 70 MB (G4 clean-machine findings) 2026-11-01 12:43:37 +01:00
Niko1221 b3dbb2c67c Light mode New chat: the shared primary look, green text on the tint (5.48:1) 2026-11-01 12:41:06 +01:00
Niko1221 63ae1eb3cd Settings: Privacy is one compact paragraph 2026-11-01 12:38:13 +01:00
Niko1221 87610346d6 Skills, Monitor and the other screens share the chat title size (one rule, tested) 2026-11-01 12:37:13 +01:00
Niko1221 7f36ac9b2e Tasks: header is just the title, at the chat's title size 2026-11-01 12:37:08 +01:00
Niko1221 ea6fcea1d8 Window title is Strata on every page 2026-11-01 12:34:39 +01:00
Niko1221 63a9b7d2ee Int3 fix (ds-left 67eb5f1f5 beside fixes 27060866e): the own-files disk verdict reads DeepSeek's memory words, so it only applies to the deepseek41 family; other families keep 27060866e's rule (no disk warning for files used where they are) 2026-11-01 12:34:18 +01:00
Niko1221 13658e773a Engine flow script: the server pid survives (no subshell) 2026-11-01 12:27:09 +01:00
hanchengme 243fdb213c serve: a request commits only the rows it can emit, so the parked snapshot matches what the client got (#4677)
In the single-request speculative paths the accepted rows of a verify window were committed first and the output loop cut to max_new or EOS afterwards, so a request's last window could park a snapshot with tokens the client never received. conversation_prefix() needs the whole snapshot to be a prefix of the next prompt, so the snapshot was refused and the older checkpoint was used (about 6% of a 12K-token prompt read again).
serve_stop_bound() caps the accepted rows at the rows the request can still emit and ends at the first EOS, before pl_commit_async. Merged prompt windows keep committing every row. HIP only (STRATA_USE_HIP), where it was measured.
Patch and policy test (2,736 cases against the old loop) by hanchengme, from the issue's attachment. Two changes on our side: it is on by default, STRATA_SERVE_STOP_BOUND=0 turns it off; the test is registered in CMake.
Measured on 4 x R9700, Qwen3.8-Flash-Next IQ3_S, 2 replicas x 2 cards, --batch 8 --batch-groups 2, four 11-15K-token conversations with 768-token answers, conversations opened one at a time: 4 of 16 continuations fell back to the older checkpoint without it, 0 of 16 with it (reuse 98.36% against 99.74%). Opened together as batch rows both builds miss the same continuations (slot overwritten before it is parked); not changed here.
2026-11-01 12:21:56 +01:00
Niko1221 58aeca07da Int3 merge f-0291-ds-left (67eb5f1f5): DeepSeek plan numbers, false disk warning. dsv41_fit.py: ds-left's accounting replaces the older AMD fit of 896818a3d (AMD_DENSE_ON_GPU_GB / AMD_RESERVE_GB); that commit's test (2,366 / 12,147 within 0.5%) is superseded by ds-left's MeasuredCardAccounting (within 3%, engine startup lines of d93547aed) and is removed 2026-11-01 12:20:52 +01:00
Niko1221 d92b6350ed Engine flow script: release files can be given from a folder (a box without internet) 2026-11-01 12:01:51 +01:00
Niko1221 75d7427789 Revert the T=5 HC down variant choice: in the engine the T5 step stays (hc down 0.77-0.79 ms either way, wall 16.17 against 16.14 ms), the cold screen's 0.7 us did not carry over 2026-11-01 12:01:48 +01:00
Niko1221 098154961d plain-errors test reads the engine catalogs too 2026-11-01 12:01:33 +01:00
Niko1221 295953de95 Engine tab host: a folder name must start with a letter or digit (never an option) before Control is asked 2026-11-01 12:01:33 +01:00
Niko1221 e6bb877df7 Engine tab: real-package flow script, update notes, update-cache test follows the split 2026-11-01 12:01:33 +01:00
Niko1221 5dea0d2abd Settings > Engine lists the installed engines (switch, delete, sizes); About only updates. Updates keep every older engine; strata components engines/use/remove share the app's code; DeepSeek has its own choice that launch, setup and the app follow 2026-11-01 12:01:33 +01:00
Niko1221 b5c237678a First-run model step starts on an installed ready model, else the catalog's recommendation, never the first size
GuidedSetup filled its family and size once at mount. When the catalog had not arrived yet it fell back to the first size in the list (Q2_0, a 66 GB download) even with an installed IQ3_S on the computer. Only the person's own choice is state now; the start is worked out from the catalog on every render (startingChoice in native-catalog.ts): a ready installed version first (the recommended one if it is among them), else the recommendation. No design change.
2026-11-01 12:00:32 +01:00
Niko1221 334e11b794 Int3: test_a_mac_is_stopped_before_anything_else follows the ISTA-DAS spelling of setup's stop message (my change in fbdcd5cbe) 2026-11-01 11:41:51 +01:00
Niko1221 ab583b6ad5 dflash2 GEMM kernels: pick the matrix of a warp's rows with constant indices (the by-value Df2Segs no longer lives in local memory: STACK 104 -> 0 in cuobjdump for every gemm4/gemm8/gemm_rows instantiation) 2026-11-01 11:40:39 +01:00
Niko1221 7d4feb2e8a HC read at T=5: the down kernel takes two rows a warp instead of the batched-staging variant (screen: 15.5 against 16.2 us, bitwise the same dots); mmvq_il_parity gets the 106,299-row draft head case 2026-11-01 11:38:10 +01:00
Niko1221 02996cf733 Int3 fix (test fixture; belongs to f-0291-term-small): StrixHaloCache's fake Strix Halo had vram_gb 2.0 beside shared_gb 112, which setup's detection never produces (vram_gb is the carve-out plus the shared pool); the new uma_cache_guard read the 2 GB as the whole pool and lowered the budget to 4 GiB, so test_cache_is_a_number_that_leaves_room failed. The fixture now says 114 2026-11-01 11:31:43 +01:00
Niko1221 54ef8615bb Int3 merge f-0291-term-small (da8c7f4a4): Terminal fixes and tests 2026-11-01 11:19:09 +01:00
Niko1221 da8c7f4a4f Tests: five terminal tests no longer depend on the box they run on (a system cloudflared, no llama.cpp checkout, Linux library path, the default data folder equal to the typed one) 2026-11-01 11:01:18 +01:00
Niko1221 ce3e621e34 UI merge fixup: five approved-item style blocks brought inside the owner's audit (npm run audit: 6 failures -> 0); behaviour unchanged
The owner's audit now scans every app sheet, and f-0291-ui's appended blocks broke
its rules (flat, tokens only, shadow only on surfaces that overlap content):
- .native-progress (#4802): gradient + --st-sh-pop shadow -> flat --st-accent-tint
  fill, the accent border stays (it still stands out; no shadow).
- .approvaldock > .askcard/.toolcard (#4725) and .approvaldock > .askq (#4244): the
  --st-sh-bar shadow dropped; the accent border that marks them stays.
- .st-code .st-btn--icon[data-copied] (#4748): the literal rgba(255,255,255,.08)
  is now color-mix(--st-code-ink 8%, transparent), the same recipe liquid.css uses
  for the code block edge.
- .quote-selection (#4785) floats beside selected words, so it is added to the
  audit's ELEVATION_ALLOWED list instead of losing its shadow.
2026-11-01 10:59:17 +01:00
Niko1221 fbdcd5cbea ISTA-DAS: the setup message that names the two sets and the DeepSeek doc's setup commands use the on-screen spelling (--model ISTA-DAS-2.25bpw is accepted by model_alias; IST-DAS-... and ist-das-... keep working) 2026-11-01 10:57:29 +01:00
Niko1221 2ff9783ac6 UI merge: the owner's 92 UI passes (f-gui-look working tree, taken read-only) onto f-0291-int3 with the approved UI list
Merged f-gui-look-snap (682c21d64): the owner's uncommitted wt-gui-look work as one
commit on 4cf0d8cd3, 32 files +343/-216 plus the two untracked files
apps/host/src/styles/liquid.css and apps/host/scripts/browser.mjs. Line endings were
normalised from CRLF to LF when the snapshot was taken (the raw working-tree diff of
~14,800 lines is the CRLF flip; nothing of it is carried here). The approved UI list
(f-0291-ui, 14 items) is already in f-0291-int3.

Three-way merge result: no textual conflicts. Files both sides changed, and how each
resolved:
- components/NativeModels.tsx (int3: progress card, context memory + KV hint; owner:
  Manage models ghost/sm + compact dialog, "Other local models" class, Expert scroll
  waits for layout): disjoint hunks, both kept.
- lib/plain-errors.ts (int3: out-of-VRAM rule; owner: unwrap {"error":..} / HTTP 400
  bodies before the rules run): both kept; the unwrap runs first, so a wrapped
  out-of-VRAM message still reaches its rule.
- screens/Serving.tsx (int3: two addresses, CopyBtn; owner: empty-state "Connect an
  app" is the primary button): disjoint, both kept.
- screens/Settings.tsx (int3: While-an-answer-is-written row, Select import; owner:
  Engine panel gets the "Engine" heading): disjoint, both kept.
- styles/app.css (int3: appended blocks + the reading-comfort rule; owner: edits in
  place): auto-merged, both kept.
- styles/onboarding.css (int3: language Select rules appended; owner: welcome
  headline, badge ink, glow removed): disjoint, both kept.
- components/TuneRunView.tsx also auto-merged (int3 +1 line, owner: progress bar uses
  transform).
2026-11-01 10:56:47 +01:00
Niko1221 682c21d641 Snapshot of the owner's UI passes 1-92 (wt-gui-look working tree, uncommitted, taken read-only; line endings normalised to LF; includes untracked liquid.css and scripts/browser.mjs) 2026-11-01 10:54:55 +01:00
Niko1221 8c6c0a8a9f Int3 (test only): cuda_spill_guard's ALLOWED table brought to the measured state. On v0.2.9 the guard already failed on 11 kernel families; now it passes on the merged tree on Windows MSVC sm_120, z440 sm_86+sm_120, boston sm_86, l620 sm_120 and Spark sm_121 and fails again on a real increase. New in 0.2.9: dflash2 gemm4_kernel (STACK 104, Df2Segs struct on the stack: BW-DRAFT) and prefill rms_rope_rows_kernel (STACK 32, RopeTab struct on the stack: STRIX-PF). No kernel changed 2026-11-01 10:52:27 +01:00
Niko1221 424a2af019 Int3 fix (belongs to f-0291-amd-q b9e3e2b86): prefill.cpp's no-fused stub block lacks fused::gfx12_iq2s_default, so a HIP build without STRATA_PREFILL_MMQ (every Windows HIP build that does not pass it) fails to link; release builds pass MMQ=ON and were not affected 2026-11-01 10:48:56 +01:00
Niko1221 7a87440629 DFlash2: drop the 128-thread QK tile (bitwise but 18% slower than the 64-thread tile in the bench) 2026-11-01 10:45:26 +01:00
Niko1221 2b32d0ff0b Int3 merge f-042-maya-rc (690f8b234): DS4_BORROW off by default on Metal 2026-11-01 10:44:03 +01:00
Niko1221 690f8b234a dsv41 Metal: the prompt path does not lend the experts' room by default (DS4_BORROW defaults off on Metal)
M5 Max, DeepSeek 2.25 bpw, 3 interleaved pairs: with the lent room a 639-token prompt took 6.2-7.6 s against 4.1 s with DS4_BORROW=0 (same bytes
read, the compute waited for reads equally long), 8K prompts -3.9%, 32K -20% (borrow also selects the 32,768 chunk); and the engine died
("mtl: wrap 7763574784 failed", exit 4) in 8 of 9 runs of a mixed 165/600-token/8K sequence that the build with DS4_BORROW=0 finished 3 of 3.
Only Metal changes (one_gpu_or is under STRATA_METAL); CUDA and HIP keep devs.size() == 1. DS4_BORROW=1 turns it back on.
2026-11-01 10:42:29 +01:00
Niko1221 9df71d5980 DFlash2: opt-in Q4_0 drafter pack (from 15165e170): dtype q4_0, tools/dflash2_pack.py --q4_0, nibble-packed tiled weights, gemm4_kernel, df2_q4_tile at load, CPU parity test dflash2_q4_parity 2026-11-01 10:20:59 +01:00
Niko1221 d7e50a515b DFlash2 drafter (bitwise): draft head through the interleaved Q5_K kernel, drafts only to the widest window - 1, R-split Q8 GEMM by matrix size, one rope table launch with precomputed inverse frequencies, selector projection beside the head, 128-thread QK tile variant (bench) and bench harnesses that check bitwise equality 2026-11-01 10:20:18 +01:00
Niko1221 0d2ee8defe Int3 fix (test programs only; four were already broken on MSVC in v0.2.9, gr_v3_screen is new): setenv / unsetenv / __builtin_popcount shims so a Windows build with tests ON compiles 2026-11-01 10:05:00 +01:00
Niko1221 4bc7e35e41 Int3 fix (belongs to f-042-strix-pf and f-0291 b89bf5947): Metal links again. stubs_0291.cpp (qsa_prompt_attn_batch_gated answers not taken, rms_rope_rows stops with its name), io_priority.cpp in the Metal host sources (strata_ds4 calls restore_normal_io_priority) 2026-11-01 09:58:02 +01:00
Niko1221 af8b47d45f Int3 fix (belongs to v0.2.9, f-0291 NVFP4 port): mmq_nvfp4_parity uses x86 F16C intrinsics; only built on x86 so an aarch64 CUDA build with tests ON no longer fails on -mf16c 2026-11-01 09:58:02 +01:00
Niko1221 67eb5f1f57 DeepSeek plan: experts on the cards from the engine's own accounting; own files need no free disk
The plan said 2,947 experts for one R9700 and 13,180 for four; the engine held 2,366-2,566 and 12,147-12,192. It took 4.5 GB of dense weights and 1.15 GB of reserve per card and forgot the driver context, the prompt buffers and the dense weights every card repeats. Constants now come from the engine's startup lines (f-042-maya-rc d93547aed): 2.25 bpw one R9700 2,543, four 12,260; boston 2 x A6000 within 1%, A4000 and RTX 5070 within 5%.
strata launch --model-path on files that stay where they are printed 'tight: needs more disk space' with nothing to download: the menu row counted the whole download against Strata's folder. With own files the verdict now comes from the memory words alone.
2026-11-01 09:56:34 +01:00
Niko1221 34c0f7dae7 Launch: a remembered card choice is said when the PC has more cards (NVIDIA DeepSeek after --gpu 0 stayed on one card without a word); Strix Halo: a fixed small expert cache lowers setup's own RAM budget (the person's cache and budget are only warned about) and a silent exit code 1 gets a plain next step 2026-11-01 09:51:26 +01:00
Niko1221 14c15d67a0 Tests: every shared DeepSeek file has a pin, so a copy placed by hand is checked locally and an offline PC with all files can start 2026-11-01 09:51:26 +01:00
Niko1221 ab6dfa03d6 Setup on a Mac: only this user's engine stops a model preparation (pgrep asks for this user's uid), so another person's server on a shared Mac no longer sends setup away with 'stop the server' 2026-11-01 09:51:21 +01:00
Niko1221 3410d5520d Int3 merge f-042-maya-rc (8e8e3cafa): Metal DeepSeek router NaN guard, dense Q4_K dequantizer
# Conflicts:
#	src/dsv41/ds4_gguf.cu
2026-11-01 09:50:55 +01:00
Niko1221 8e8e3cafad dsv41 Metal: router NaN guard in the MSL route kernels, and a dense Q4_K dequantizer; tests for both
Router: ds4_route and ds4_route_publish left an all-NaN gate row's pick at 0x7fffffff and read sc[0x7fffffff] (threadgroup memory) and
vram[0x7fffffff]; the test process dies with SIGBUS on the old kernel. ds4_route_fallback_pick takes the lowest expert not picked yet
(the CUDA/HIP guard's rule) and raises a flag in a buffer of the Metal host; router_nonfinite_take synchronises and reads it
(metal_host::router_bad_take), so ds4_main ends the request with the error it already has. The warning names the GPU on Metal; the
CUDA/HIP text is unchanged. Test: metal/tests/ds4_route_guard_parity.cpp (finite, all NaN, +-inf, half NaN, fewer than k finite).

Dense Q4_K: ds4_gguf.cu rows() sends a dense matrix of a type metal_host::dequant_rows does not know to iq::dequant_rows, whose CUDA
launch has no Metal dispatch and aborts the engine. ds4_dq_q4k (ggml's dequantize_row_q4_K, the Q5_K kernel without the fifth bit) is
added; any other dense type now fails with a plain message naming it. Test: metal/tests/ds4_dq_parity.cpp (Q4_K, Q5_K, Q6_K, Q8_0, BF16
against ref_quant, padded row stride).
2026-11-01 09:49:17 +01:00
Niko1221 02c367d5ce gfx12: fused IQ2_S / IQ4_XS gate/up kernels ON by default (STRATA_PF_FUSED_IQ2S=0 or STRATA_PF_FUSED=0 turn them off); the CPU share limit on gfx12 follows to 1,536 (owner 11-01) 2026-11-01 09:48:15 +01:00
Niko1221 cc703b5e48 #4040 --lookup-max-t 12 by default (serve, 8-row CUDA build, one GPU, no batch/DFlash2/layer split/chain); --lookup-max-t 0 or STRATA_LOOKUP_MAX_T=0 turns it off (owner 11-01) 2026-11-01 09:48:05 +01:00
Niko1221 daed630ed5 ISTA-DAS in the last user-facing lines: the setup --check list and the own-files message print the set's label (ids unchanged); comments 2026-11-01 09:38:03 +01:00
Niko1221 c24a6836aa Int3 merge f-0291-pr-amd (c31f1fa31) 2026-11-01 09:37:11 +01:00
Niko1221 6360274c8d Int3 merge f-0291-df2-spark (15165e170) 2026-11-01 09:37:10 +01:00
Niko1221 de856c7b8c Int3 merge f-0291-bw-next (4c5a55491); generate.cpp: req_df2 and draft_long both kept 2026-11-01 09:37:10 +01:00
Niko1221 ea9a58d8a9 Int3 merge f-0291-bw-df2serve (eeb21d0e4); generate.cpp: draft_df2 and draft_long both kept 2026-11-01 09:36:57 +01:00
Niko1221 0e2f24b6ad Int3 merge f-042-strix-pf (2970fe74a) 2026-11-01 09:36:39 +01:00
Niko1221 34f90e919c Int3 merge f-0291-gh2-speed (899935fae) 2026-11-01 09:36:38 +01:00
Niko1221 9e75dc6cf5 Int3 merge f-0291-pr-nv2 (e20842779) 2026-11-01 09:36:38 +01:00
Niko1221 af1cfe99cb Int3 merge f-0291-pr-nv (5e46c3415) 2026-11-01 09:36:38 +01:00
Niko1221 e208427797 Revert "serve: --lookup-long-match N, a lookup window past 8 rows only where the matched context is N tokens or more (opt-in, with --lookup-max-t)"
This reverts commit 394af4cd0f.
2026-11-01 06:51:24 +01:00
Niko1221 d10d67b005 docs: DETAILS.md, --lookup-max-t row now says 12 and carries the cap and agent-loop measurements 2026-11-01 06:51:24 +01:00
Niko1221 394af4cd0f serve: --lookup-long-match N, a lookup window past 8 rows only where the matched context is N tokens or more (opt-in, with --lookup-max-t)
Keeps the wide lookup sizes for copy-heavy text (a rewrite matches long) and leaves lists and prose, which match short, at the 8-row cap. Off by default. Tried and reverted in the next-but-one commit: it did not remove the prose cost of 16 rows and cost rewrite gain.
2026-11-01 06:51:24 +01:00
Niko1221 5e46c34159 DGX Spark: STRATA_SIDE_MIXER=2 is one of the sm_121 defaults (docs: five speed defaults)
Port of pull request 4737 by InB4DevOps (commit c14ee15d9 on v0.2.8). The mixer's input-only QSA/GDN work runs on graph branches; scheduling only, no arithmetic change.
STRATA_SM121_PERF=0, STRATA_DEFAULTS=026 or STRATA_SIDE_MIXER=0 turn it off. Measured here on a Spark, IQ3_S, greedy: ids identical 48 of 48; alone +1.0..+2.9% per class, with mode 8 +3.6% pooled.
Not yet measured at parallel 4.
2026-11-01 06:06:57 +01:00
Niko1221 f94a27da60 CUDA: grouped native experts use mode 8 on DGX Spark (sm_121) with the sm_121 defaults
Port of pull request 4699 by InB4DevOps (commit 76c0aa601 on v0.2.8). The existing mode 8 kernels are selected for compute capability 12.1 when the sm_121 table is on;
STRATA_SM121_PERF=0, STRATA_DEFAULTS=026 and an explicit STRATA_EXP_MODE keep the old mode 0.
Measured here on a Spark, Qwen IQ3_S, serve, greedy: bit-exact (ids identical 48 of 48), with the side mixer +3.6% decode pooled over 8 prompt classes (4 rounds), mode 8 alone +0.3..+2.4%.
2026-11-01 06:06:56 +01:00
Niko1221 2970fe74a3 QSA prompt attention (matrix-core kernel): less LDS a block and the next chunk's ids read ahead, same output bits
prompt_attn_wmma_kernel was limited by its LDS (21,648 B a block, 5 waves a SIMD), not by VGPRs (amdgpu_waves_per_eu
and amdgpu_num_vgpr change nothing here). Two exact changes, on by default:
- STRATA_PA_TRIM (default 2; 0 off, 1 first level): the score and probability buffers hold the 12 real query rows
  instead of 16 (rows 12..15 of the WMMA are zero queries nothing reads; the 4th wave skips the softmax of them),
  and at level 2 the staged V rows are 72 bytes instead of 80. LDS 21,648 -> 19,008 -> 17,984 B, 7 waves a SIMD.
- STRATA_PA_RPF (default on; 0 off): chunk ci + 1's selected-cell id and page-table reads are issued before chunk
  ci's compute.
The arithmetic and its order do not change. hip_prompt_attn_wmma checksums are identical across all variants (32K
context 8f3a85a9cb885e7e, 8K context de4684fb3f370000), and engine first-token logits with every exact lever of this
branch on against off are byte-identical on a 4.5K and an 8K prompt (IQ3_S pack). Kernel time, gfx1151, int8 KV,
STRATA_PA_FAST: 32K context / 2,048 queries 11.05 -> 9.95 ms, 8K context / 4,096 queries 21.76 -> 18.90 ms.

The idea of raising the kernel's occupancy is the qsa3 change of halo-box/strix-llama.cpp PR 213 (MIT, The ggml authors);
the means here differ and the code is ours.
2026-11-01 05:44:44 +01:00
Niko1221 c31f1fa31b gfx12: matrix-core QSA scorer (STRATA_SELECT_WMMA) past 28,672 tokens by default (changes the answers there; separate, droppable commit)
Needs the owner's yes: the existing opt-in STRATA_SELECT_WMMA=1 with STRATA_SELECT_ALT_MIN_TOKENS=28672 becomes the gfx12 default in
gfx12_extra.hpp, with its own log line.  A chunk whose context is below the floor keeps the warp kernel, so a prompt that stays under about
28K tokens is bit-identical (first-token KL 0.00000 on twelve 8K-24K prompts).  Off: STRATA_SELECT_WMMA=0, STRATA_GFX12_DEFAULTS=0 or
STRATA_DEFAULTS=026; a variable the user sets is kept.  Chosen over PR 3073's simt2 (author Vop3p) on the data.
R9700 (p620, IQ3_S, serve, interleaved pairs, prompt tok/s against unset, 3 pairs): 32K +1.83% (3/3), 98K +5.74% (3/3); first-token KL on seven
30K-96K prompts mean 0.0018, max 0.0055, top-1 7/7; needle recall 25/25 (8K to 96K, 5 depths).  Without a floor: 24K +3.34%, 32K +4.5..6.0%,
98K +6.9..7.3% but values change from 8K up (KL 0.0056); simt2 is slower below 32K.  arch_defaults_test now clears the variables this
default sets before its integrated-GPU checks; gfx12_extra_test covers the default and its off switches.
2026-11-01 04:50:11 +01:00
Niko1221 98d4f2033e Trial fix (belongs to f-0291-pr-intel 8fffe81d4): sycl_queue.hpp line 164 holds a raw NUL byte inside a character constant (the backslash-zero lost), tools/test_source_nul_bytes fails 2026-11-01 04:25:33 +01:00
Niko1221 899935faed Expert arena give-back message: the line ends with a newline escape again (a literal line break had split the string) 2026-11-01 04:03:37 +01:00
Niko1221 10ce6ae508 Trial fix (belongs to f-0291-gh2-speed eb8917247): expert_source.cpp #4846 message has a raw newline inside the string literal (the backslash-n was lost), so the file does not compile on any backend 2026-11-01 04:02:39 +01:00
Niko1221 b25c0c323d Trial2 merge f-0291-dq-test (e6d81d7a2) 2026-11-01 03:46:14 +01:00
Niko1221 0b244c0346 Trial2 merge f-0291-engine-card (27de07cef): NativeModels.tsx imports both EngineUpdateCard and duration; onboarding.css keeps both rule sets 2026-11-01 03:46:14 +01:00
Niko1221 2e8fb221cf Trial2 merge f-0291-gh2-app (0dfa2d9b1) 2026-11-01 03:46:05 +01:00
Niko1221 a17b79a7e8 Trial2 merge f-0291-keepcurrent (94edf723c): ds4_iq.cu keeps dq_old_ok<T>() (already on the line from 9d58393db), which covers keepcurrent's staging-fit condition 2026-11-01 03:46:05 +01:00
Niko1221 01f804ea18 Trial2 merge f-0291-mac-q (8e7ed6818) 2026-11-01 03:45:53 +01:00
Niko1221 9d642d1f71 Trial2 merge f-0291-amd-q (b9e3e2b86): share_rules/prefill.cpp take amd-q's gfx12 share limit; native_bf16.cu keeps bw-dense's per-device mmvf_fast and adds amd-q's STRATA_MMVF_CHECK mode on top (mmvf_fast_bit + mmvf_fast) 2026-11-01 03:45:53 +01:00
Niko1221 3fe5edd315 Trial2 merge f-042-strix-pf (66545a497) 2026-11-01 03:45:21 +01:00
Niko1221 87b2a61251 Trial2 merge f-0291-gh2-speed (eb8917247) 2026-11-01 03:45:21 +01:00
Niko1221 62db0bf10f Trial2 merge f-0291-pr-amd (82eaeb422): verify.cpp head keeps both: pr-nv2's chunked head (h0/HT, up to 8 columns a launch) and pr-amd's fused q8_1 image (q8_head, image at the chunk's own xq_ offset) 2026-11-01 03:45:21 +01:00
Niko1221 5438023a6a Trial2 merge f-0291-pr-intel (2b4d43bac) 2026-11-01 03:44:34 +01:00
Niko1221 2716defb83 Trial2 merge f-0291-pr-nv2 (64bf69080) 2026-11-01 03:44:33 +01:00
Niko1221 1306424beb Trial2 merge f-0291-pr-nv (97b3d614f) 2026-11-01 03:44:30 +01:00
Niko1221 4c5a554912 IQ4_XS 5-column interleaved MMVQ runs as 6 columns with 5 stored (STRATA_IL_PAD, bitwise); --draft auto: MTP requests keep the MTP's prompt-lookup width 2026-11-01 03:28:12 +01:00
Niko1221 d93547aed5 dsv41: no warm_flow on Metal (it aborted the engine at start)
warm_flow() runs cublasSgemm / cublasGemmEx on tiny scratch inputs at load. The Metal compatibility layer (metal/include/metal_compat/cublas_v2.h) only has the dispatches the real flow uses: its sgemm_tn found no host view for the scratch buffers and the call ended in strata_metal_dense_gemm_missing(), an abort (exit code -6) on every start, restarted in a loop by the server. start_warm() now returns at once under STRATA_METAL. Found on an M5 Max with the 2.25 bpw set: the v0.2.9 engine answered the four checks; this tree aborted until DS4_WARM=0 was set.
2026-11-01 03:13:37 +01:00
Niko1221 82eaeb4221 gfx12: STRATA_HEAD_MIX_MULTI=1 by default (exact; own function, droppable alone)
The final hyper-connection mixer of the verify window and of the draft layer as one fused_gr_read_multi instead of one gr_read per
token (PR 3264, author bsorensen110).  Kept apart from the main gfx12 table in gfx12_extra.hpp so it merges with the table on
another branch without touching it; STRATA_GFX12_DEFAULTS=0, STRATA_DEFAULTS=026 and a variable the user sets decide first.
R9700 (p620, IQ3_S, serve, static residency, 8 interleaved pairs with an A/A arm): json_extract +0.82% (6/8), agent +0.63% (6/8),
code_py -0.96% (3/8, that prompt's A/A moves 3.5% a pair); greedy ids identical in 16/16 and 15/16 request sets.
2026-11-01 03:10:59 +01:00
Niko1221 66545a497a QSA norm + rope fusion: keep the square out of the first butterfly add so the sum of squares rounds as rms_rows_kernel's does
On gfx1151 the compiler folded x * x into the first add of block_sum (fma of this lane's x with the other lane's
square), a different rounding than the sum of two rounded squares, so about 4% of rows came out with a scale one ulp
off. An empty asm on the square stops the fold (__fmul_rn compiles to the same fma). Checked in the assembly
(v_mul then v_add) and on the device: hip_qsa_nr_parity 44 of 44 OK, every output element bit for bit, T 1..8191;
engine first-token logits with STRATA_QSA_FUSE_NR / STRATA_QSA_FUSE_GATE on and off are byte-identical on a 4.5K and
an 8K prompt (IQ3_S pack).
2026-11-01 03:06:40 +01:00
Niko1221 64bf690800 window limit: STRATA_WINDOW_MAX_T=12 as a third value (8, 12, 16)
The exact-column instantiations 13..16 of the wide kernels (native MMVQ, the wave variant, the packed Q8_0 and Q6_K launches,
the BF16 multi GEMV) are built only above 12, MAX_NCOLS follows the cap, and kVerifyLongT follows a wide cap (a cap of 12 keeps
--lookup-max-t at 12). Measured for the narrow/wide comparison in phaseA-tests/pr-priorities/RESULTS-NV2.md.
2026-11-01 03:02:28 +01:00
Niko1221 55ab701e1b QSA selector: STRATA_SELECT_ALT_MIN_TOKENS keeps the warp kernel for short reads; the community simt2 default of gfx103x starts at 28,672 tokens 2026-11-01 02:57:52 +01:00
Niko1221 5206a299f8 docs: DETAILS.md rows for --lookup-max-t, STRATA_PF_GROUP_SCRATCH / STRATA_PF_HC_REGION, STRATA_PW_WITH_BATCH, STRATA_MTP_ROOM_MIN, with the numbers measured on f-0291-int-trial
The four switches are ports of PRs 4040 (merbanan), 4039 (merbanan), 3095 (Vop3p) and 3307 (gopinath87607); all stay off by default.
2026-11-01 02:57:12 +01:00
Niko1221 b130965ca6 AMD community tuning: the simt2 QSA selector (PR 3073, author Vop3p) is the default on gfx103x only
Mode 2 of STRATA_SELECT_SIMT was added for HIP in the previous commit as an opt-in.  For the RX 6000 cards its contributor measured it
on (gfx103x) it now joins the one community switch: on by default with STRATA_AMD_COMMUNITY_TUNING on, any STRATA_SELECT_SIMT value
the user sets wins, and no other card changes.  Values change in the last bits (another summation order); the contributor's numbers are
labelled as theirs in docs/AMD_HIP.md and have not been reproduced (no gfx103x card here).  amd_community_test checks the default and
that the switch turns it off.
2026-11-01 02:15:38 +01:00
Niko1221 b3e73dbfb6 Quality ids files: a token id outside the model's vocabulary fails loudly (the engine's --score / --profile-run / --spec-eval, and quant_quality.py run)
data/quant_quality/v1/set.ids.json is tokenized for Qwen (ids up to 248,069); fed to the DeepSeek engine it gave nonsense text and, for the
runs that dump logits, a late "token out of range".  read_score_ids now takes the model's vocabulary and refuses the file at once, naming the
segment, the id, its position and the fix; quant_quality.py run does the same against its config's tokenizer folder (or the vocab_size that
`ids` now records in the ids file it writes).
2026-11-01 01:54:24 +01:00
Niko1221 2b4d43bac2 sycl/CMakeLists.txt: drop the duplicate native_expert_bench target the #4185 port added (this tree has it already) 2026-11-01 01:46:33 +01:00
Niko1221 161eb97fc6 docs/INTEL.md: the #4185 decode kernels, the build option and the controls, with what is on by default 2026-11-01 01:30:29 +01:00
Niko1221 eb8917247a Expert arena: when the RAM still falls short after the wait, the PLE table lock the residency policy took is given back before the arena is registered (#4846) 2026-11-01 01:29:51 +01:00
Niko1221 4a7bb1d06d SYCL: only the six #4185 controls that measured a gain are on by default on Battlemage (MMVQ_SHAPE_RPW, IQ2S/IQ3XXS/IQ3S_GU_ROW_REUSE, GR_LO_REUSE, ATTN_MERGE_ACTIVE); the other eight stay opt-in (=1) 2026-11-01 01:28:51 +01:00
Niko1221 b9e3e2b86a gfx12: opt-in switch STRATA_PF_FUSED_IQ2S=1 for the native fused prompt kernels on the IQ2_S and IQ4_XS gate/up layers (Linux; the IQ3_S pack's 21 layers that keep MMQ), and the CPU share limit drops from 3,072 to 1,536 tokens only when it is on (share_rules::gfx12_share_limit), so a chunk from 1,536 up streams all and runs them. OFF by default: nothing changes without the variable. R9700 (p620, IQ3_S, 54% resident), on against the share-only default: prompt 2K +10.8%, 4K +12.2%, 8K +13.2%, 32K +10.0% (5 pairs each, 5/5), 1K and 1.5K unchanged; values change (first-token KL against the FP16 prompt path 0.0150 vs MMQ 0.0263). The quick quality suite lost 2 of 19 tasks with it on (summ-M-d14, code-S-d04) where off and off again agree, so it stays opt-in until the full 61-task suite has run. Separate commit: drop it alone to go back to the share-only default 2026-11-01 01:16:23 +01:00
Niko1221 8e7ed68181 Metal: STRATA_METAL_NFCHECK=1 counts non-finite logits and the largest |logit| in every head window (test tooling)
The engine's #879 check only sees logits that come back to the host; the Metal engine picks tokens on the GPU, so a half-precision overflow (STRATA_METAL_F16) would show as wrong text, not as an event. With the variable set, each head window's logits are scanned: a line for the first five windows that held a non-finite value, a count every 500 head windows. Off by default, no cost unset.
2026-11-01 01:07:41 +01:00
Niko1221 8fffe81d4b SYCL: the #4185 decode controls default on for Battlemage (STRATA_<name>=0 turns each off), STRATA_SYCL_IQ_DECODE_FAST default ON in CMake and passed by the SYCL build scripts; STRATA_SELECT_GEMM_MIN sets the block threshold of the #3248 GEMM block scores
Controls of glorysdj's pull request #4185: unset means on for a Battlemage device and off for every other device (strata::bm_default_on); a set value still decides. Measured results are in phaseA-tests/pr-priorities/RESULTS-INTEL.md.
2026-11-01 00:50:05 +01:00
Niko1221 3277bfcd71 Metal: opt-in fp16 GEMVs and Q2_0 experts in decode windows (STRATA_METAL_F16=1), from PR 4807 by rasa04
Unset, the output is unchanged (the build with the change, unset, gives the same greedy ids as the build without it in 12 of 12 runs, gemv_parity / gr_parity / expert_parity pass). With STRATA_METAL_F16=1 the decode windows of a resident model use half-precision GEMVs (Q2_0, Q3_K-Q6_K, IQ4_XS) and the Q2_0 routed experts.
M5 Max, spec 3 + min-p 0.5, 3 interleaved pairs, decode tok/s unset -> on: Q2_0 story 105.0 -> 114.9 (+9.4%), code 125.6 -> 143.7 (+15.5%), json 130.8 -> 135.1 (+3.3%, one noisy pair), agent 116.1 -> 131.6 (+13.4%); IQ3_S (dense GEMVs only) story 93.1 -> 104.3 (+8.8%), code 103.6 -> 118.5 (+12.5%), json 122.5 -> 135.4 (+10.6%), agent 103.8 -> 117.8 (+12.3%).
It changes arithmetic: teacher-forced over 4,995 positions against the same build unset, Q2_0 top-1 97.12%, mean KL 0.0094 (p99 0.119), perplexity 4.456 against 4.442; IQ3_S top-1 96.16%, mean KL 0.0160 (p99 0.211), perplexity 4.119 against 4.135. The half range is not guarded (see the PR). Off by default.
2026-11-01 00:39:53 +01:00
Niko1221 7737bca3eb Prompt path: one start-up line says whether the QSA norm + rope and attention-gate fusions are on 2026-11-01 00:34:24 +01:00
Niko1221 27de07ceff App: one card tells a person with an older engine that a newer one is available, with one button (Update engine); #4848
After an app or Control update the engine is never touched, and the app said nothing. Strata Control keeps what the last update or check found newer
(update_available of 'strata components engine', Qwen engine and DeepSeek engine; read without the network); the host passes it to the app as
engine.updateAvailable and the Models screen shows one card in plain words with the existing Update engine job as its button (disabled while a job
runs or a model is loaded). A DeepSeek engine update is reported as an engine update, not as 'Strata is updated'. JavaScript only; the Control side is
on f-0291-keepcurrent.
2026-11-01 00:34:13 +01:00
Niko1221 3304347bde dsv41 (gfx11 dequantizer): dq_old_ok is __host__ __device__, so the CUDA build can call it from dq_rows_kernel
c9e0d6688 uses it in a __global__ function; nvcc refuses a constexpr host function there (error: calling a constexpr __host__ function from a __global__ function is not allowed). HIP accepted it, CUDA did not: the f-042-maya-dq line did not build for NVIDIA.
2026-11-01 00:32:39 +01:00
Niko1221 9d58393dbd dsv41 (gfx11): the dense dequantizer defaults to dq_rows_kernel2 on every type; the old row-per-lane kernel is DS4_DQ_OLD=1 only, and only for types its staging serves
The old kernel (the gfx11 default since 0.2.7, picked for speed) gave different dense weights on every run on a Strix Halo: layer 0 of a
200-token prompt differed by 27% between two runs of the same engine on the 2.25 pack (Q5_K) and by 26% on UD-IQ1_S, while dq_rows_kernel2
is bit-identical run to run and 1.2-1.3% from the window path (GEMV, no dequantizer).  A gfx11 device now defaults to kernel2 whatever
DS4_DEFAULTS says; DS4_DQ_OLD=1 forces the old kernel there and elsewhere, but only for a type whose staging fits (Q4_K, Q5_K, Q2_K:
dq_old_ok); Q6_K (840 bytes against the 704-byte buffer) and the rest always use kernel2.  2.25 prompt speed on the Strix Halo: 4K 166.1 -> 164.9
tok/s, 8K 297.9 -> 294.2 (3 runs each, -0.7% / -1.2%).  UD-IQ1_S's long prompts and agent loop work.
2026-11-01 00:22:11 +01:00
Niko1221 db1bfaaae1 mtp: the draft layer's fused q8_1 images follow STRATA_QFUSE_HC and gfx12 only, not STRATA_QFUSE (gfx1151 keeps what it ran) 2026-10-31 23:58:11 +01:00
Niko1221 606f6272dc serve: STRATA_MTP_ROOM_MIN, a long prompt takes the MTP drafter's 512 decode-only experts as ring (opt-in)
Port of PR 3307 by gopinath87607 (rebased onto f-0291-int-trial, three commits squashed; the mtp.cpp signatures keep the
gated-proposal DraftResult argument and the prompt-helper arming keeps the failed-allocation cudaGetLastError).

STRATA_MTP_ROOM_MIN=TOKENS (--serve with --mtp): a request that reads at least TOKENS prompt tokens points the prompt
path's ring at the drafter's 512 routed experts while it is read. The block is lent in place: never freed, never allocated
again, never moves, so no prompt path is laid out again and the decode's captured graphs stay valid; the decode's own bytes
come back (a DMA from a pinned copy) before the first window. Unset, nothing of it runs.
2026-10-31 23:51:16 +01:00
Niko1221 f7bd10209f ds4_route_guard_test: the non-finite flag 2026-10-31 23:42:48 +01:00
Niko1221 97b3d614fc hc_q8_stream_test: spin on the clock where __nanosleep does not exist (sm_60 / sm_61), so the test target builds with tests on for Pascal
Follow-up to the port of pull request 4715 by cedrik-fuoco: the test failed to compile on sm_60 (identifier __nanosleep is undefined).
2026-10-31 23:42:10 +01:00
Niko1221 c940106485 prefill: say when STRATA_PF_GROUP_SCRATCH / STRATA_PF_HC_REGION took effect, and why GROUP_SCRATCH is ignored where the layout is not all MMQ
Follow-up to the port of PR 4039 by merbanan. The switches stay opt-in and the prompt path is unchanged when they are off;
the lines are the activation evidence the test plan asks for (chunk, rows held).
2026-10-31 23:41:31 +01:00
Niko1221 03627326dd serve: --lookup-max-t N, prompt-lookup windows up to 16 tokens while the MTP keeps --spec (opt-in)
Port of PR 4040 by merbanan (rebased onto f-0291-int-trial, bitwise by the author's checks).

--lookup-max-t N (up to 16) lifts only the prompt-lookup windows to N tokens; the MTP drafts keep --spec. A window
past kVerifyMaxT tokens runs as two token groups of at most kVerifyMaxT (the --spec-split layout), the head runs in
column chunks, the batched GDN commit takes up to 16 kept rows, MtpDrafter::draft_long catches the drafter up past its
window, and every lookup size is its own window graph (--lookup-sizes A,B picks them; one that finds no VRAM is dropped).
A build whose windows already hold 16 rows (STRATA_WINDOW_MAX_T=16, the HIP default) runs the 16 as one group.
Batch, pipelined and layer-split windows stay at kVerifyMaxT. Without the flag nothing changes.

Port notes: kVerifyLongT = 16 for both window widths, the per-window-size graph arrays keep the trunk's
[sel_long_][T] layout, the head chunk loop keeps fused_gr_read_multi's own slicing, the penalty history is sized for
the widest window only when --lookup-max-t is past 8, and draft_long forwards the gated-proposal view.
2026-10-31 23:38:42 +01:00
Niko1221 3b3f3b6bd0 prefill: STRATA_PREFILL_CPU_SHARE_BATCH=1 (opt-in) gives the prompt path the CPU pool beside batch slots, for a read with no slot decoding
Port of pull request 3382 by Vop3p (commit 2b65710f9 on v0.2.0-beta), rebased by hand on the 0.2.9 trial tree.
Conflict in generate.cpp: the share gate goes first in run(), before the co-read branch, and the kva_floors / set_cell_floor bookkeeping after the read is
kept as it is. Addition to the original: a read run on its own thread beside decoding slots (STRATA_BATCH_CONCURRENT_READ=1, run_conc) keeps the share
blocked for the whole read and does not touch the slots' state from the reader thread. Off unless the variable is set.
2026-10-31 23:36:21 +01:00
Niko1221 1f8ca8de1e RDNA4 routed-expert, draft, shared-expert and head kernels (PR 3259 and PR 3264, author bsorensen110)
Ports the two RDNA4 pull requests onto this tree by hand (three-way merge of the rejected hunks).  Where the contributor made a
default depend on a device query, the default is the gfx12 table or the env variable instead, so STRATA_GFX12_DEFAULTS=0 still
gives the old behaviour; the LDS-only barriers and the S2 and head changes that were only run on gfx1201 apply to gfx12 only,
every other target keeps what it ran before.

- iq_kernels.cu (3259): LDS-only barriers in the grid / activation staging, first-pass barrier skipped, vectorised ed_stage_entry,
  expdec_gu_kernel launch bounds in waves per EU on HIP, EXPDEC down read at first use; expdec_bench builds on HIP.
- s2_expert_grouped.cu (3264): single-entry gu/down specialisation for the draft layer, fused swiglu + q8_0 on the hit paths,
  LDS-only barrier on gfx12.
- verify_kernels.cu, quantize_act.cu, elementwise.cu, shared_expert.cu, fused_gr.cu, mtp.cpp, verify.cpp (3264): multi-block argmax
  and top-prob on gfx12, warp-per-block quantize_q8_0_scaled, mapped copies in one launch on HIP, shared expert reuses the q8_1
  image, counter-free q8_mixed in the hyper-connection read, the draft layer and the head write the q8_1 image themselves.
- parity tests of both pull requests (verify_parity on HIP, s2/shared/gr parity cases).
2026-10-31 23:35:50 +01:00
Niko1221 15165e170b DFlash2: opt-in Q4_0 drafter pack (dtype q4_0, tools/dflash2_pack.py --q4_0): nibble-packed tiled weights, gemm4_kernel on the existing dp4a activations path, df2_q4_tile at load, CPU parity test dflash2_q4_parity (40 of 40 cases within 2.4e-6 of 19) 2026-10-31 23:33:22 +01:00
Niko1221 55ae7814d8 HIP SIMT2 selector (PR 3073, author Vop3p) 2026-10-31 23:33:11 +01:00
Niko1221 1db96cda5f serve --draft auto: --draft-rule learn picks per kind of request (first prompt tokens) the drafter with the better measured decode speed, with exploring 2026-10-31 23:32:24 +01:00
Niko1221 3eb91676df AMD community tuning: gfx1010 SDWA dot4 and two gfx1030 rocBLAS tables under one switch
Ports three community changes that we have no card to run (no gfx1010, no gfx1030 here), on by default under the one
setting STRATA_AMD_COMMUNITY_TUNING (=0 turns the tables off; cmake -DSTRATA_AMD_COMMUNITY_TUNING=OFF compiles the portable
dot for gfx1010 again).  Each is limited to the card or exact library build it was made for.

- gfx1010 (RX 5700 / 5700 XT) signed dot4 takes the SDWA byte-select sequence gfx1012 already used: PR 4855, author Keoian.
  Same arithmetic, four signed byte products accumulated modulo 2^32.
- rocBLAS table gfx1030 / 5.4.0.ad84daa81c (TheRock gfx103X-all 7.13 wheels): PR 3343, author elightcap.
- rocBLAS table gfx1030 / 5.2.0.dabb6df2b98-dirty (Arch Linux ROCm 7.2.4): PR 3924, author schmittcn.

include/strata/core/amd_community.hpp holds the switch, the list of community tables and the engine log line (printed only
on gfx1010 and gfx1030); gemm.cu refuses a community table with the switch off, the existing loader still refuses any table
whose architecture or full rocBLAS version string differs.  amd_community_test checks that the switch turns each one off.
The contributors' numbers are labelled as theirs in docs/AMD_HIP.md and have not been reproduced.
2026-10-31 23:30:43 +01:00
Niko1221 7a498a05b4 serve --draft auto: the idle drafter's upkeep is queued on its own stream and waited for before the next window (DFlash2 context_async / context_wait, MTP prefill without sync) 2026-10-31 23:29:51 +01:00
Niko1221 3cc47f0297 prefill: STRATA_PF_HC_REGION=1, the hyper-connection read's scratch in the shared region (opt-in, bitwise)
Port of PR 4039 by merbanan (rebased onto f-0291-int-trial; same bits).

The hyper-connection read's scratch (xn16, lo, lo16, gated and the BF16x2 low halves) is taken as a fourth set of the
region the attention half and the MoE half share: a half's read is done with it before that half's set is written, and
the fused write at a half's end (gr_write_norm_rs: the next half's xn16) runs after the half's last region reader on the
same stream.  The same kernels on the same values: the same bits, ~63 KB a token less outside the region, so --prefill
auto finds a bigger chunk.  Off by default, and off with the unfused hyper-connection path.

bytes_needed also counts STRATA_PF_GROUP_SCRATCH's larger bounds table (bounds_ints), as carve takes it.
2026-10-31 23:29:01 +01:00
Niko1221 fbf6e1c753 prefill: STRATA_PF_GROUP_SCRATCH=1, the MMQ path's GU / H / Hq hold one expert group's rows (opt-in, bitwise)
Port of PR 4039 by merbanan (rebased onto f-0291-int-trial; the group-scratch path stays off for weight-stationary layers, whose launches write the whole chunk's rows into GU; the WS offsets follow bounds_ints).

A streamed prompt chunk sends every non-resident expert of every layer over PCIe once, so the chunk size sets
how often an expert crosses the link per token.  The chunk is capped by its buffers, which live in the cache
slots the prompt path borrows; on an 8 GB card (RTX 2060 SUPER, IQ3_XXS, 1453 slots) a 4096-token chunk took
1198 of the 1325 lendable slots, and --prefill 8192 fell back to 4096.

The MMQ path's gate/up output, its SwiGLU and its q8_1 copy were sized for the whole chunk (T*K rows, ~84 KB
a token), but a group's products read them and are done before the next group starts.  With the switch they
hold one group's rows: gate/up writes its absolute rows at GU - ra, SwiGLU / quantize / down read them from 0.
The cap is T (one expert's most: a token routes to an expert once) plus three times a group's average; a
group that passes it runs as sub-ranges of its experts, each with its own relative bounds appended to the
bounds table.  Only on a layout whose every layer takes MMQ (no fused experts, no FP16 fallback); a layer
that would take another path stops the prompt with a message instead of overrunning the smaller buffers.
STRATA_PF_GROUP_CAP=<rows> forces a smaller cap (test knob).

Measured (RTX 2060 SUPER, Ryzen 9 3900X, Qwen3.8-Flash-Next GSQ-RCO IQ3_XXS, resident RAM, k8v4 + KV
streaming; v11 at --prefill 4096 vs this at 5120, 3 pairs, 3 requests per load):
- --prefill auto: 4096 tokens (1197 slots) -> 5120 tokens (1219 slots); 6144 and 8192 still do not fit
- prefill 4K 445.4 -> 449.3, 16K 456.8 -> 493.5 (+8.1%), 32K 456.2 -> 504.5 (+10.6%); decode +0.1 / +0.4 / -0.9
- first-token logits after a 15.6K-token prompt at the same chunk size: bitwise equal to the full buffers;
  with STRATA_PF_GROUP_CAP forcing sub-ranges on most groups KL 0.001 (MMQ's stream-k order per launch)
2026-10-31 23:29:01 +01:00
Niko1221 5a41f3b391 SYCL: #4185 opt-in mixed-IQ3 decode optimizations on Battlemage (from glorysdj's PR #4185), ported onto this tree, not yet measured
Ported from glorysdj's draft pull request #4185 (commits 961f62963 and 0726ef5ad of that branch; the documentation commit is not taken).
A compile option STRATA_SYCL_IQ_DECODE_FAST (default OFF: packed Q2 expansion, IQ4 bit-select lookup, fast IQ3_S sign arithmetic) and fourteen runtime controls, all default off:
STRATA_IQ3S_GU_SLM, STRATA_IQ4NL_DOWN_SLM, STRATA_RESIDENT_PLAN_COMPACT, STRATA_IQ2S_GU_ROW_REUSE, STRATA_IQ3XXS_GU_ROW_REUSE, STRATA_GR_CACHED_NORM, STRATA_GR_LO_REUSE, STRATA_MMVQ_SHAPE_RPW, STRATA_GDN_QK_REG, STRATA_EXPERT_ROW_E3, STRATA_Q6_SHAPE_GRF, STRATA_IQ3S_GU_ROW_REUSE, STRATA_GR_REDUCE_ILP, STRATA_ATTN_MERGE_ACTIVE.
Conflicts resolved against this tree: the kprof launch wrappers kept (new launches in the conflicted hunks use strata::kprof::kp), the resident-plan launch keeps the kprof wrapper with the compact thread count, native_expert_bench.cpp keeps this tree's fixed bench (the PR's older version of the same hunks is dropped), qsa_prompt_attn_parity.cpp keeps the per-cell check and adds the PR's FP32HASH line and QA_FP32_GRAPH timing.
Also in the diff: docs/INTEL.md section from the PR.
2026-10-31 23:26:20 +01:00
Niko1221 b3ff140d68 Prefill: the fused ring's layer check is cached per CUDA device, so a layer split over unlike cards keeps MMQ's buffers where it must
Port of pull request 4380 by moses-sun (commit 5d5e380c4 on v0.2.7), applied without conflict. Refs issue 3577. Homogeneous rigs answer what the single cached bool used to;
not run on a heterogeneous split (no such box here).
2026-10-31 23:26:18 +01:00
Niko1221 0922b91b64 prefill: layer-major walk expands the hyper-connection projections on the path's stream; test that hc_q8_to_bf16 honours the stream it is given
Port of pull request 4715 by cedrik-fuoco (commits bd1410701 and d5a0d8294 on 0.2.7.1). The three bf16_proj calls of the layer-major walk (now in Prefill::lm_run)
omitted the stream argument; under STRATA_HC_REQ8=1 the int8 hyper-connection codes were expanded on the default stream while the GEMMs read them on another.
2026-10-31 23:26:06 +01:00
Niko1221 1b1a28e77e prefill: STRATA_PREFILL_CPU_SHARE_BATCH=1 (opt-in) gives the prompt path the CPU pool beside batch slots, for a read with no slot decoding
Port of pull request 3382 by Vop3p (commit 2b65710f9 on v0.2.0-beta), rebased by hand on the 0.2.9 trial tree.
Conflict in generate.cpp: the share gate goes first in run(), before the co-read branch, and the kva_floors / set_cell_floor bookkeeping after the read is
kept as it is. Addition to the original: a read run on its own thread beside decoding slots (STRATA_BATCH_CONCURRENT_READ=1, run_conc) keeps the share
blocked for the whole read and does not touch the slots' state from the reader thread. Off unless the variable is set.
2026-10-31 23:25:45 +01:00
Niko1221 5273fb667d prefill: STRATA_PREFILL_ROUTED_STREAM=1 (opt-in) does not copy the streamed experts no token of the chunk routes to; test
Port of pull request 4283 by bitaccesscomau (two commits 3956b94a7 and fcb0e399c on v0.2.8), applied without conflict on the 0.2.9 trial tree.
Off unless the variable is set; STRATA_PREFILL_ROUTED_STREAM_DEPTH sets the copies queued ahead (default 16).
2026-10-31 23:25:11 +01:00
Niko1221 d34c9a31ef SYCL: #3248 prompt kernels (from maxfridbe's PR #3248): per-cell prompt attention fallback, QSA block scores as oneMKL GEMM tiles
Ported from maxfridbe's pull request #3248 (commits e55a11c08 and 8cc654f23 of that branch).
- STRATA_ATTN_PERCELL (default on): qsa_prompt_attn_percell scores one work-item per cell in the prompt path's fallback attention (reached when the Battlemage ESIMD kernel refuses the pools, e.g. FP16 or Q4_0 KV, or STRATA_PROMPT_ATTN_ESIMD=0). =0 keeps the warp kernel. Not bitwise: FP32 in another summation order.
- STRATA_SELECT_GEMM (default: from 2048 blocks): QSA block scores as oneMKL FP32 GEMM tiles plus qsa_block_scores_reduce; =0 the warp kernel, =1 always. Not bitwise.
- qsa_prompt_attn_parity also checks the per-cell kernel and is registered with ctest.
Conflict resolution against the tree: qsa_select.dp.cpp keeps qsa_block_scores_rows beside the new reduce kernel; docs/INTEL.md keeps this tree's text (the PR's table is not copied; measurements are in the Intel notes).
2026-10-31 23:25:08 +01:00
Niko1221 8dcda377a0 SYCL: #3248 correctness fixes (from maxfridbe's PR #3248): host-only copies on the CPU, s2 grouped kernels without FMA contraction, gr_parity QFUSE skip, crossover cache dir
Ported from maxfridbe's pull request #3248 (commits d95a9c4b0, 5fbef34f9, 344e43e1d, c44fb63be of that branch), cherry-picked without the prompt-kernel commits.
- conversation_snapshot: a copy with neither end in device memory (pinned host <-> pageable host) is a memcpy on the CPU; through the queue it runs on the Arc's copy engine, which the PR reports hanging (Engine reset: engine_class=bcs).
- s2_expert_grouped: #pragma clang fp contract(off) in the two transposed grouped kernels (fixups.py 9d keeps it across re-migrations; 9c the snapshot test's sycl::free).
- gr_parity: the STRATA_QFUSE read check is skipped when the SYCL fused GR read writes no q8_1.
- strata-sycl.sh: STRATA_CACHE_DIR defaults to the mounted data root so the int8 crossover cache survives the --rm container.
2026-10-31 23:24:31 +01:00
Niko1221 d17c52e7d0 batch: STRATA_PW_WITH_BATCH=1 keeps --pipeline-windows for the solo path beside --batch slots
Port of PR 3095 by Vop3p (rebased onto f-0291-int-trial; the slot drafter load and bind
conflicts with STRATA_PIPED_MTP resolved by keeping both).

With --batch slots a request that is alone runs the solo path, and that path can keep
--pipeline-windows: both parities' verifiers share the stage's session, so copy_to_slot reads a
whole state after the pipeline drains. With --batch-mtp the slot drafters follow the solo drafter
to the first stage's card when mtp_first. Opt-in; off it is unchanged.
2026-10-31 23:23:52 +01:00
Niko1221 ce2098dcc9 dsv41: the dense-weights check refuses only when the free memory itself is smaller; between that and the reserve it warns
The previous commit stopped whenever the dense weights did not fit in free memory minus --reserve-mib. They would load in the margin, so that case is now a warning (the load goes on) and only need > free memory, where the cudaMalloc cannot succeed, ends the start.
2026-10-31 23:18:37 +01:00
Niko1221 dc1bc4c34d dsv41: the router tells the request when it met NaN scores
The router's stand-in pick (previous commit) keeps a NaN from becoming a memory fault, but the layer's output is garbage and the request would stream nonsense. route / route_publish now set a device flag when no pick came out; ds4_main polls it at the first token and every 8 (one small async copy and a stream wait, not a per-token sync), and the model says once in the log which device met it. The request ends with 'ERR the model's numbers went non-finite ...' and the conversation is dropped (reset), the same ERR-then-DONE shape as a failed prompt. The --parallel loop is not covered; the host-routed window path already errors on a non-finite weight.
2026-10-31 23:18:37 +01:00
Niko1221 94edf723ca DeepSeek engine updates move to the explicit update; a start only repairs a stale path (replaces the start-time feed check)
Starting never reaches the feed again (test_no_acquisition passes unchanged). What a start does now, locally: a saved profile whose DeepSeek engine
folder an update removed is pointed at the newest installed package of its own kind (strata_control/dsv41_paths.py, called from runtime.start), which
fixes the 'engine could not start: file not found' after a strata launch updated the engine beside the app.

The explicit update (strata update, the app's Update engine, updater.run_update with the engine) now also updates an installed DeepSeek engine
(updater.update_dsv41): the feed's newer package of the same kind is downloaded, checked (signature, every file), unpacked next to the old one,
started once (--version), and only then every saved run config is pointed at it; older folders go, except one a running engine uses. update --check
reports it; the check kept after any update or check also feeds engine_component.cached_offers / status()['update_available'], which the app reads
without the network to show one card. strata launch's own upgrade (dsv41_engine.py) is left as it was.

Tests: tests/terminal/test_dsv41_update_path.py (the old keep_current tests, adapted, plus a start that makes no request).
2026-10-31 23:15:10 +01:00
Niko1221 0dfa2d9b19 App: a model install that also updates a working engine says from which version to which, that the old one is kept and that Settings can go back to it (#4823) 2026-10-31 23:12:53 +01:00
Niko1221 2473978007 Server: on /v1/messages a context overflow starts with Anthropic's own wording (prompt is too long: N tokens > M maximum, or input length and max_tokens exceed context limit) and keeps Strata's sentence after it, so Claude Code compacts or shortens max_tokens instead of retrying the same request (#4838) 2026-10-31 23:12:06 +01:00
Niko1221 6bd2565594 App: a tuning that did not finish puts the tuner's own reason and what this PC has (memory, graphics card) behind Details, and says to open it, instead of 'could not start with these settings' alone (#4805) 2026-10-31 23:12:06 +01:00
Niko1221 1ab4c2ce46 serve: --draft auto keeps both drafters current; a request picks the MTP or DFlash2 (draft= key, --draft-rule), own window width and policy per drafter 2026-10-31 23:07:24 +01:00
Niko1221 d2068bde56 CPU prefill share on by default for a single gfx12 card (R9700 / RX 9070 / RX 9060): the CUDA change's logic (share_rules::default_share_limit, staged chunks up to 3,072 tokens, only where experts are missing, one card, no batch slots) now also arms on HIP when arch_defaults::gfx12_prefill_share_default says so; gfx11 (Strix Halo, 780M, RX 7000), other AMD families and multi-card AMD stay off until measured; STRATA_GFX12_DEFAULTS=0, STRATA_DEFAULTS=026 and STRATA_PREFILL_CPU_SHARE (0 / auto / a number) decide first; R9700 (p620, IQ3_S, 54% resident): prompt 500 +18.3%, 1K +30.6%, 2K -0.19%, 8K -0.18% with it set by hand, first-token KL 0.022 at 1K (29 prompts, top-1 29/29) 2026-10-31 23:07:11 +01:00
Niko1221 3410a85881 QSA prompt attention: the output gate in the matrix-core kernel's epilogue (STRATA_QSA_FUSE_GATE)
qsa_prompt_attn_batch_gated writes half(attn * sigmoid(gate)) straight from the epilogue of
prompt_attn_wmma_kernel, with the expressions of gate_attn_kernel (a * (1 / (1 + expf(-g))), round to
FP16), so the image the o-projection reads is the same bits and the FP32 attention buffer is neither
written nor read back (about 0.4 GB a QSA layer at 8K tokens). Used only where that kernel runs (HIP,
int8 KV, STRATA_HIP_WMMA) and the output is not rotated back (kv_rot / hybrid K8V4 keep gate_attn);
anything else, and STRATA_QSA_FUSE_GATE=0, runs the two-step path. Both prompt-layer loops use it.

Exactness is checked end to end (first-token logits and greedy ids with the switch on and off); there is no
kernel-level test for it yet. Idea from halo-box/strix-llama.cpp PR 213 (attention gate fused into the QSA
prefill epilogue, MIT, The ggml authors); the code is ours.
2026-10-31 23:06:54 +01:00
Niko1221 de83080059 CPU prefill share: the default limit goes from 1,024 to 3,072 tokens (CUDA, one GPU, only where experts are missing): TTFT of a 1-2K prompt on a card holding half the experts drops 30-60%, P100 +25% at 1K and +13% at 2K, 4K and above unchanged; share_rules::default_share_limit keeps a class gate, STRATA_PREFILL_CPU_SHARE_MAX=1024 restores the old limit, unit test added 2026-10-31 23:05:20 +01:00
Niko1221 0a02ee4108 Launch: a runtime host left in failed or stale is cleared before the tuning and before the start, the 'profile already has a live runtime' message names launch --stop and --retune, and --save-preset is kept when the start fails after the tuning (#4824) 2026-10-31 23:03:01 +01:00
Niko1221 f6eed469e7 dsv41: a non-finite router input no longer reads outside the expert tables
When every score left is NaN (the gate's input went non-finite), nothing beats -FLT_MAX in route_kernel / route_publish_kernel, the pick stays at its 0x7fffffff start, and the next lines read sc[0x7fffffff] (shared memory), vram[0x7fffffff] and, in the prompt path, vs[0x7fffffff] in moe_sort_kernel: a memory fault in a kernel far from the cause. The thread that picks now takes the lowest expert not picked yet when no pick came out (one compare a pick on the finite path); the weights stay NaN as before. The host-routed window path, which has the picks on the host anyway, ends the request with a clear error on a non-finite weight.

tests/dsv41/ds4_route_guard_test.cu (target ds4_route_guard_test, needs a GPU, no model): valid and distinct picks for NaN / inf / partly NaN logits, the host's top-k when finite, and launch times (DS4_ROUTE_TIMING=1).
2026-10-31 22:57:45 +01:00
Niko1221 8b1cae731c Engine install: a checked engine that Windows will not let Strata rename into place is copied instead after a 40 s wait, and the first-run card says why it stopped (Details, plain next step, Continue named) instead of the generic sentence (#4853) 2026-10-31 22:57:23 +01:00
Niko1221 3949737f1a Trial fix (belongs to f-0291-bw-dense): fused_gr.cu ldg_v4 PTX asm and gr_v3 --resources used cuda-only names on HIP; plain load / no-op on HIP 2026-10-31 22:56:06 +01:00
Niko1221 14a85eca91 QSA prompt layers: K, query and indexer-query norm + rotary in one pass each (STRATA_QSA_FUSE_NR)
rms_rope_rows replaces split_q + rms_rows + rope for the query and rms_rows + rope for K and the indexer query.
It uses the same block_sum, the same (s * x) * w product and rope_kernel's own angle and rotation expressions, so
the outputs are the same bits; the traffic drops from about 1.1 GB to 0.4 GB a QSA layer at 8K tokens. On by
default, STRATA_QSA_FUSE_NR=0 runs the old chain.

hip_qsa_nr_parity compares every output element bit for bit against the old chain (T 1..8191, four start
positions, NaN-poisoned outputs); hip_pf_cover_test poisons every output of the gfx11 prompt GEMMs (wmma_gemm.cu)
and checks that each element is written and that sampled rows match an FP64 reference. Neither has run on a GPU yet.

Idea from halo-box/strix-llama.cpp PR 213 (fused RMS norm + RoPE, MIT, The ggml authors); the code is ours.
2026-10-31 22:52:23 +01:00
Niko1221 d4d0c30da5 DeepSeek engine: the app's start keeps the engine current (not for the 0.2.9.1 hotfix; for the next normal release)
Before runtime start, a saved profile that runs strata-dsv41 from an engine-dsv41 folder is pointed at the newest installed package of its kind
(every saved profile's run config, not only the one started), and the signed feed is asked for a newer package at most once in six hours (half an hour
after a failed check). The package is fetched by a separate background process the start waits for at most 90 seconds; after that the start goes on
with the installed engine and the fetch ends in the background. Nothing in it can fail a start. A package folder a running engine uses is never removed
(also in upgrade() and install(), so a strata launch beside the app is safe), and two fetches never run at once.

Known: tests/terminal/test_no_acquisition.py::test_the_deepseek_engine_check_is_reached_only_from_launch_and_setup fails by design, because a start now
reaches the feed (owner rule: starting never acquires). It needs the owner's decision before this branch merges.
2026-10-31 22:51:27 +01:00
Niko1221 e6d81d7a2a SYCL: dq_dense_parity, the dense dequantizers and mmvq against the CPU dequantizers with random blocks 2026-10-31 22:50:06 +01:00
Niko1221 93b35ee84c dsv41: a GPU too small for its layers' dense weights fails at the start with what it needs, instead of a cudaMalloc error part way into the load
Port of Project Maya 8b56730ad (MIT): the dense weights (attention, shared experts, head) always stay in VRAM, so a device whose free memory cannot hold its whole layers' dense weights is told so before anything loads. Devices with tensor-parallel members are not checked.
2026-10-31 22:47:00 +01:00
Niko1221 cb49530dd6 DeepSeek: a model-free test of the dense dequantizers, GEMVs and chunk GEMM for every dense weight type (ds4_dq_test)
Random legal blocks against a CPU reference: iq::dequant_rows through both dequantizers (DS4_DQ_OLD=0 / 1 / the default), iq::mv with 1..16 columns, f16_rows, ds4_gguf.cu's rows and mv on Q8_0 / BF16 / ggml types, the library GEMM and the WMMA kernel after the dequantizer, and a repeat path that runs one call many times. Shapes include k % 1024 == 0, which ds4_iq_test never ran.
2026-10-31 22:44:22 +01:00
Niko1221 c60833cc38 dsv41: a run of chunks drains the recent slots' fills whenever recent slots exist, not only while the one-GPU path is on
one_gpu_plan may turn ln_one / ln_land off after the lane timing, but recent_n and the slots' fills stay (recent_n is set at the load, before the plan). borrow_begin then skipped rc_drain: a fill done but not yet mapped, or still writing, met the run's buffers in the tail, and the tick after the hand-back mapped its expert over what the run wrote (NaN routing, then an illegal memory access on a long prompt after shorter ones on one GPU). rc_in_tail() (recent_n > 0 and the fill stream exists) now also triggers rc_drain.

Port of the idea of Project Maya v1.1.45 (c968b9486, MIT): our rc_drain has no for_lend argument and this tree has no x3 / copy_flush, so only the condition is taken.
2026-10-31 22:32:46 +01:00
Niko1221 711bba9cc9 Version 0.1.55.1 for the DeepSeek engine hotfix (the CMake project version; the Terminal, the app and Control keep their 0.2.9 / 0.1.55 packages) 2026-10-31 22:32:31 +01:00
Niko1221 8422d864c8 dsv41 (gfx11): the old dense dequantizer stages only what fits its buffer, so a Q6_K dense matrix no longer overflows into the next warp's
dq_rows_kernel, the default dense dequantizer on gfx11 (Strix Halo), stages a warp's 32 sub-blocks in a 44-uint4 (704 byte) shared buffer.
Four Q6_K blocks are 840 bytes, so the staging ran into the next warp's buffer and the dense rows came out wrong: NaN in layer 0 of every
chunked prompt (96 tokens and more) of a UD-IQ1_S or UD-IQ1_M set, whose attention is Q6_K. Q3_K (440 bytes, not a multiple of 16) read 8
bytes past what was staged. Q5_K (704 bytes, the ISTA-DAS sets' dense type) fits and keeps the staged path. Stage only when the span fits
and is a multiple of 16 bytes.

(from d6be7ff6a on f-042-strix-ds)
2026-10-31 22:32:31 +01:00
Niko1221 beae52dfc5 MMVF check: build fixes (volatile loads as two floats, format string) 2026-10-31 22:16:15 +01:00
Niko1221 0ceb8ba68e Trial fix (belongs to f-0291-ui): markdown-images and markdown-soft-breaks stub './ui' without Icon, which CopyBtn (#4748) now imports (reliability.test.mjs already has it) 2026-10-31 22:13:29 +01:00
Niko1221 007fbd1ed2 Trial fix (belongs to f-0291-bw-dense): fused_gr.cu ldg_v4 PTX asm and gr_v3 --resources used cuda-only names on HIP; plain load / no-op on HIP 2026-10-31 22:11:27 +01:00
Niko1221 0f31e635a3 dsv41 (gfx11): the old dense dequantizer stages only what fits its buffer, so a Q6_K dense matrix no longer overflows into the next warp's
dq_rows_kernel (the default on gfx11, e.g. Strix Halo) stages a warp's 32 sub-blocks in a 44-uint4 (704 byte) shared buffer. Four Q6_K
blocks are 840 bytes, so the staging ran into the next warp's buffer and the dense rows came out wrong (NaN in layer 0 of every chunked
prompt of an Unsloth UD-IQ1_S/UD-IQ1_M set, whose attention is all Q6_K; windows of 16 use the GEMV and were fine). Q5_K (704 bytes, the
IST-DAS sets' dense type) fits and keeps the staged path. Stage only when the span fits and is a multiple of 16 bytes.
2026-10-31 22:09:43 +01:00
Niko1221 8eb3b6a8c2 STRATA_MMVF_CHECK=1 diagnostic: the constant-trip router read also runs the plain loop with volatile loads and counts accumulators that differ 2026-10-31 22:02:29 +01:00
Niko1221 b35bbfc2ee Trial merge f-0291-ui (2c2893081) 2026-10-31 22:01:53 +01:00
Niko1221 eaae5a865c Trial merge f-0291-mac-q (5957d10f7) 2026-10-31 22:01:52 +01:00
Niko1221 1b2825712d Trial merge f-0291-amd-q (c0988d553): CMakeLists keeps gr_v3_screen and mmvf_unr_hash; native_bf16.cu and verify_kernels.cu take bw-dense's per-device mmvf_fast/gdn_ab_fast (SMALL_UNR default on for cc 12.0) over amd-q's default-off copy 2026-10-31 22:01:52 +01:00
Niko1221 d4d7f164e9 Trial merge f-042-strix-q (dbf6a6bb6) 2026-10-31 22:01:20 +01:00
Niko1221 76f12eb225 Trial merge f-0291-bw-dense (08b20b104) 2026-10-31 22:01:16 +01:00
Niko1221 656fcfae13 Trial merge f-0291-bw-measure (c4082ce8c) 2026-10-31 22:01:16 +01:00
Niko1221 566e1c7c10 Trial merge f-0291-qwen-par (b19b38aff) 2026-10-31 22:01:12 +01:00
Niko1221 7d94b1d5b9 Trial merge f-0291-gh-app (5c0259e20) 2026-10-31 22:01:09 +01:00
Niko1221 9567238476 Trial merge f-0291-gh-eng (31c3a16f4): NOTICE.md keeps the base's block (thread pool fence #618 already listed; ds4_pool.hpp identical) 2026-10-31 22:01:09 +01:00
Niko1221 0f2778ae22 Trial merge f-0291-fixes (dde320ffa): keep both _parallel_lines and _broken_nvidia_note in launch/cmd.py 2026-10-31 22:00:53 +01:00
Niko1221 2d47b3e96b Trial merge f-041-maya-par-offer (2784dfdcc) 2026-10-31 22:00:46 +01:00
Niko1221 2c2893081b App: a goal, a plan step and a /loop run without anyone there, so the agent's question is declined or skipped at once there and cannot stall them; normal chat still shows the question card (#4244) 2026-10-31 21:59:32 +01:00
Niko1221 c0988d5531 arch_defaults_test: the gfx12 table has twelve switches (SMALL_UNR, GR_Q32_G, GR_Q8_PFS are off under STRATA_DEFAULTS=026) 2026-10-31 21:51:04 +01:00
Niko1221 eeb21d0e4f serve: --draft-conf honored with --draft dflash2 (the window ends where the product of the selector's probabilities falls below P), as in the CLI loop 2026-10-31 21:45:10 +01:00
Niko1221 a9a2939fe9 App: the transcript stops following the newest token while the reader has words selected in it, so a selection is not pulled out from under the pointer while a reply is written; the way back to the newest message is offered (#3818, selection half) 2026-10-31 21:37:47 +01:00
Niko1221 5957d10f75 Mac config: --spec 3 --spec-min-p 0.5 for a resident Q2_0 pack too; IQ3_S numbers re-measured
M5 Max, MTP, 5 interleaved pairs, fixed harness (each run's spec, min-p and context read from the engine's own log), greedy ids identical in every run, over --spec 2:
Q2_0 story 104.4 -> 104.7 (+0.4%), code 114.4 -> 127.3 (+11.5%), JSON 124.6 -> 132.5 (+6.3%), agent turn 115.9 -> 117.9 (+1.9%, 3/5 pairs).
IQ3_S story 94.2 -> 93.3 (-0.9%), code 103.8 -> 106.1 (+2.5%), JSON 105.1 -> 122.6 (+16.7%), agent turn 102.1 -> 107.1 (+4.9%).
The earlier Q2_0 'flat' result and IQ3_S '+1.4..3.6%' came from a harness that gave the spec flags to the first pair of an arm only. Other sets keep --spec 2.
2026-10-31 21:35:23 +01:00
Niko1221 1405970281 App: the agent's own questions (ask_user_question) get a card in the style of the approval card, with options, the person's own words, one answer or a skip; the tool is offered again as the group Questions for you in Settings -> Tools (#4244) 2026-10-31 21:32:33 +01:00
Niko1221 2f4b422968 App: a turn or a selected passage can be quoted into the next message: quotes sit above the box, can be removed or jumped to, go to the model in one fixed form, and show apart from the reply in the transcript (#4785) 2026-10-31 21:23:23 +01:00
Niko1221 51f9e432b9 App: while an answer is being written, Enter can wait (as before), send the message after the answer, or interrupt it (the answer stops where it is and the message goes at once); the choice is in Settings and the waiting message is shown above the box (#4783) 2026-10-31 21:20:04 +01:00
Niko1221 7d8c2a965b App: each helper row in the Subagents panel takes a message (the Harness's own subagents/prompt: steer for a working helper, queue for a finished one) and says plainly when it was sent or refused (#4749) 2026-10-31 21:15:29 +01:00
Niko1221 a75409dc57 mmvf_unr_hash: also hashes gdn_ab_multi (STRATA_SMALL_UNR bit 1) 2026-10-31 21:14:03 +01:00
Niko1221 656ddfd050 docs/MAC.md: --parallel works on Metal; the numbers measured on an M5 Max with IQ3_S 2026-10-31 21:10:12 +01:00
Niko1221 8218055ad3 App: Context memory says in one line what each format costs, and a start that ran out of graphics memory says so in plain words with the next step (#3723, context-memory and out-of-VRAM parts) 2026-10-31 21:06:01 +01:00
Niko1221 cb7491f02d App: a first start opens in the language of the computer, and the first screen carries a language choice (#3723, language part) 2026-10-31 21:05:57 +01:00
Niko1221 91411e17f8 Metal: 256 routed experts (the expert-pruned Coder, #4706): the routers, the prompt chain and the SSD stream take the model's expert count
Window::init accepts 512 or 256 experts a layer; the decode router (router_route / moe_route), the prompt router (route_topk) and the plan buffers use the model's count, the SSD stream reads it from the GGUF. The route kernels are templated on the logits a lane holds (16 for 512, 8 for 256); the 512 path is the old code token for token, the logits rows keep their 512-float stride, the predictor's unused columns are -inf. The MTP layer keeps its own 512 experts.
Tests: prompt_parity route_topk and fuse_parity router_route run both counts (all pass on the M5 Max). IQ3_S 512 experts: greedy ids identical to the build before in every class and prompt (3 interleaved pairs), speed equal (decode +/-0.5% median, prompts -0.5..+5.9%, inside the spread). Not run: an end-to-end 256-expert model (58 GB download).
2026-10-31 20:54:38 +01:00
Niko1221 cab56e7af8 App: paragraphs, lists and quotes in a reply share one width and tables and code blocks keep the full column; the list rule named a class the renderer never emits (#4722) 2026-10-31 20:53:49 +01:00
Niko1221 4473c386a5 App: while sharing is on, Serving shows the address on this computer and the one for other devices, with one plain line on why their ports differ (#4798) 2026-10-31 20:53:48 +01:00
Niko1221 de0608b443 App: the progress card of a download, load or tuning run has its own border and tint, a spinner and a running clock ("Still working for 3m 12s. You don't need to do anything."), and a line saying the buttons below wait until it finishes (#4802) 2026-10-31 20:53:48 +01:00
Niko1221 445baf12e4 App: a plain click on a web address in a reply opens it in the person's own browser (the app window asks the host, a browser tab opens it itself); files keep the side panel (#4721) 2026-10-31 20:53:48 +01:00
Niko1221 3cb03ed32c App: Tune this model says in one plain sentence why Start tuning is greyed out (still reading the saved tunings, could not read them, another operation running), and no longer says "No saved tuning" while it is still reading (#4804) 2026-10-31 20:53:48 +01:00
Niko1221 2506046a60 App: every Copy control shows on itself that the copy worked (a check mark for about two seconds, "Copied" in menus), and the seven that never reported anything now do (#4748) 2026-10-31 20:53:48 +01:00
Niko1221 9197e13a4b App: the Approve and Deny card stays above the message box while a long reply scrolls, and the reply says it is waiting for an answer, so nobody has to scroll up to find it (#4725) 2026-10-31 20:25:16 +01:00
Niko1221 2784dfdcc4 DeepSeek --parallel: the engine flag probe lives in its own module (strata_control/dsv41_flags.py), because serving must not import dsv41_engine.py (it reaches the network) 2026-10-31 20:13:11 +01:00
Niko1221 04e9da601f Mac: the PLE residency policy stays off unless STRATA_PLE_AUTO_RAM=1
Since the conversation-cache fix gave sample_host_memory a macOS branch, the 0.2.7 policy (table in RAM when it fits) started firing on the Mac: the 26.8 GiB n-gram table was locked in RAM, the footprint went from 8 to 35-42 GB and free memory dipped to 19 GB. M5 Max, IQ3_S, 3 interleaved pairs, same binary, same tokens: a cold 4K prompt after decode work is 10.6% slower with the table in RAM (-10.7 -10.6 -10.0% against the v0.2.9 binary, +2.4 +15.2 +12.2% for direct reads against RAM), decode is the same inside the spread. Direct reads are what the Mac had before; an explicit --ple-io still wins.
2026-10-31 20:11:35 +01:00
Niko1221 d8801f628c serve: say when a DeepSeek engine runs fewer conversations than --parallel asked for 2026-10-31 20:05:38 +01:00
Niko1221 09982fff66 gfx12 decode table: STRATA_SMALL_UNR=3, STRATA_GR_Q32_G=2, STRATA_GR_Q8_PFS=1 (bitwise load-ordering variants, measured on an R9700) 2026-10-31 20:03:04 +01:00
Niko1221 93af8275dc launch: DeepSeek Parallel step test; comments say DeepSeek is one at a time unless --parallel is asked for 2026-10-31 19:56:07 +01:00
Niko1221 009491d561 strata launch --parallel for DeepSeek: the launch prints what it does and the numbers, as the plan does; tests; app test fixture lists the setting 2026-10-31 19:50:10 +01:00
Niko1221 cc7857135b mmvf_unr_hash: hashes of the router-shaped bf16 reads for comparing STRATA_SMALL_UNR settings 2026-10-31 19:46:46 +01:00
Niko1221 87ec8fd83e WIP gfx12 HC q8 up read: software-pipelined group loads (STRATA_GR_Q32_PL) 2026-10-31 19:40:59 +01:00
Niko1221 d6912d7744 docs: DeepSeek --parallel (what it does, the numbers, where it was run); app test: Concurrent requests reaches a DeepSeek run config and nothing is added without it 2026-10-31 19:37:26 +01:00
Niko1221 31c3a16f4f Qwen3.6-35B-A3B: the penalties count the reply's tokens by default too; STRATA_PENALTY_PROMPT=1 brings the old window back (#4590)
The Qwen3.6 engine (src/qwen35/q36_engine.cu) started its penalty history from the whole prompt (`hist = ids`), so its model-card presence penalty 1.5 counted the prompt's last tokens as in the other engines. The history now starts at the last prompt token and grows with the tokens written (penalty_history_start in include/strata/qwen35/sampling.hpp), the same window as src/program/generate.cpp; batch slots copy that history. STRATA_PENALTY_PROMPT=1 keeps the whole prompt.

Tested: qwen35_sampling_test (276 checks, 9 new for the default and the opt-out), and a CUDA sm_86 build on z440. Replay on z440, one A4000, UD-Q6_K_XL, the reporter's conversation cut to 250 records, 400 tokens: 16 sampled seeds and 2 greedy requests per arm. This model did not return the old JSON in either arm (0/16 with the default, 0/16 with STRATA_PENALTY_PROMPT=1; 0/2 greedy in both), so this replay does not show a difference between the two windows; no answer in either arm started with the old JSON or a code fence.

docs/DETAILS.md: the sentence saying the Qwen3.6 path still counts the prompt is replaced.
2026-10-31 19:33:33 +01:00
Niko1221 094132ca77 strata launch --parallel N for DeepSeek: passed on instead of dropped, off by default; --plan says what it does with the measured numbers; a pick beyond what was run warns (Windows, Mac, small or unknown card); an engine from before the flag is asked (--help) and the launch goes on one at a time; the TUI step offers it for DeepSeek 2026-10-31 19:33:28 +01:00
Niko1221 8ce20293e5 WIP gfx12 HC q8 read: G rows a warp loaded together in gr_up_q8_q32_kernel (STRATA_GR_Q32_G), staging loads of all tokens together in gr_down_q8_fast_kernel (STRATA_GR_Q8_PFS) 2026-10-31 19:31:44 +01:00
Niko1221 c49eb5d194 WIP: single-column router bf16 read with constant-trip loads (STRATA_SMALL_UNR bit 0), for the HIP A/B 2026-10-31 19:29:54 +01:00
Niko1221 de4d21e6f8 WIP small kernels: STRATA_SMALL_UNR (bit 0 bf16 router read, bit 1 GDN a/b projection) constant-trip fast paths, default off
Same FMAs in the same order as the runtime-bounded loops (bitwise); nsys: gdn_ab 352 -> 290 us/round, router mmvf 533 -> 492 us/round. Engine A/B pending.
2026-10-31 19:29:03 +01:00
Niko1221 3ae0fbfad1 DeepSeek --parallel: a card that cannot hold every conversation's buffers runs as many as it can hold (the failed one's partial buffers freed, said in one line), instead of stopping the engine at the start 2026-10-31 19:25:10 +01:00
Niko1221 c4082ce8c4 CPU prefill share: the default limit goes from 1,024 to 3,072 tokens (CUDA, one GPU, only where experts are missing): TTFT of a 1-2K prompt on a card holding half the experts drops 30-60%, P100 +25% at 1K and +13% at 2K, 4K and above unchanged; share_rules::default_share_limit keeps a class gate, STRATA_PREFILL_CPU_SHARE_MAX=1024 restores the old limit, unit test added 2026-10-31 19:09:33 +01:00
Niko1221 40da04d49a Serve: DFlash2 on (ENGINE_SERVES_DFLASH2 = True); tests cover both states 2026-10-31 19:04:57 +01:00
Niko1221 6667ac43d4 wip: DFlash2 in the serve loop (ring stamps, request history, prompt rows, spec loop), serve test harness 2026-10-31 19:04:57 +01:00
Niko1221 081d1c04d2 PDL: on by default for the all-resident verify graph on a compute capability 12.0 card (bit-identical, +0.3..0.5% decode on an RTX PRO 5000); the doorbell graph and batch windows keep ordinary launches, STRATA_DF_PDL=0/1/2 and STRATA_SM120_PERF=0 are honoured 2026-10-31 18:45:16 +01:00
Niko1221 36d94496fa SYCL: the penalties count the reply's tokens by default too; STRATA_PENALTY_PROMPT=1 brings the old window back (#4590)
sycl/src/program/generate.cpp is its own copy of the serve loop, so the change in ed76b12f9 did not reach Intel cards. Same window: the fed-back last prompt token and the reply's tokens, built from the request's prompt ids on every request (a restored conversation gets the same window as a cold read). The helpers are added to sycl/include/strata/kernels/sampler.hpp, which that build includes first.

Measured on two Arc Pro B65 (SYCL AOT bmg-g31), Qwen3.8-Flash-Next IQ3_S, the reporter's 4-message conversation (32,951 tokens, cached), --spec 2, 16 sampled seeds, 500 tokens: old JSON 0/16 and numbered plan 16/16 with the new default; old JSON 8/16, plan 7/16, other 1/16 with STRATA_PENALTY_PROMPT=1. Greedy, 2 requests per arm: numbered plan 2/2 in both arms on this model.
2026-10-31 18:13:49 +01:00
Niko1221 06ebf2e4d6 PcieAuto: start the PCIe share from the pool size (min(65, 4 GB/s per worker) through the refinement's own formula) instead of the link-only 0.55, so the first answers after a launch do not run ~50 windows at a share the machine does not want; the converged share is unchanged, STRATA_PCIE_START=old keeps the old start 2026-10-31 18:10:43 +01:00
Niko1221 12a1f2c545 Mac config: --spec 3 --spec-min-p 0.5 for a resident IQ3_S pack (+1.4..3.6%, same tokens)
M5 Max, MTP, IQ3_S, 5 interleaved pairs (2 + 3), greedy tokens identical in every run: --spec 3 --spec-min-p 0.5 against --spec 2:
story 93.8 vs 92.4 tok/s (+1.4%), code 103.4 vs 100.4 (+3.0%), JSON 105.3 vs 101.6 (+3.6%), agent turn 100.8 vs 98.0 (+2.8%).
Q2_0 measured flat (-1.0% to 0.0%, 5 pairs; 800-token story -0.7%), so it keeps --spec 2; --spec 3/4 without min-p lose on both.
mac_fit.spec_args picks the flags by model; engine_args takes the model name (setup.py and metal/setup_mac.py pass it); a streaming
Mac keeps STREAM_SPEC, a Mac without the draft layer keeps --spec 2. The engine's own default (--spec 2) and an explicit --spec in a
config are unchanged. Test: tools/test_setup_mac.py.
2026-10-31 18:09:56 +01:00
Niko1221 08b20b104c BW-DENSE: STRATA_SMALL_UNR default 3 and STRATA_GR_V3_PF default 5 on compute capability 12.0 cards (bw_dense_defaults.hpp); STRATA_SM120_PERF=0 / STRATA_DEFAULTS=026 / the variables turn them off
Both are bitwise the loops they replace. Engine A/B: SMALL_UNR on top of the HC change +0.18% agent, +0.28% code_py, 5/5 pairs each, identical text.
2026-10-31 17:54:59 +01:00
Niko1221 381d70902d gr_v3_screen: print registers, local memory, smem and resident blocks of the v3 kernels (--resources) 2026-10-31 17:52:33 +01:00
Niko1221 ed76b12f9d The repeat, frequency and presence penalties count the tokens the reply has written, not the last tokens of the prompt; STRATA_PENALTY_PROMPT=1 brings the old window back (#4590)
With the model-card presence penalty 1.5 over the last 64 tokens, the prompt's own tokens (the digits of a new task, a quoted JSON answer) were penalised, and a conversation whose last turn asks for something new could get its previous answer back. The window now starts at the end of the prompt: the fed-back last prompt token and the reply's tokens. Earlier assistant turns inside the prompt are not counted. The history is built from the request's prompt ids on every request, so a conversation restored from the conversation cache or a saved slot gets the same window as a cold read. The STRATA_ADMIT_MERGE_FIRST merged first window is not used for a request with a penalty window, since its prompt rows would join the reply window.

Measured with the opt-in switch (618cd7a4b) on a P100, IQ2_XS, the reporter's saved 4-message conversation, 32 seeds per arm: old JSON returned 13/32 as shipped, 4/32 reply-only, 3/32 with presence 0. Qwen IQ3_S on an R9700, 16 seeds: old JSON 8/16 as shipped, 0/16 reply-only. 61-task quality027, greedy, one run per arm: 56 and 55 passed as shipped (two runs), 55 reply-only, 54 with presence 0.

Added penalty_window_test (header-only, CPU) and a line in docs/DETAILS.md.
2026-10-31 17:46:28 +01:00
Niko1221 2d381fcf51 WIP small kernels: STRATA_SMALL_UNR (bit 0 bf16 router read, bit 1 GDN a/b projection) constant-trip fast paths, default off
Same FMAs in the same order as the runtime-bounded loops (bitwise); nsys: gdn_ab 352 -> 290 us/round, router mmvf 533 -> 492 us/round. Engine A/B pending.
2026-10-31 17:26:08 +01:00
Niko1221 5c55046d72 Remove dense_tc (E03 rejected): the Q6_K INT8 tensor-core projection is 1.4-2.3x slower than the engine's MMVQ at T 1-4 on every real shape
Measured on an RTX PRO 5000 (cold weights, real shapes, head included); the earlier commits keep the code and its parity test.
2026-10-31 16:38:28 +01:00
Niko1221 97e5a6aeca dense_tc_parity: sync after pageable host-to-device copies (the T=7/8 mismatch was the test reading a column before its copy landed, not a kernel race) 2026-10-31 16:38:28 +01:00
Niko1221 adbe6b9b95 DeepSeek Windows build: ds4_os.hpp includes timeapi.h and links winmm for timeBeginPeriod (the merged include order no longer brought it in with windows.h) 2026-10-31 16:25:36 +01:00
Niko1221 e1b27d54b0 After a fatal GPU error the engine ends itself and the server starts it again, instead of staying up and failing every later request (#4350)
DeepSeek: an illegal memory access, launch failure or hardware exception in a prompt read or a decode round used to turn into an ERR/cancel while the engine kept running on a dead GPU context, so every later request failed with HTTP 400 until someone restarted it. strata-dsv41 now prints 'ERR fatal GPU error: ...' and exits (code 3). Qwen: a failed decode window, commit or final commit wait that leaves a sticky fatal runtime error does the same (the prompt-read path already did, #224), by the runtime's error text so CUDA, HIP and SYCL builds agree.
Server: that ERR line fails the request in flight as an engine death (503, retryable) and marks the engine ended, so the next request starts it again at once, with one plain log line and a plain death note. Restart-loop guard: a third fatal death within ten minutes stops the restarts and answers 503 with the reason until the window has passed; one death counts once.
Test hooks (off unless set): DS4_INJECT_FATAL=N / STRATA_INJECT_FATAL=N fail the Nth request / decode window with a real fatal error's text; *_ONCE=<file> fires it once across restarts.
Tested: serve/test_fatal_gpu_restart.py (7 tests); through the real server, request 2 injected, DeepSeek on boston (IQ1_M pack, 2x A6000): request 2 got a 503, the restart took 44 s, requests 1, 3, 4, 5, 6 answered 42; Qwen on a P100 (IQ2_XS), fault injected mid-decode in request 4: 503, restart, requests 1-3, 5, 6 answered 42. The guard counted '1 of 3' there and '2 of 3' in an earlier run where the injection fired on every restart. NOT tested: a real GPU fault (none reproduces), the Windows and Metal builds, the third-death refusal through a running server (unit-tested only), conversations coming back from saved slots after the restart (no check made).
2026-10-31 16:11:49 +01:00
Niko1221 3e2598a835 Dispatch counters: count the HC v3 read and the Q8 weights of the hyper-connection read, key a doorbell-graph launch as its own kind 2026-10-31 15:58:34 +01:00
Niko1221 3ce5193eb4 WIP HC v3: STRATA_GR_V3_PF default 5 (batched staging loads in gr_down, batched partial/row loads in gr_up) on compute capability 12.0 cards, 0 elsewhere; =0 restores the plain kernels
Bitwise the plain v3 read (gr_v3_screen, T 1-8, 0 differing); 4-8% faster per read on cold weights. Weight prefetch and 2/4 rows a warp measured slower and stay off. Engine A/B pending.
2026-10-31 15:55:17 +01:00
Niko1221 9469a0c264 Metal: token_chain CHAIN_GDNGRID timing probe (gdn_step grid size) 2026-10-31 15:39:20 +01:00
Niko1221 96a85f7fe8 Metal: token_chain probes and the measured-negative kernels of the hyper-connection / loop-batching study
hcf.metal (gr_norm folded into the producer of bo with a last-arrival threadgroup: bit-exact, +1.2 ms slower), gemvu.metal and
moeu.metal (unit loops unrolled x2: +2.3 / +1.5 ms), qsab.metal (batched qsa_attn: +0.07 ms). None is used by the engine; they
stay with their numbers (phaseA-tests/mac-q/RESULTS.md section 7). token_chain gets the CHAIN_* switches that measured them
(NORM2, COMB2, NONORM, NOOPS, SKIPK, HCF, KEEPNORM, GEMVU, MOEU, QB, DEFS).
2026-10-31 15:32:09 +01:00
Niko1221 21c14feb86 Metal: the drafter's prompt rows run on a second queue beside the next prompt chunk (+1.4-2.0%); mtp_rms with batched loads
Window::prompt ran each chunk's MTP-layer K/V rows (9% of an 8K prompt) after the chunk, with the GPU otherwise idle for them.
They now go to a second command queue (Context::run_graph_async / wait_async) while the next chunk's main work runs on the
first. The chunk's final residual rows are double-buffered (pb_.R / pb_.R2); the rows of chunk c+1 wait for chunk c's (shared
drafter cache and scratch); every way out of prompt() joins. Not used with M-RoPE (images). STRATA_METAL_MTP_OVERLAP=0
restores the sequential rows.

mtp_rms_b: mtp_rms with the row loaded up front (same sum, same products, same order); under STRATA_METAL_LDB=0 the original.

Engine, Q2_0, MTP spec 2, 5 interleaved pairs, same binary: p8k 1936 vs 1910 tok/s (+1.4%, 3/5), p32k 1747 vs 1711 (+2.0%, 4/5),
greedy ids identical in every run. The overlap is partial: the two queues share the GPU.
2026-10-31 15:32:09 +01:00
Niko1221 618cd7a4b6 Opt-in STRATA_PENALTY_PROMPT=0: the repeat, frequency and presence penalties count the tokens the reply has written, not the last tokens of the prompt (#4590)
With the model-card default presence penalty 1.5 over the last 64 tokens, the prompt's own tokens (digits of a new task, a quoted JSON) were penalised, and a conversation whose last turn asks for something new could get its previous answer back. Measured on a P100, IQ2_XS, the reporter's saved 4-message conversation, 32 seeds per arm, 400-500 tokens: as shipped 13/32 returned the old JSON and 16/32 a numbered plan; reply-only penalty 4/32 and 25/32; presence 0 3/32 and 26/32. quality027 --quick (19 tasks, greedy, one run each, no noise arm): shipped 15, reply-only 16, presence 0 17 passed; the 1-2 task differences are single tasks and inside run-to-run noise. Default unchanged (switch off). The old JSON still came back in about 1 of 10 in the best arms, so the penalty is a large part of the cause, not all of it.
2026-10-31 15:27:53 +01:00
Niko1221 9abe0d6c45 WIP HC v3 read: bit-exact load-ordering variants of gr_down_v3 / gr_up_v3 behind STRATA_GR_V3_PF (default 0 = the existing kernels), gr_v3_screen bench
bit 0-1 down (batched staging loads, weight prefetch), bits 2-3 up, bits 4-5 rows per warp 2/4 for T<=4. Screen bit-for-bit against the plain v3 read; speed not established yet.
2026-10-31 15:15:40 +01:00
Niko1221 da1339d547 WIP dense_tc: sm_120 INT8 tensor-core Q6_K projections (T 1-8), off unless STRATA_DENSE_TC=1; parity test against a scalar reference of its arithmetic contract
Rounding-changing path (not native_mmvq's order), so opt-in until a KL check and the owner's OK. HIP/other builds get stubs. Not benchmarked yet.
2026-10-31 15:15:40 +01:00
Niko1221 af6ba4c607 Dispatch counters: which kernel path a run really took (dense MMVQ by type/shape/rows, grouped experts and gate/up form, HC read form, window graph kind, layer paths, PDL edges, head), written as JSON at exit with STRATA_DISPATCH_REPORT 2026-10-31 15:03:07 +01:00
Niko1221 1b9a1b235a Metal: gr_norm and the MoE combine with their loads batched (decode +2-3%, same tokens)
gr_norm stored R in place inside its loop, so each of its ten iterations (and the ten of the second loop) waited a full
memory latency for the next load: 98 times a token, ~6 us of a 7.7 us dispatch. gr_norm2 loads R, bo and w_norm first, then
sums, reduces and stores; moe_combine_sh2 loads the ten plan entries, then the ten rows, then sums. The arithmetic and its
order are the originals', so the outputs are bitwise the same. STRATA_METAL_LDB=0 selects the original kernels.

token_chain (Q2_0, 1 token, graph): 10.65 -> 10.14 ms. Engine, MTP spec 2, 4 interleaved pairs: story 100.97 -> 103.90 tok/s
(+2.9%, 4/4), code 108.63 -> 110.84 (+2.0%, 4/4), greedy ids identical in every run.
2026-10-31 14:11:21 +01:00
Niko1221 dbf6a6bb62 Stall report: STRATA_STALL_STACKS=1 lists every thread's call stack (Windows), and the stall dump carries thread, handle and memory data
Off by default. With STRATA_STALL_STACKS=1 the stall report suspends each other thread in turn, unwinds it with the unwind tables
(no heap, no loader lock, no stdio) and prints the frames, named from the PDB when it lies beside strata.exe. The minidump flags now
include process thread data, handle data and memory info. Used to locate the Windows gfx1151 UD-Q4_K_XL stall (host thread inside
the HIP runtime after a graph launch).
2026-10-31 14:09:27 +01:00
Niko1221 5a13c34f1b PLE table lock: raise the soft memlock limit to the hard one first, and when the table still does not fit say how much this program may lock and what to set (#4671)
A systemd service without LimitMEMLOCK may lock 8 MiB, so the 26.8 GiB mlock of the PLE table was refused and the engine ran with a reclaimable table (the reporter measured up to 6% slower decode) behind a two-line message. Before mlock the engine now raises RLIMIT_MEMLOCK's soft value to the hard one (needs no privilege) and says so; if the table still does not fit, the message gives the allowed size and the remedy (LimitMEMLOCK=infinity, or ulimit -l unlimited). The residency policy itself is unchanged. Tested here: the CUDA engine builds (sm_60). The run under 8 MiB soft/hard, 8 MiB soft with a higher hard limit, and unlimited is queued, not yet seen.
2026-10-31 14:06:55 +01:00
Niko1221 b8c69334bc Metal probe: busy_flag_probe shows the DS4 busy-spin kernel does not see the host's flag write (#4771) 2026-10-31 13:34:26 +01:00
Niko1221 d01b181751 Metal: conversations park and restore (#4730, #4144)
The Mac now reads its free RAM for the conversation-cache guard (arena_avail's rule), the window's checkpoint uses the shared
contract (3 tail rows a layer, a scratch block_pos), and the session's QsaStates point at the window's K/V buffers (and the
drafter's), which the generic snapshot copies as cell-major byte prefixes. Before, the session's own pools were never written on
Metal. Two conversations taking turns (11K and 6.5K, 24K and 3K tokens): the second request of each re-read its prompt (6.3 s,
14.9 s); now it reuses it (46-65 ms), with the same answer ids.
2026-10-31 13:30:39 +01:00
Niko1221 b0b942fa9e A pack that keeps a native-eligible projection unquantized (BF16) no longer stops the engine with 'no native GGUF blocks': the matrix is served as Q8_0 like its siblings (#4741)
Heretic v2 IQ4_NL keeps blk.0.attn_gate.weight as BF16 (GGUF type 30) while the other 35 gates are Q8_0. NativeDense has no kernel for type 30 and skipped it, so the prompt path and the verify window, which read the gate through the native route, stopped with 'blk.0.attn_gate.weight has no native GGUF blocks (run with --native)' on the first request. NativeDense::load now makes such a matrix (BF16 in the file, BF16 row in the pack, 2-D, a multiple of 32 wide) into Q8_0 on the host (the same rounding the engine already uses for STRATA_HC_Q8) and binds it, so every dispatcher finds a native projection; weight_bytes_for counts it. The pack's BF16 copy stays. This is not bit-exact against reading the BF16 values (a Q8_0 rounding on one 2560x6144 matrix, the same format its siblings have); reading the BF16 directly would need changes in the prompt path, the verify window (with its side-stream branches) and gdn_layer. Tested here: the CUDA engine builds for sm_60 (p1004). NOT tested: any run, because the 109 GB Heretic IQ4_NL model is not on a test box; the reporter's own run is the first test.
2026-10-31 13:09:49 +01:00
Niko1221 b89bf5947d Windows: an engine started from a low-priority program no longer reads its model at Low disk priority; its disk priority goes back to Normal and one plain line says so (#4733)
A Windows process starts with the I/O priority of the program that started it. Run from a Task Scheduler task, a service, or a terminal that an IDE or coding agent keeps at low priority, the engine inherited Low I/O priority: the reporter measured PLE direct reads about 15x slower, cold prompt reads 3-5x slower and decode 14% slower on the same drive, and the engine blamed the drive. Both engines (strata and strata-dsv41) now read ProcessIoPriority at start and set it to Normal when it is lower (no privilege needed to go from Low to Normal). STRATA_KEEP_IO_PRIORITY=1 leaves it as inherited. The CPU priority class is not touched. New io_priority_test (Windows): started at Low it ends at Normal, started at Normal it is untouched, KEEP leaves Low alone. Tested here: that test passes on Windows; the HIP engine builds. Not tested: the reporter's slowdown itself (needs their drive and a low-priority launcher with an engine run).
2026-10-31 13:01:12 +01:00
Niko1221 5c0259e20c Server: the sampler's and the HIP probe's files are closed after reading, so the ResourceWarning lines of every run are gone; no answer changes (#4654) 2026-10-31 12:58:46 +01:00
Niko1221 c9efd49202 Docker: RETUNE=1 passes --retune to the launcher, so a tuned preset is measured again (#4779) 2026-10-31 12:58:46 +01:00
Niko1221 89b648a65e App: a helper row saved as In progress is shown as stopped when the page loads again, so a stopped helper no longer keeps its row and the running count after a restart; a live update brings back a helper that really runs (#4218) 2026-10-31 12:57:44 +01:00
Niko1221 6d8fbf4194 App agent: the summary that replaces older history is asked for in the language the person used (the Harness's instruction says concise English); paths, commands and identifiers stay exact (#4750) 2026-10-31 12:57:26 +01:00
Niko1221 c59f766a0a App: the Open button of a website preview hands the address to the default browser through the host (the app window drops window.open, so it did nothing); http and https only (#4721) 2026-10-31 12:55:07 +01:00
Niko1221 abd5c8cb8a App serving log: an Anthropic stream's prompt count (message_start) is read too, so tokensIn is no longer left empty while tokensOut is filled (#4678) 2026-10-31 12:54:19 +01:00
Niko1221 788e372792 Own model files: Strata's draft layer (mtp-q2_0.gguf) beside a model is no longer counted as a second model, so the folder is not refused with 'holds more than one model' (#4693) 2026-10-31 12:53:10 +01:00
Niko1221 ecb2fc4502 ds4 thread pool: fence the generation store before reading the sleepers, and start workers from generation 0 (a possible part of #4664, not shown to be its cause)
A worker going to sleep at the moment a batch started could miss both the new generation and the wake-up (the caller then runs the batch alone, slower, not wrong). Taken from Project Maya v1.1.36 (#618, Cody Harrison), MIT, credited in third_party/project-maya/NOTICE.md.
2026-10-31 12:52:56 +01:00
Tycho Rasch f6e3042a61 Windows HIP: keep the pinned expert arena's GPU aliases clear of the process's memory, so one start in ten no longer reads every long prompt wrong (#3431, #2960; PR #4787)
On Windows the HIP runtime gives every cudaHostRegister'ed range a second address for the GPU and takes any host pointer inside it for the registered memory, but it does not reserve that range in the process's address space. In about one start in ten the stack of the thread that pins the expert arena lay there: small cudaMemcpy calls from local variables then read (and wrote) the arena, and every long prompt was read wrong until a restart.

- pinned.cu: each registered range's alias is reserved (MEM_RESERVE); a range whose alias already covers memory in use is unregistered and treated as refused by the driver, so it stays resident but unpinned (logged).
- generate.cpp: main keeps the free address space below 1 TiB reserved for the life of the process and runs the engine on a new thread, so its stack and later allocations lie above the aliases (STRATA_STACK_ISOLATE=0 turns it off).
- pinned_alias_test, docs/AMD_HIP.md.

Windows HIP only; other backends keep their behaviour. Author's measurements: 0 bad starts in 110 (main + change 0 in 70, main alone 5 in 70) on an RX 7900 XTX. Here: the HIP objects, pinned_alias_test and pin_budget_test build for gfx1100 with ROCm 10.2 (no gfx1100 card to run them on). The hunk in pinned.hpp was applied by hand because v0.2.9 has the shared-file fields between the lines the PR touched.
2026-10-31 12:52:41 +01:00
Niko1221 679961662d Control: the tuning probe's child output is read with errors replaced too, the last place of #4675's kind (#4629: the same crash on a Russian-locale PC was fixed in 0.2.9 by 7146d6983) 2026-10-31 12:52:12 +01:00
Niko1221 44bca4099f App: DeepSeek-V4.1-Flash is ready to read pictures without the Qwen reader program; the Models screen no longer tells its owner to update an engine that is fine, and a load with Read pictures on no longer asks for a vision section it never has (#4772)
The screen looked for strata-vision in the engine folder for every model; DeepSeek carries its own picture tower (--vision on|off), so the check always failed for it and Read pictures stayed off and locked.
2026-10-31 12:51:29 +01:00
Niko1221 35057071c1 Tool calls: a call written as tools.write, functions.write or default_api.write is the declared tool write (#4751)
A programmatic-tool-calling harness shows the model its tools as tools.write(...) inside code, and the model then wrote the native call with that spelling; the call came back as text and the run stopped. The prefix is read like the other unambiguous aliases (a declared name always wins, an undeclared name stays text). Also: a tool_reference with a non-string name no longer fails the request. Tests for the prefixes and for the history rule on the Anthropic and Responses shapes.
2026-10-31 12:47:49 +01:00
Niko1221 6a8752f9bb Tool calls: a tool the conversation already called counts as declared on the Anthropic and Responses routes too, not only on Chat Completions (#4663)
The 0.2.9 rule read only assistant messages' tool_calls. It now also reads tool_use blocks (Anthropic) and function_call / custom_tool_call input items (Responses, with the namespace in front of the name). Test on the /v1/messages route added.
2026-10-31 12:47:49 +01:00
xripandNiko1221 9efac55f51 Anthropic tool search: a request with a defer_loading tool returns a call for an undeclared name as a tool_use, and a tool_reference loads the tool's definition where it stands (#4663, PR #4700)
Claude Code declares ToolSearch and a defer_loading placeholder and names its MCP tools only in the conversation; when the model
calls one of them straight away the call came back as text and the session stopped. Ported from PR #4700 by xrip onto v0.2.9
(the server.py hunk for the parser call moved); the PR's own tests are included.

Co-authored-by: Niko1221 <[email protected]>
2026-10-31 12:46:05 +01:00
Niko1221 2fba107658 Merge branch 'f-041-maya-a' into f-041-maya-int 2026-10-31 12:34:50 +01:00
Niko1221 b19b38aff1 server: note that the FP16 weight cache and the no-loan prompt path are the engine's own automatic defaults 2026-10-31 12:33:01 +01:00
Niko1221 dde320ffaf Launch: after a saved --backend choice the launch says which make is used; the plan for an Intel card no longer announces a draft layer download and says Strata builds the pack 2026-10-31 12:18:14 +01:00
Niko1221 b0fde18b16 Launch: a PC whose NVIDIA driver does not answer but has working Intel Arc cards is told so, with the one next step (--backend intel) 2026-10-31 12:15:24 +01:00
Niko1221 caef1e879a Metal: spin-then-block wait in run_graph (decode +5%, same tokens); STRATA_METAL_PROFILE prints periodically and at exit 2026-10-31 12:12:24 +01:00
Niko1221 4f0fafbc8f Warning for UD-Q4_K_XL on a Strix Halo in setup, launch and the plan (can stop on the first request after a start; points to UD-IQ4_XS; warn only, written to be removed with the stall fix) 2026-10-31 12:05:53 +01:00
Niko1221 d88531297c Launch on an Intel Arc card prepares the model itself: setup.py --prepare-pack builds the pack from the GGUF files and fetches gguf-py on its own, the run settings are written as before, and docs/INTEL_ARC.md's quick start is true 2026-10-31 12:05:53 +01:00
Niko1221 17a572f3e3 Launch: --backend nvidia|amd|intel|auto chooses the engine make on a PC with cards of several makes and is remembered; --plan and the Cards step say the Intel cards exist; a chosen make is never refused 2026-10-31 12:05:52 +01:00
Niko1221 e809e67ecf DeepSeek Metal build: the PCIe lane code compiles on macOS (no sched_getaffinity, no PCI bus id or registered-memory attribute, no globaltimer asm); Linux/CUDA/HIP paths unchanged 2026-10-31 11:33:44 +01:00
Niko1221 6a07e8ad72 Setup: the no-internet DeepSeek message keeps its wording and adds the proxy sentence after it; the install.sh hardware-line test puts its fake tools first on PATH 2026-10-31 11:29:40 +01:00
Niko1221 5f19bf5b32 Merge branch 'f-041-maya-par' into f-041-maya-port2 2026-10-31 11:16:35 +01:00
Niko1221 b2c3847fc0 HIP: Q2_K's GEMV always through the 4-column kernel (in slices), so a column does not depend on the rows beside it; ds4_gguf_test --rows says what a differing column depends on 2026-10-31 11:13:41 +01:00
Niko1221 9f86e95a3c serve: STRATA_DS4_RESTORE_SHARED=0 leaves the shared start out of the restore points the server asks for (for A/Bs) 2026-10-31 11:08:42 +01:00
Niko1221 e9d9ed8bb3 DeepSeek engine: a shared start a fresh prompt reaches is kept from the one read, not by stopping there (A3)
A read from token 0 that reaches the shared start (shared=, pin=) no longer stops at it: the start is a restore point inside
the one run of chunks (feed_marks), and the state at it is saved apart for new conversations after the read, from that mark
(Model::slot_save(out, err, at): the window rings and partial groups from the mark, the compressed KV and index keys of rows
0 .. at / ratio, the tokens up to it).  The stop cost a whole extra pass over the streamed experts: a fresh 6000-token prompt
with the shared start at 4500 and a tail point, DGX Spark first token 21.0 s -> 16.4 s (14.35 s with no keys at all).
STRATA_DS4_PREFIX_STOP=1 stops at the start as before.  A new chat behind the start still takes it back in 0.11 s and reads
the same tokens as a cold read; the agent loop total is unchanged (188.8 s against 187.9 s, 3 runs each).

Credit: Project Maya #607 (mw00), MIT, third_party/project-maya/NOTICE.md.
2026-10-31 11:07:52 +01:00
Niko1221 8becec7d5c gfx1151 on Windows: the exact parallel-request switches are on by default for 2 or more batch slots
serve/server.py (0.2.9) sets the nine qualified switches only on Linux; Windows had none. Measured on a Strix Halo Windows PC
(Qwen3.8-Flash-Next IQ3_S, serve/server.py --parallel 4 over the engine, tools/par_load.py w2 at 4 clients, 3 rounds, interleaved,
download paused): 51.0 / 51.2 / 51.7 / 50.7 tok/s without, 55.0 / 55.6 / 55.2 / 55.1 with (+8%), TTFT p50 2.57 -> 2.43 s,
max chunk gap p95 1.97 / 1.37 -> 1.27 s, no errors. Not STRATA_BATCH_CONCURRENT_READ.

parallel_default_env() in arch_defaults.cpp: CKPT_POOL, CKPT_POOL_READY=4, SLOT_CKPT_MOVE, SLOT_COPY_DIRECT, SLOT_COPY_BATCHSYNC,
BATCH_GDN_GROUPS, BATCH_ATTN_ROWS, BATCH_SELF_COMMIT, QSA_SLOTS, set from generate.cpp with --batch / --slots of 2 or more on a
Windows gfx1151; a switch the user set is kept; STRATA_GFX1151_DEFAULTS=0 turns it off. Unit test in arch_defaults_test.cpp.

verify.cpp read STRATA_BATCH_SELF_COMMIT and STRATA_QSA_SLOTS into namespace-scope constants, before main() sets the defaults, so
two of the nine would never have seen them (the first engine build measured 52.2 / 52.6 tok/s, the base). They are read at the
first use now. Also logs the time of each verify window graph capture.
2026-10-31 10:53:26 +01:00
Niko1221 b3b47b108b build_windows.bat: STRATA_CMAKE_EXTRA reaches the cmake configure line (for example -DSTRATA_WINDOW_MAX_T=8 builds the 8-row engine) 2026-10-31 10:53:19 +01:00
Niko1221 cda6463e8e Qwen parallel: FP16 weight cache sized by free VRAM (3 GiB kept free), no loan of cache slots when every expert fits, replica profile keeps 2500 MiB free 2026-10-31 10:47:25 +01:00
Niko1221 169d5d6a9e DeepSeek --parallel: the DSpark drafter in the steps of several requests (Project Maya #611, needmorevram; with mw00's 1434096, 4b2378b, 0d0f261) 2026-10-31 10:16:57 +01:00
Niko1221 ab0ed4feab Launch: the Intel Arc 'cannot set up this card' message points to docs/INTEL_ARC.md and the pack tool 2026-10-31 10:15:14 +01:00
Niko1221 ed3de732cc Tests: install.sh hardware-line test writes its fake tools with real newlines 2026-10-31 10:02:06 +01:00
Niko1221 b3c3909147 Setup: finish marks of a read-only model folder are kept in Strata's own folder (the 110 GB Engram is hashed once, not on every launch); a prepared model starts with no internet: the llama.cpp source is fetched only when a preparation tool runs, and a pinned file copied in by hand is checked locally instead of asking the server 2026-10-31 09:57:47 +01:00
Niko1221 35ca7ec773 App agent: the system prompt says which computer it runs on (Windows/PowerShell, macOS, Linux), so the model stops probing with ls, uname and wc 2026-10-31 09:56:02 +01:00
Niko1221 69896982ae DeepSeek --parallel on an event scheduler: prompts read a piece at a time, a read alone yields to a request, opt-in STRATA_DS4_READ_PIECES (Project Maya #587, mrsirg97-rgb) 2026-10-31 09:55:53 +01:00
Niko1221 896818a3de DeepSeek plan on AMD: the expert count matches the engine's carve-out (2,365 on one R9700 and 12,152 on four, where real runs held 2,366 and 12,147) 2026-10-31 09:54:21 +01:00
Niko1221 86c94204a1 Naming: the DeepSeek sets are ISTA-DAS on screen and in the docs; the ids (--model IST-DAS-2.25bpw) are unchanged and the new spelling is accepted too 2026-10-31 09:51:11 +01:00
Niko1221 939e1acbae install.sh: the graphics line comes from the PCI class and a failed nvidia-smi gives no name (an SSD and an error sentence were printed as the card) 2026-10-31 09:50:49 +01:00
Niko1221 61b39f08b7 Setup: a download that cannot reach its server says what to set behind a proxy 2026-10-31 09:50:49 +01:00
Niko1221 27060866eb Launch: no disk warning for model files used where they are, and the tight-fit notice no longer says tight twice 2026-10-31 09:50:48 +01:00
Niko1221 3da4517962 Update wording: a first install does not say a previous engine is kept; a second rollback says it moved to the newer version and no longer marks it skipped; --check names the command to force a skipped version; the app line says the Terminal cannot tell the installed app version 2026-10-31 09:50:48 +01:00
Niko1221 a5112326a0 Server: a stop that arrives the moment the ready file appears takes the clean stop (file removed, exit 0) instead of an uncaught interrupt; fixes the flaky ready-file test 2026-10-31 09:44:39 +01:00
Niko1221 866681a889 Tests: the encoder-build test no longer reads the real cards of the box it runs on 2026-10-31 09:44:39 +01:00
Niko1221 45f0e48725 Local models: the importer accepts the Flash-Next family's own architecture (qwen4exp) instead of sending it to llama-server (#4726, part 2 of 4) 2026-10-31 09:44:05 +01:00
Niko1221 050741096b Parser: bare and unwrapped calls keep their old closing rule; the DeepSeek plain-parser test settles its call at the end of the turn (#4701 follow-up, full serve suite 1265 passed on p1006) 2026-10-31 09:44:05 +01:00
Niko1221 49b880c01f Parser: a call's closing tags end a value only when no prose and later closing tags follow, so a file that lists them is written whole; a closed call is held until the turn ends, a new call starts or 1500 characters pass (#4701, Option C) 2026-10-31 09:44:05 +01:00
Niko1221 e9b4bf0055 Parser: tests that pin the #4701 decision cases (one expected failure today); options written up in phaseA-tests/urgent029/OPTIONS-4701-4704.md 2026-10-31 09:44:05 +01:00
Niko1221 499a68dba6 DeepSeek --parallel: the prompt read a piece at a time with the others' decode steps between the pieces, opt-in STRATA_DS4_PIECE (the idea of Project Maya #587, mrsirg97-rgb) 2026-10-31 09:42:19 +01:00
Niko1221 331ef6890d DeepSeek prompt ring sized by the GPU's measured host link, opt-in DS4_RING_MB=link (Project Maya #609, mw00) 2026-10-31 09:37:43 +01:00
Niko1221 3ce04cd88d ds4_gguf_test --rows: say which row count and column differ, and the single-column result bits 2026-10-31 09:35:38 +01:00
Niko1221 c85cfc7cc2 ds4_main: --multi-check reads --ids @file after it is expanded 2026-10-31 09:35:38 +01:00
Niko1221 ca89975ec1 serve: DeepSeek engines with --parallel (tagged T/PP/DONE lines, STOP <rid>, parallel -> --parallel) 2026-10-31 09:35:38 +01:00
Niko1221 27f3137493 DeepSeek --parallel: --serve multi protocol (GEN rid=, READY multi=), --multi-check, ds4_gguf_test --rows (Project Maya #332, mw00) 2026-10-31 09:35:38 +01:00
Niko1221 29c2d574f8 DeepSeek --parallel from Project Maya (#332, mw00): per-conversation state, attention split out of layer(), step_multi / sample_multi 2026-10-31 09:35:38 +01:00
Niko1221 f080938676 Merge branch 'f-041-maya-a' into f-041-maya-b
# Conflicts:
#	src/dsv41/ds4_main.cpp
#	src/dsv41/ds4_model.cu
2026-10-31 09:08:07 +01:00
Niko1221 8ea5361bfc Merge tag 'v0.2.9' into f-041-maya-b
Strata 0.2.9 (engines and the SYCL package built from 99866a47a2b8a21b131aad162a584ee9da92f8f0; the Windows and Linux x86 app, Terminal, Control and installers from c62d5e3a15b40ea51c96aae9e8d36928965f53ab; the Mac set from 4cf0d8cd3 and the Spark Control from 38f56619c, which the later card fix does not touch; the later commits change the Python Control, server, setup, tests, tools, docs, the app host and the Mac Metal engine only)
2026-10-31 09:07:42 +01:00
Niko1221 908a4dee7b Merge tag 'v0.2.9' into f-041-maya-a
Strata 0.2.9 (engines and the SYCL package built from 99866a47a2b8a21b131aad162a584ee9da92f8f0; the Windows and Linux x86 app, Terminal, Control and installers from c62d5e3a15b40ea51c96aae9e8d36928965f53ab; the Mac set from 4cf0d8cd3 and the Spark Control from 38f56619c, which the later card fix does not touch; the later commits change the Python Control, server, setup, tests, tools, docs, the app host and the Mac Metal engine only)
2026-10-31 09:07:32 +01:00
Niko1221 7d4465f75d DeepSeek engine: DS4_CHUNK_LOG=1 prints each chunked read's long-prompt skip (rows, kept rows, first restore point) 2026-10-31 09:07:32 +01:00
Niko1221 2bfd207d33 DeepSeek prompt path: Maya's buffered flow reads (DS4_FLOW_BUFFERED, 1024 tokens on one GPU) stay off by default
Measured on Boston (one A6000 as a 12 GB card, ballast leaving 64 GB of RAM, UD-IQ1_M): the first 1000-token prompt read took 17.0-17.5 s with the
B1 default against 11.1 s before B1 and 11.2 s with DS4_FLOW_BUFFERED=0. The setting is Project Maya #312's (Peasant Smith / mw00); it stays available
as DS4_FLOW_BUFFERED=<tokens> until it is measured to win here.
2026-10-31 08:03:15 +01:00
Niko1221 45a38347a5 DeepSeek expert cache: the RAM store's put reads the slot through sat() (tail slots), not vram_base + slot * unit
With a layer's slots partly in the lent tail region (B1, Project Maya #312 tail lending), a swap that hands its outgoing expert to the RAM store
(DS4_STORE_PUT, on by default) read vram_base + slot * unit past the layer's own allocation: on HIP (p620, short prompt, windows path, cache on)
the first swap raised an illegal memory access and poisoned the context. The DBG slot dump had the same address. Adds tests/dsv41/short_prompt_check.py
(a 40-token prompt twice with the default sampler, then greedy; fails on ERR or illegal-access / router-picked lines).
2026-10-31 07:40:38 +01:00
Niko1221 54eff48756 DeepSeek engine: a short chunk streams only the experts it picks on the batches path, opt-in (A4b); DS4_UNPICKED_STAT
Project Maya #691 (needmorevram, c7d08c5), the "a chunk streams only the experts it picks" part, onto src/dsv41's batches
path (Streamer / copy_ahead; DS4_FLOW=0 or a pack the flow does not take): with DS4_SKIP_UNPICKED_ROWS=N (or
DS4_SKIP_UNPICKED=1 for every chunk) a chunk of at most N rows waits for its layer's routing and then reads and copies only
the experts some row of it picked, into the same slots and batches, so the summing order is the same.  Off by default.
Strata's flow path already streams only routed experts, so the default path gains nothing from it.
DS4_UNPICKED_STAT=1 prints, per chunk size, the share of a layer's experts no row of the chunk picked.

Credit: Project Maya #691 (needmorevram), MIT, third_party/project-maya/NOTICE.md.
2026-10-31 07:17:30 +01:00
Niko1221 490b4b0fad Merge f-041-maya-a2 (A2 tail cost, A5 path calibration, small items) into f-041-maya-a
Keeps A3's shared-start store and A4a's feed_marks together with Project Maya #668's tail-cost judgement (mw00).
2026-10-31 04:53:53 +01:00
Niko1221 0be5d73f00 DeepSeek engine: restore points near a prompt's end kept in one read, feed's after (A4a)
Project Maya #691 (needmorevram, c7d08c5), the restore-point part, onto src/dsv41: Model::feed_marks() keeps the restore
points inside a run of chunks from snapshot sets the layers fill on the way (kSnapN = 8 sets; feed_mark's one set became
an array), each the state a read up to that point leaves, instead of a run up to each point; Model::marks_inside() says
whether that is worth it (the long-prompt skip's extra rows at most a chunk, inner_cost); the serve loop reads the rest
of a prompt that way when the points left are inside it (not past a shared start a read from token 0 stops at, #607).
STRATA_DS4_MARKS_INSIDE=0 stops at each point as before.  feed()'s `after` (the rest of the prompt still to come) moves
the long-prompt skip's kept rows to the prompt's end.  Not ported from #691: the chunk streaming only the experts it
picks, the EXL3 lend, DMA read-back of host sums.

Credit: Project Maya #691 (needmorevram), MIT, third_party/project-maya/NOTICE.md.
2026-10-31 04:37:02 +01:00
Niko1221 c62d5e3a15 DeepSeek plan: the NVIDIA cards it counts are the ones setup runs (a card of 8 GB or less starts Off beside a bigger one), and the plan says which card is not used 2026-10-31 03:23:01 +01:00
Niko1221 7b96b21cf9 DeepSeek engine: DS4_TEST_SAMPLE_FAIL test hook for the sampler error handling (Project Maya #619, Cody Harrison) 2026-10-31 03:22:59 +01:00
Niko1221 7aa9a584aa Setup: say which card is not used when a card under 8 GB sits beside a bigger NVIDIA card (a line only, no change to the cards used) 2026-10-31 03:10:24 +01:00
Niko1221 987ebd442f Launch: list the Arc Pro B65 (0xe222) and the other discrete Battlemage/Alchemist ids, so two cards are both named for a launch that names none 2026-10-31 03:10:24 +01:00
Niko1221 c17b458c4f DeepSeek engine: a new conversation behind the same system prompt and tools reads only its own part (A3)
Project Maya #607 (mw00, b2d4c30) onto src/dsv41: when a prompt read from token 0 reaches its shared start (shared=,
pin=) the state the read keeps there is also copied apart from the conversation, in RAM (slot_save).  A prompt that does
not continue the conversation in the model takes back the longest kept start it begins with, when that covers more than
the model's own restore points (a kept conversation slot only when it covers more still), and reads only the rest.  With
or without STRATA_DS4_SLOT_KEEP.  STRATA_DS4_PREFIX_KEEP (2 starts, LRU), STRATA_DS4_PREFIX_GB (2), STRATA_DS4_PREFIX_MIN
(1024); 0 keeps none.  Starts with pictures are not kept.

Credit: Project Maya #607 (mw00), MIT, third_party/project-maya/NOTICE.md.
2026-10-31 03:09:06 +01:00
Niko1221 70f1f90d75 DeepSeek engine: DS4_ASYNC_T stays 8 by default (16 is opt-in; Project Maya #608, mw00): the window-size logits test did not show the claimed independence 2026-10-31 03:07:58 +01:00
Niko1221 e4031fc4d0 AMD cards fix, review round: setup's pinned one-card pick is still written (with gpus_by), only DeepSeek on AMD is judged when a copy adopts an earlier card, the launcher uses the plan's card rule and remembers a named card, Windows uses setup's amd_rank and no Linux arch list, message names strata launch --gpu 2026-10-31 03:05:41 +01:00
Niko1221 dec8f1366c Tests for the AMD card rules (4 cards, APU, mixed architectures, Windows, --gpu, --gpus, old configs, Intel, NVIDIA unchanged); stale-stamp test stubs setup.gpus 2026-10-31 02:56:19 +01:00
Niko1221 6f7f763ce5 DeepSeek on several AMD cards uses every card when none was named: setup's own pick is no longer 'asked'; old configs of that pick move to every card at the start; the launcher and the app stop keeping it 2026-10-31 02:56:19 +01:00
Niko1221 f93ec8a924 DeepSeek card rule shared by the plan, the options and setup: every card that counts, one AMD card on Windows, only cards the engine package has code for, an APU beside a discrete card left out 2026-10-31 02:56:19 +01:00
Niko1221 9edd253c25 NOTICE: fix the B4 credit line 2026-10-31 02:13:51 +01:00
Niko1221 1daf05cb02 Merge branch 'f-041-maya-a' into f-041-maya-b
# Conflicts:
#	third_party/project-maya/NOTICE.md
2026-10-31 02:05:45 +01:00
Niko1221 a280f5bbd0 DeepSeek one-GPU decode from Project Maya: copies pulled in place, windows streamed, landings in recent slots (#476 RubenTorney)
With the lane on (DS4_STREAM=1) and one discrete NVIDIA GPU, the CPU lane's RAM copies are pinned and the fetch kernel pulls a streamed
expert where it lies; a verify window's experts stream too and land in a recent slot, so the link's bytes stay in VRAM.
Gating and follow-ups: one-GPU path only where experts stream (1b60249, 482e46c #496, 7c0ceb1 #551, mw00), no landing while the swap is pending
and a table-update wait (32f3c8c handmade0octopus), a failed fill is never published (#646 handmade0octopus), Windows with pin by need (#567 TheSmokeDev),
HIP pull clock on gfx11/gfx12 and DS4_IQ1S_ROWS (#563 Cass, mw00). Everything stays behind DS4_STREAM=1; DS4_RAM_SKIP_W (changes answers) is opt-in.
Not ported: the helper-GPU condition (#415). Built on CUDA sm_120; model identity/A-B not run yet.
2026-10-31 02:04:11 +01:00
Niko1221 ec723c27e8 DeepSeek expert cache: recent slots from Project Maya (#344, needmorevram), selective port
The layer keeps DS4_RECENT=<n> VRAM slots (taken from its coldest experts) for CPU-lane experts it used twice in its last 8 ticks.
A thread copies each fill on its own stream, the next tick maps it by one tab_set launch a device; a swap leaves recent slots and
experts being filled alone, and a run of chunks that lends the tail waits for the fills first. Opt-in (default 0), values unchanged.
Not taken from #344: all-reduce over DMA, segmented top-k, DS4_INDEX_TC, the cuBLAS workspace pin.
ds4_gguf_test: its Q8_K default now follows the engine (opt-in, DS4_IQ_Q8K=1).
2026-10-31 02:04:03 +01:00
Niko1221 5322f1c2bd NOTICE for libstrix (asf0, MIT): the gfx1151 fused-expert barrier follows its barrier() 2026-10-31 01:21:21 +01:00
Niko1221 c28b5141a0 Project Maya v1.1.19 to v1.1.20 DeepSeek changes (staged merge, B3 prerequisite) 2026-10-31 01:13:52 +01:00
Niko1221 216252f034 DeepSeek PCIe lane and whole-token judge from Project Maya (#312 Peasant Smith, #329 mw00), staged merge v1.1.17 to v1.1.19
Lane and Q8_K rounding opt-in (DS4_STREAM=1, DS4_IQ_Q8K=1) until their gates have run.
2026-10-31 01:10:17 +01:00
Niko1221 abe5e67923 ds4 thread pool: fence the generation store before reading the sleepers, and start workers from generation 0
A worker going to sleep at the moment a batch started could miss both the new generation and the wake-up (the caller then runs the batch alone, slower, not wrong). Taken from Project Maya v1.1.36 (#618, Cody Harrison), MIT, credited in third_party/project-maya/NOTICE.md.
2026-10-31 01:08:49 +01:00
Niko1221 3ec28f5cc0 DeepSeek engine: tail restore point judged by its cost (A2), per-class path calibration (A5), sampler/lane/bf16 fixes, DS4_ASYNC_T 16
Taken from Project Maya (MIT), staged against v1.1.42:
- A2: the tail restore point is left out where its read stop costs more than
  reading its tokens again (TailCost wired into the serve loop; one in 32 still
  kept to measure again; STRATA_DS4_RESTORE_TAIL_AUTO=0 keeps it always). Maya
  #668 (mw00, 8721412 and 66fb17b).
- A5: the flow timing correction of the opt-in timing pick stays in the prompt
  size class that measured it (ds4_path_policy.hpp + test). Maya #612 (Will
  Hampson, 6e0d27d). Our default pick is by length already.
- A sampler CUDA error ends the request instead of sending token -1; logits()
  copies on the head stream. Maya #619 (Cody Harrison, 2040174).
- A CPU-lane layer that cannot compute its experts stops the engine instead of
  answering zeros; a layer with no GPU kernel is refused at load, any other
  warns. Maya #620 (Cody Harrison, e598716).
- bf16_rn by arithmetic on CUDA too. Maya #676 (rilken, 79628d7).
- DS4_ASYNC_T default 8 -> 16: windows of 9-16 rows take the same device path
  as 1-8. Maya #608 (mw00, b9c07e7). DS4_ASYNC_T=8 gives the old path.
2026-10-31 01:08:29 +01:00
Niko1221 e6d9221305 serve: ask the DeepSeek engine for restore points (marks=, tail=, shared=) (A1)
Project Maya #639 (needmorevram) and v1.1.37 / v1.1.39 (mw00), written onto Strata's server: deepseek_restore_points()
reads the last user turn, the first user turn (the end of the system prompt and tools) and the tail from the prompt's ids
as the template wrote them; Service.prepare puts them in the request's _restore (never a client's own), StrataEngine
sampling_keys turns them into marks=<pos>,... tail=<pos> shared=<pos>.  The shared start stays Strata's pin=N where the
request already has one (the system messages); shared= is asked only without it.  STRATA_DS4_RESTORE_POINTS (4, 0 = off)
and STRATA_DS4_RESTORE_TAIL (512, 0 = none).  serve/test_ds4_restore_points.py.

Credit: Project Maya #639 (needmorevram), mw00; third_party/project-maya/NOTICE.md.
2026-10-31 00:56:53 +01:00
Niko1221 6e53c7f8d6 DeepSeek engine: restore points (marks) a request asks for, kept in the conversation and in the slots (A0)
Ports Project Maya's restore points (#639, needmorevram, 50a3313; ds4_marks.hpp v1.1.42 by mw00 and needmorevram) onto
src/dsv41 as a per-file merge, not a commit apply:

- ds4_marks.hpp as in Maya v1.1.42 (latest_point, mark_plan, mark_victim, TailCost; TailCost is not wired in yet, #668).
- Model::mark(shared) / marks_cap() / check_bytes(): up to STRATA_DS4_RESTORE_POINTS (4, 0 = none, at most 16) restore
  points besides checkpoints 0 and 1 and the anchor, the least recently used giving way, the shared start last.
  restore() and best_check() go back to a mark; a slot keeps the marks with their roles (3 mark, 4 shared start), slot
  identity strata-ds4-session-4; the .meta file takes up to 3 + 16 checkpoint positions.
- Replaces Strata's fourth checkpoint (ck[3], the pin=N shared start) by a shared mark: pin=N is now shared=N.
- GEN keys marks=<pos>,... tail=<pos> shared=<pos> (pin=N kept): the prompt read stops at each point mark_plan keeps and
  keeps the state there.
- Tests: tests/dsv41/marks_test.cpp (Maya's ds4_marks_test), slot_test for the longer checkpoint list.

Credit: Project Maya #639 (needmorevram), #668 / ds4_marks.hpp (mw00), MIT, third_party/project-maya/NOTICE.md.
2026-10-31 00:56:53 +01:00
Niko1221 4cf0d8cd38 Merge f-039-mac-dsu-rel (33bbbca44) 2026-10-30 22:59:51 +01:00
Niko1221 33bbbca442 Offer UD-IQ1_M on a Mac too: not run there, said plainly in its plan (the Mac gate stays as an empty mechanism) 2026-10-30 22:59:35 +01:00
Niko1221 38f56619cc Both Unsloth DeepSeek sets are offered on a DGX Spark: both passed a real Spark run (AARCH64_NOT_YET is empty); tests keep the mechanism 2026-10-30 22:50:34 +01:00
Niko1221 1a26492064 Merge f-039-mac-dsu-rel (6a7d75057) 2026-10-30 22:46:09 +01:00
Niko1221 bee469c462 serve: the qualified GDN speedups are on from the server (Linux CUDA: GDN_TC + GDN_CONVL2=2; Linux HIP gfx12 only: GDN_CHUNKED), Qwen3.8 only, explicit env wins 2026-10-30 22:46:09 +01:00
Niko1221 6a7d750578 UD-IQ1_S on a Mac: the measured line from the M5 Max run (alone: 15-18 tok/s, 3K prompt 230-310, 14K 520) 2026-10-30 22:44:19 +01:00
Niko1221 417765b8d8 tests: the Spark gate still holds both Unsloth sets 2026-10-30 22:36:49 +01:00
Niko1221 5c25767fcd Offer Unsloth UD-IQ1_S on a Mac (run on an M5 Max); UD-IQ1_M stays off until it has run 2026-10-30 22:36:26 +01:00
Niko1221 ea5dbffa7f Metal: Q6_K multi-column dense matvec (ds4_q6k_mvn) 2026-10-30 22:36:13 +01:00
Niko1221 a3a1103c13 dsu_parity: --bench and the ds4_mm pipelines 2026-10-30 22:36:13 +01:00
Niko1221 ba3d28b7e3 Q6_K matvec: packed loads via uchar4 2026-10-30 22:36:12 +01:00
Niko1221 0dba618d34 Metal Q6_K dense matvec: unaligned block loads (210-byte blocks), found by dsu_parity; test fix 2026-10-30 22:36:12 +01:00
Niko1221 5ff7da4a70 Metal tests for the Unsloth set kernels: IQ1_S matvec and tile loader, Q6_K dequant and matvec, reference checked against ggml 2026-10-30 22:36:12 +01:00
Niko1221 1b6c67122a Metal DeepSeek engine: IQ1_S expert kernels (decode GEMV, window, prompt tile) and a Q6_K dense dequant for the Unsloth sets 2026-10-30 22:36:12 +01:00
Niko1221 7851a8dab6 Merge f-038-ds-unsloth (final 4148c5125) 2026-10-30 20:26:56 +01:00
Niko1221 4148c51257 launch: ask dsv41_fit.not_here directly (the launch tests stub the setup object) 2026-10-30 20:25:25 +01:00
Niko1221 bd3c5cd4cb Keep Unsloth's 1-bit DeepSeek sets off a DGX Spark until a Spark run passes (AARCH64_NOT_YET, one rule beside the Mac one) 2026-10-30 20:23:59 +01:00
Niko1221 99866a47a2 Merge f-029-urgent (bc694233b) 2026-10-30 20:06:15 +01:00
Niko1221 73e0a3b6ed Merge f-038-ds-unsloth (final 9144da7b1) 2026-10-30 20:00:26 +01:00
Niko1221 9144da7b1a tests: the launch list's download sizes include the two Unsloth sets 2026-10-30 19:59:31 +01:00
Niko1221 a833fb46f3 Merge f-038-ds-unsloth (macOS gate 32806f82a) 2026-10-30 19:58:24 +01:00
Niko1221 32806f82aa Keep Unsloth's 1-bit DeepSeek sets off the Mac until the Metal engine has an IQ1_S kernel (lists, --model, --model-path, app check, setup) 2026-10-30 19:57:24 +01:00
Niko1221 bc694233bf Hardware list: AMD cards are counted from the kernel's KFD files when there is no setup module and no rocm-smi (ROCm from a Python venv), instead of not at all 2026-10-30 19:45:23 +01:00
Niko1221 dd3f9ec82f tests: the catalog download sizes cover the two Unsloth DeepSeek sets (241.0 and 258.4 GB) 2026-10-30 19:35:11 +01:00
Niko1221 47cbb76690 Merge f-038-ds-unsloth (current head) 2026-10-30 19:28:13 +01:00
Niko1221 ab083a693f Chat: streamed text is applied about ten times a second instead of once per token, so a long answer no longer saturates the page (main-thread time 15.7 s to 9.0 s, peak heap 114 MB to 62-75 MB for a 40,000-token answer; Mio's Out of Memory report) 2026-10-30 19:24:02 +01:00
Niko1221 ad4755c249 Say plainly that the 1-bit UD-IQ1 sets are weaker on long or careful work 2026-10-30 19:23:07 +01:00
Niko1221 b60fbb54f7 serve: the parallel stack default is Linux CUDA only (HIP and Windows get nothing: no measurement outside the replica config) 2026-10-30 19:22:21 +01:00
Niko1221 8f13b8c4ad Server: one plain line at start when a graphics card in use is of an architecture the compiled engine was not built for (#4662) 2026-10-30 19:18:45 +01:00
Niko1221 0b1808f072 Update: a failed test start of the new engine names the kept log file to report (#4692) 2026-10-30 19:18:01 +01:00
Niko1221 cfa7f737cc Setup: a compiled engine gets code for every usable NVIDIA card in the PC, not only the card setup picked (a TITAN RTX + RTX 3080 build for sm_75 only failed every long prompt with unspecified launch failure) (#4662) 2026-10-30 19:18:01 +01:00
Niko1221 b3de587e03 Merge f-036-strix-u (7f6bc3308): the Windows gfx1151 default set holds both STRATA_SELECT_WMMA and STRATA_PF_F16 (seven switches); test counts updated 2026-10-30 19:17:27 +01:00
Niko1221 7f6bc33081 gfx1151 on Windows: the prompt experts run on the F16 matrix-core kernels by default (STRATA_PF_F16)
Owner rule: a consistent Strix Halo speed gain goes on by default. Windows gfx1151, engine 0.1.54 builds, interleaved, idle box
(ab_f16def, 2 rounds, no environment on either side): IQ3_S prompts 4K 1,180 -> 1,279, 8K 1,289 -> 1,398, 32K 1,299 -> 1,404 tok/s
(x1.08); UD-IQ4_XS 8K 1,092 -> 1,223, 32K 1,135 -> 1,248 (x1.10-1.12). Decode over 200 tokens unchanged (58.6 -> 58.2, 48.8 -> 48.5),
first greedy ids identical, 38 of 38 runs exit 0. The rounding differs from the int8 kernels; KL is information only.
STRATA_PF_F16=0 turns it off. arch_defaults_test expects six switches on Windows gfx1151.
2026-10-30 19:15:54 +01:00
Niko1221 1ead709790 Control: the time-skip check and tune engine reads also tolerate non-UTF-8 output (#4675) 2026-10-30 19:12:42 +01:00
Niko1221 dd63f88fdd Qwen3.6 engine: the warm-up reads only cells that exist and starts its small probes in a fresh sequence, so a prompt chunk equal to the context no longer writes past the KV cache (#4708) 2026-10-30 19:12:21 +01:00
Niko1221 2a9ccfe3ea serve: the parallel stack default only where it was run (Linux CUDA nine switches, Linux HIP seven, nothing on Windows or macOS) 2026-10-30 19:08:30 +01:00
Niko1221 dd471d3140 serve: the exact parallel stack is the default with batch slots (explicit env still wins)
With --parallel/--batch >= 2 on a CUDA or HIP Qwen engine the server sets CKPT_POOL=1, CKPT_POOL_READY=4, SLOT_CKPT_MOVE, SLOT_COPY_DIRECT, SLOT_COPY_BATCHSYNC, BATCH_GDN_GROUPS, BATCH_ATTN_ROWS, BATCH_SELF_COMMIT and QSA_SLOTS unless the environment or the config's env already names them (=0 turns one off). DeepSeek, SYCL, Metal and Vulkan engines are untouched.
2026-10-30 19:08:30 +01:00
Niko1221 4a75fa2b62 API gateway: a client that hangs up right after data: [DONE] no longer turns a finished request into "Request cancelled." (#4690) 2026-10-30 19:07:42 +01:00
Niko1221 3282c61343 Server: a tool the conversation already called counts as declared, so a call to it comes back as a tool call, not raw text (#4663) 2026-10-30 19:06:50 +01:00
Niko1221 209dcadb3f Parser: a fence is closed only by one as long as it opened with, and an indented code block is a quote, so calls shown in them are not run (#4702 #4703) 2026-10-30 19:06:11 +01:00
Niko1221 e66a445892 Server: a picture reader written into an older engine folder follows the engine in use after an update (#4658) 2026-10-30 19:03:21 +01:00
Niko1221 68718c33fa Monitor: the drive-temperature reader (every physical drive, from the first second) runs only while the Monitor is open, not at app start (#4661) 2026-10-30 19:02:16 +01:00
Niko1221 ea52e55cd7 Chat: only the streaming message renders again on each token, so a long chat no longer redoes its whole history per token (Out of Memory report from Mio; heap and main-thread time measured) 2026-10-30 18:58:34 +01:00
Niko1221 08e8770c6e Prefill (layer-major): an expert with no mapped blob is copied by its source instead of ending the request (#4695) 2026-10-30 18:46:16 +01:00
Niko1221 7146d6983f Setup, Control and Terminal: a child's non-UTF-8 output (GBK adapter names) no longer kills Step 1 or launch (#4675) 2026-10-30 18:44:41 +01:00
Niko1221 01ab6b51bb Merge f-036-strix-ds (957038a1c) 2026-10-30 18:43:54 +01:00
Niko1221 957038a1cd dsv41 (Windows HIP, integrated GPU): the shards' views are dropped after the load by default (DS4_NO_VIEW=0 turns it off, =1 forces it elsewhere)
Unbuffered reads of a file that has a view in the process run at half the drive's rate on Windows (probe on Strix Halo: 6.5 GB/s with no view, 3.2 with one). Default on for an integrated GPU, where the CPU lane's misses read through the store's slots. Decode 9.1 -> 9.4-9.7 tok/s (code, drafter off, 2 interleaved pairs); the 32K prompt is unchanged; no failed reads in any run.
2026-10-30 18:43:29 +01:00
Niko1221 d340dc8dd1 apps/host test: planning a whole-drive models folder no longer deletes <drive>\Strata (#4689) 2026-10-30 18:43:11 +01:00
Niko1221 ffaa6ad78c tests: the app preview and launch list know the two Unsloth sets 2026-10-30 18:41:24 +01:00
Niko1221 4006305476 tests: the lockfile versions are 0.2.9 2026-10-30 18:40:48 +01:00
Niko1221 97e6f1fe19 Version 0.2.9: Terminal and app 0.2.9, engine and Control 0.1.55 (every Qwen and DeepSeek package, Metal and SYCL included) 2026-10-30 18:40:46 +01:00
Niko1221 33bd74182b Merge f-038-ds-unsloth 2026-10-30 18:40:13 +01:00
Niko1221 4d12701181 Merge f-0281-ds 2026-10-30 18:40:13 +01:00
Niko1221 aed382885e Merge f-036-strix-ds 2026-10-30 18:40:12 +01:00
Niko1221 274257630b Merge f-036-strix-u 2026-10-30 18:40:12 +01:00
Niko1221 d96873fc56 Merge f-036-strix-q 2026-10-30 18:40:11 +01:00
Niko1221 86bed1c92f Merge f-037-arc (5eaee81a4) 2026-10-30 18:40:11 +01:00
Niko1221 203df526f1 serve/test_admit_order: the engine stand-in sets the arrivals deque the coalescing code reads 2026-10-30 18:39:58 +01:00
Niko1221 b6fc410ee5 Metal window.cpp: qualify NG_HC_DIM / NG_HIST in finish_rows (the --batch rows code did not compile in the host build) 2026-10-30 18:39:58 +01:00
Niko1221 0a76d62e68 pinned.cu: the shared-arena tmpfs check is Linux-only (sys/vfs.h and statfs f_type), so the Metal host build compiles again 2026-10-30 18:39:58 +01:00
Niko1221 c0e245c8be Merge f-029-par-metal (766b54afb) 2026-10-30 18:39:27 +01:00
Niko1221 16e08b50cd Merge f-029-par-amd (4772f0cd9) 2026-10-30 18:39:27 +01:00
Niko1221 1fb3e9a326 Merge f-029-par-harness 2026-10-30 18:37:54 +01:00
Niko1221 1f8f2fa8ad Merge f-029-par-big 2026-10-30 18:37:53 +01:00
Niko1221 3a76d5eaf6 Merge f-029-gdn-mx 2026-10-30 18:37:53 +01:00
Niko1221 01ef876562 Merge f-029-gdn-amd 2026-10-30 18:37:53 +01:00
Niko1221 4746bfb165 Merge f-029-gdn-cuda 2026-10-30 18:37:52 +01:00
Niko1221 0b1fbf4012 gfx1151 on Windows: STRATA_SELECT_WMMA joins the default prompt set (x1.008 at 8K, x1.037 at 32K, x1.077 at 64K; about 10 of 2,051 selected cells flip at the cutoff for queries past ~5K tokens) 2026-10-30 18:22:35 +01:00
Niko1221 6923421a61 Accept Unsloth's DeepSeek UD-IQ1_S and UD-IQ1_M sets (pins, models, app file check, fit numbers; opt-in names) 2026-10-30 18:05:31 +01:00
Niko1221 09dc5c52b3 gfx11 gr_upmix: 256 tokens a block (16 waves) halves the w_up staging per token; prefill timing splits the QSA projection stage 2026-10-30 17:55:46 +01:00
Niko1221 5eaee81a43 [SYCL] kprof: profiling-tag sections time stretches without events of their own (the oneMKL GEMMs) 2026-10-30 16:33:01 +01:00
Niko1221 d936db6547 gfx1151: a Q8_0 down expert layer takes the F16 matrix-core prompt kernel by default
UD-IQ4_XS and UD-Q4_K_XL keep five layers with Q8_0 down experts; without STRATA_PF_F16 they left the fused path for MMQ.
On those layers only the F16 kernel now runs (STRATA_PF_Q8D_F16=0 turns it off); every other format keeps its kernel.
UD-IQ4_XS on Windows gfx1151, interleaved x3: 4K prompt 1,071 -> 1,120 tok/s, 8K 1,136 -> 1,180, same output ids;
IQ3_S unchanged (1,190 -> 1,186, 1,286 -> 1,284).
2026-10-30 16:15:54 +01:00
Niko1221 ac3e0e17b5 GDN gfx12: the chunked path from 64 tokens, as on gfx11 (owner: every gain counts; final gate at 64: +1.7% to +6.1%, KL 0.0025/0.038, 55/61 tasks, soak clean) 2026-10-30 16:05:14 +01:00
Niko1221 b3bc477512 diagnostics: STRATA_QSA_STAT (overlap of consecutive queries' selections), hip_pa_bench target and PA_BENCH_REF (compare two variants of the prompt attention on a recorded selection) 2026-10-30 15:41:53 +01:00
Niko1221 db729163ce Revert "setup: a Strix Halo's BIOS carve-out holds expert slots of the Unsloth RAM-budget plans"
This reverts commit 94fc01c6fd.
2026-10-30 15:41:41 +01:00
Niko1221 19b5f873f2 dsv41 (Windows): DS4_NO_VIEW=1 drops every view of the shards and the load's buffered handles after the load, reads go unbuffered
A file with a view (or a buffered handle with cached reads) in the process halves unbuffered read speed on Windows (probe: 6.5 GB/s with no view, 3.2 with a view of the whole file or of 1 GB of it, 3.3 with a buffered handle held open, 6.7 on a copy while the original is mapped). The CPU lane reads misses through the store's slots, so the views are not needed after the load: Pack::drop_views() unmaps them, reopens never-read fallback handles and Pack::read_at goes unbuffered. StoreHold re-reads an expert that has no slot instead of falling back to the view.
Also: DS4_IO_STATS (demand read queue/read/copy times), DS4_IO_PRIO, DS4_READ_SPLIT (default 1, off; deeper queues did not help). Decode 8.9 -> 9.6 tok/s (code, drafter off).
2026-10-30 14:56:43 +01:00
Niko1221 0122eceb4c [SYCL] --batch-groups: 'auto' is understood, and two Arc B-series cards with 8 or more slots default to 2 pipelined groups (INFO batch_groups); STRATA_BATCH_GROUPS_AUTO=0 / --batch-groups 1 turn it off 2026-10-30 13:56:52 +01:00
Niko1221 8401c21f9d [SYCL] prompt attention on the int8 DPAS (ESIMD, Battlemage): hi/lo int8 Q and P, 2D block loads for the four-cell selection blocks; default on for Battlemage, STRATA_PROMPT_ATTN_ESIMD=0 off 2026-10-30 13:22:33 +01:00
Niko1221 9c2121cf5c dsv41: the CPU lane's service thread spins through a decode token (DS4_LANE_SPIN_MS, 250) and naps on a high-resolution timer; DS4_PROF_SYNC decode stage profile; DS4_LANE_SKIP timing oracle
The service thread spun 2 ms after its last answer; a Strix Halo decode layer takes ~2.8 ms, so it slept (Windows sleep_for(20 us) wakes after ~1 ms+) before almost every request.
Decode 8.2 -> 9.3 tok/s on pack 2.25 (2 interleaved pairs). DS4_PROF in decode and in the prompt hangs the HIP driver on Windows; DS4_PROF_SYNC drains the stream at each decode stage mark instead.
2026-10-30 12:26:18 +01:00
Niko1221 1cfffa2ede gfx1151 fused experts: one barrier per stage (explicit LDS wait, after libstrix's barrier(), asf0/libstrix MIT) and 3 blocks per WGP for IQ2_S/IQ2_XS gate/up on gfx11 2026-10-30 12:14:41 +01:00
Niko1221 a83252a7b7 gfx1151: the VGPR cap of the occupancy expert kernels applies on clang 24 as on clang 23 (IQ3_S/IQ4_NL layer 79.7 -> 69.9 ms at 16K tokens, parity unchanged) 2026-10-30 11:40:16 +01:00
Niko1221 d8056fbe00 [SYCL] STRATA_KPROF=1: per-kernel GPU timer (event-profiled queues, kp() launch wrapper, decode/prompt tables); tools/arc A/B harness 2026-10-30 11:27:59 +01:00
Niko1221 6fb110e557 Opt-in research switches: STRATA_PAR_COUNTERS, STRATA_PIPED_CONCURRENT_MIN, multi-prompt layer-major read (Prefill::run_multi, BCO co-reads, server STRATA_COALESCE_PREFILL), STRATA_CRASH_BT 2026-10-30 11:08:53 +01:00
Niko1221 28c4543e48 tools: parallelism load harness (par_load, par_analyze, par_run, sweep, recon), E00 capability probe, batch_test --co/--seq/--pad and the phase-1/2 variant lists 2026-10-30 11:08:53 +01:00
Niko1221 e1e7e32680 prefill timing: STRATA_PREFILL_TIMING also splits the hyper-connection read into down, up, inject and mix 2026-10-30 11:03:01 +01:00
Niko1221 94fc01c6fd setup: a Strix Halo's BIOS carve-out holds expert slots of the Unsloth RAM-budget plans
uma_expert_cache counted only the RAM the OS sees, so UD-IQ4_XS (55 GiB of experts) and UD-Q4_K_XL (72 GiB) on a PC with a
64 GB carve-out and 64 GB seen by Windows got no cache slots beside the 40 GiB budget and read the rest from the SSD.
The carve-out (less 14 GB for the dense weights and buffers) now adds its slots. Tests added.
2026-10-30 11:00:42 +01:00
Niko1221 b0ba459514 Merge f-029-par-sycl (SYCL batch windows, GDN/attn rows kernels) 2026-10-30 10:56:29 +01:00
Niko1221 2fef430202 Merge f-034-cand (0.2.9 candidate) into f-036-strix 2026-10-30 10:52:31 +01:00
Niko1221 7a9245b6c0 [SYCL] batch window: port the one-launch-for-all-rows kernels (GDN groups, block scores rows, decode attention rows; opt-in STRATA_BATCH_GDN_GROUPS / STRATA_BATCH_ATTN_ROWS) and the batch-window stage profile; no speed gain on 2x Arc B65 2026-10-30 08:25:37 +01:00
Niko1221 368bcb7a26 serve/test_admit_order: the engine stand-in sets the arrivals deque the coalescing code reads 2026-10-30 07:32:39 +01:00
Niko1221 5ad0135c00 Metal window.cpp: qualify NG_HC_DIM / NG_HIST in finish_rows (the --batch rows code did not compile in the host build) 2026-10-30 07:32:39 +01:00
Niko1221 cdb5470c70 pinned.cu: the shared-arena tmpfs check is Linux-only (sys/vfs.h and statfs f_type), so the Metal host build compiles again 2026-10-30 07:32:39 +01:00
Niko1221 87970c8ae0 Merge f-029-par-metal (766b54afb) 2026-10-30 07:32:39 +01:00
Niko1221 777d11f8a3 Merge f-029-par-amd (4772f0cd9) 2026-10-30 07:32:39 +01:00
Niko1221 dcc79c5230 Merge f-029-par-harness (9b5965c3c) 2026-10-30 07:32:32 +01:00
Niko1221 db0606b4f8 Merge f-029-par-big (714438ec1) 2026-10-30 07:32:31 +01:00
Niko1221 4bfd66686e Merge f-029-gdn-mx (348457223) 2026-10-30 07:32:31 +01:00
Niko1221 5c00af8a1f Merge f-029-gdn-amd (f0a34fa9b) 2026-10-30 07:32:31 +01:00
Niko1221 78859bbcb1 Merge f-029-gdn-cuda (51567de1a) 2026-10-30 07:32:30 +01:00
Niko1221 9b5965c3c1 z_ctx 2026-10-30 07:32:03 +01:00
Niko1221 836dd2d86e sweep: server flags column; stacked-candidate variant lists 2026-10-30 07:21:57 +01:00
Niko1221 6d7255f205 z_pool 2026-10-30 07:20:46 +01:00
Niko1221 97bbd76c92 tools/par/overhead.sh (counters overhead A/B) 2026-10-30 07:20:14 +01:00
Niko1221 36e798f4c5 Opt-in research switches: STRATA_PAR_COUNTERS, STRATA_PIPED_CONCURRENT_MIN, multi-prompt layer-major read (Prefill::run_multi, BCO co-reads, server STRATA_COALESCE_PREFILL), STRATA_CRASH_BT 2026-10-30 07:19:55 +01:00
Niko1221 1510e6d6d2 tools: parallelism load harness (par_load, par_analyze, par_run, sweep), E00 capability probe and the phase-1/2 variant lists 2026-10-30 07:19:55 +01:00
Niko1221 4772f0cd9a co-read adaptive: look 10 ms for the burst's other arrivals before deciding 2026-10-30 06:21:21 +01:00
Niko1221 7aafac422c shared arena on Windows: a named pagefile-backed section (CreateFileMapping/MapViewOfFile) with the same header and load state word, pid election on both OSes, server option enabled, pinned_shared_win_test 2026-10-30 06:02:30 +01:00
Niko1221 62b042e843 co-read: adaptive - the leader waits and the read is shared only when the engine has seen at least 3 requests arrive together (STRATA_COALESCE_MIN_REQS, STRATA_COALESCE_BURST_MS) 2026-10-30 05:58:22 +01:00
Niko1221 9d8337844e replicas: the memfd arena warns when the RAM will not hold all the engines 2026-10-30 05:24:08 +01:00
Niko1221 714438ec19 GDN TC: two value tiles per head on cards with >= 96 SMs (STRATA_GDN_TC_TILES), harness --tiles 2026-10-30 05:12:40 +01:00
Niko1221 d3af2d42ad GDN: single-pass conv+SiLU+L2 (STRATA_GDN_CONVL2=2), output-only reduced-precision experiment mode (STRATA_GDN_TC_NP=2) 2026-10-30 05:12:40 +01:00
Niko1221 1a865fb3db GDN TC: fused output norm for the split scan, NP experiment variants (STRATA_GDN_TC_NP), STRATA_GDN_TC_FUSE 2026-10-30 05:12:40 +01:00
Niko1221 a0e7f5160d GDN harness: tc any_sms bypass for multi-wave tests, output hash 2026-10-30 05:12:40 +01:00
Niko1221 dc149ef003 GDN: production-linked parity/bench harness, dispatch probe, FP64 fixtures, contract doc; opt-in STRATA_GDN_TC on 99-KB cards (split v/T/P buffering) 2026-10-30 05:12:40 +01:00
Niko1221 9f1d0ad041 replicas: a replica that fails to start beside a living one is started again on the same shared arena before the sharing is given up 2026-10-30 05:07:50 +01:00
Niko1221 1064ff7783 STRATA_COALESCE_WAIT_MS 2026-10-30 04:52:38 +01:00
Niko1221 301a09f100 STRATA_PREFILL_LM_FORCE (research reference for the multi-prompt read) 2026-10-30 04:52:38 +01:00
Niko1221 4f88a2fa8b multi-prompt read: relaxed stage-chunk rule, sub-chunks at the smallest stage chunk; gated identity variants 2026-10-30 04:52:37 +01:00
Niko1221 1f05fdc283 [Engine] CUDA hyper-connection read (v3): the transposed warp sums also for 5..7-token launches 2026-10-30 04:52:35 +01:00
Niko1221 a8fba1f5b7 multi-prompt PLE: tables, constants and weights from the main session, the history of the prompt's own 2026-10-30 04:52:32 +01:00
Niko1221 2f0d6a1c15 replica profile (opt-in): setup --replica-profile picks one engine per card + shared expert arena + the host stack when the RAM and VRAM check passes 2026-10-30 04:51:14 +01:00
Niko1221 42aee7a349 [Engine] CUDA hyper-connection read (v3), 8-token launches: the eight warp sums in one transposed butterfly (same additions, same bits; decode c=8 +2%) 2026-10-30 04:47:30 +01:00
Niko1221 3782008944 shared arena: memfd backing (not limited by the /dev/shm size), one engine reads the experts and the others skip the read, RAM check before the pages are taken 2026-10-30 04:46:51 +01:00
Niko1221 05cd12e7a2 [Engine] batch window: STRATA_BATCH_GDN_GROUPS now also works with STRATA_BATCH_SELF_COMMIT (the grouped GDN launch keeps its one-row groups itself) 2026-10-30 04:19:00 +01:00
Niko1221 0c94851835 shared arena: the header is written before any engine reserves pages, an importer waits for a header still being written, and every engine reserves the tmpfs pages 2026-10-30 04:13:24 +01:00
Niko1221 21f950b4b4 [Engine] CUDA hyper-connection read (v3): exact 5..8-token down kernels with two rows per warp (same dots, same bits; decode c=8 +5%) 2026-10-30 03:56:23 +01:00
Niko1221 3ccfd5aed3 replicas: opt-in shared expert arena (one /dev/shm file for all replicas, stale-file sweep, private fallback); the engine reserves a tmpfs arena up front 2026-10-30 03:50:58 +01:00
Niko1221 51567de1a6 GDN TC: two value tiles per head on cards with >= 96 SMs (STRATA_GDN_TC_TILES), harness --tiles 2026-10-30 03:46:18 +01:00
Niko1221 f0a34fa9bf kGdnW12MinT comment: the 8K figure 2026-10-30 03:46:05 +01:00
Niko1221 84aa2d7d57 GDN gfx12: chunked path only from calls of 12000 tokens (measured crossover for the +5% bar); p2006/KL scripts 2026-10-30 03:45:42 +01:00
Niko1221 1d9b4c48e9 prefill: STRATA_LM_HIP=1 opts the layer-major (and multi-prompt) read in on HIP builds 2026-10-30 03:21:45 +01:00
Niko1221 2072510a47 [Engine] --batch on one GPU: opt-in concurrent prompt read beside the decoding slots (STRATA_BATCH_CONCURRENT_READ) 2026-10-30 03:10:51 +01:00
Niko1221 0ea3e455fd GDN: single-pass conv+SiLU+L2 (STRATA_GDN_CONVL2=2), output-only reduced-precision experiment mode (STRATA_GDN_TC_NP=2) 2026-10-30 03:05:51 +01:00
Niko1221 a9f154e82b run_multi: layer-major even for a short total 2026-10-30 03:02:51 +01:00
Niko1221 766b54afb1 [Engine] Metal: --batch N / "parallel": N runs, and the slot rows share one window
- the pdl capability queries answer false instead of aborting the engine (#3981, #2715)
- each batch slot keeps its own running state in the Metal window (GDN state and conv history, QSA K/V, indexer tail and pooled keys,
  PLE history), side by side in bigger buffers; the slot copy, the checkpoints and session_zero follow the session they are for
- a batch window holds one token of each active slot: the GDN step, the QSA kernels and the PLE conv take a per-row slot, cell and
  history parity (function constants, off for every other caller), so the dense and expert weights are read once for all rows
- STRATA_METAL_BATCH_ROWS=0 runs the slots one window each instead (diagnosis)
2026-10-30 02:58:46 +01:00
Niko1221 faa1be268c co-read: server side (STRATA_COALESCE_PREFILL), par_load --keep-text, cmp_texts 2026-10-30 02:53:32 +01:00
Niko1221 47a1047750 WIP multi-prompt layer-major prefill (Prefill::run_multi) and BCO co-reads in the batch admission 2026-10-30 02:53:25 +01:00
Niko1221 ee05114846 GDN TC: fused output norm for the split scan, NP experiment variants (STRATA_GDN_TC_NP), STRATA_GDN_TC_FUSE 2026-10-30 02:12:18 +01:00
Niko1221 d8c94b45c8 [Engine] CUDA prompt path: opt-in FP16 cache of dequantized dense weights (STRATA_PF_W16_CACHE_MIB) 2026-10-30 02:11:45 +01:00
Niko1221 236adb3987 GDN gfx12: conv + SiLU + q/k norm written as FP16 and read by the chunked kernels (one entry, STRATA_GDN_FUSE=0 off); ablation hooks removed 2026-10-30 01:51:05 +01:00
Niko1221 3484572236 Metal: tensor-op chunk-state probe, layer bench with before/after, A/B driver 2026-10-30 01:50:15 +01:00
Niko1221 e0cc4e859e GDN harness: tc any_sms bypass for multi-wave tests, output hash 2026-10-30 01:47:46 +01:00
Niko1221 cea3057e2c Metal: GDN chunked v2 (measured, slower), M5 GEMM tiles + fused GDN input projections (bit-identical), layer bench 2026-10-30 01:42:07 +01:00
Niko1221 5e9995d296 GDN gfx12: LDS-only barrier in the chunked scan; conv + SiLU + q/k norm folded into the chunked kernels (opt-in STRATA_GDN_CHUNKED=1, STRATA_GDN_FUSE=0 off), bench check mode 2026-10-30 01:33:37 +01:00
Niko1221 095464152c Metal: GDN prompt scan chosen per chip (M4 scanr_4_8, M5 Max scanr_4_4 from T 4096), bit-identical 2026-10-30 01:24:05 +01:00
Niko1221 2cf3fd181f Metal: gdn_scan_f (scan + gdn_post fused, bit-identical, slower: kept as a measured negative), bench 2026-10-30 01:22:28 +01:00
Niko1221 06f212aefe GDN: production-linked parity/bench harness, dispatch probe, FP64 fixtures, contract doc; opt-in STRATA_GDN_TC on 99-KB cards (split v/T/P buffering) 2026-10-30 01:12:19 +01:00
Niko1221 4d9d9cefd3 GDN: the chunked WMMA recurrence on gfx12 (opt-in STRATA_GDN_CHUNKED=1), gfx12 fragment layout 2026-10-30 00:59:13 +01:00
Niko1221 f34ab1c8f5 GDN gfx11 WMMA: compile the device pass for gfx1103 too (the runtime gate lists it; the pass trapped there) 2026-10-30 00:55:55 +01:00
Niko1221 280885fda0 Metal: GDN prompt scan benchmark (sequential vs chunked), extra gdn_scanr TB instantiations, opt-in STRATA_METAL_GDN_SCAN 2026-10-30 00:49:57 +01:00
Niko1221 ae2657a19d gdn_stage_bench: fix auto deduction 2026-10-30 00:42:26 +01:00
Niko1221 d4f15adf87 tests/hip: gdn_stage_bench, the GDN prompt stage (gates, conv, recurrence) per piece with output and state hashes 2026-10-30 00:39:06 +01:00
Niko1221 dbdec0bb9e Control 0.1.55 and Terminal 0.2.8.1 (Control-only release: the app stays 0.2.8, the engine packages 0.1.54) 2026-10-30 00:32:32 +01:00
Niko1221 d7cb41ba87 DeepSeek local GGUF: accept both layouts of the IST-DAS sets, the pinned four-shard one and ISTA-DASLab's six-shard one (owner decision, option B)
The download default stays on the four-shard pin cb02422c; nothing is fetched under the new names. The six-shard files (Hub commit a419d74c,
27 October: five shards a set, the Engram as the sixth) and the four-shard files with the first shard ISTA re-published on 26 October (what the
Hub's last four-shard commit 653a9874 serves; #4237 was refused by the app because only the pinned size of shard 1 was known) are taken from
files already here, by size and SHA-256.

- setup.py: DSV41_ISTA6_FILES (15 files), shards6 on the three sets, dsv41_layout_of / dsv41_parts / dsv41_pack_layout; install_dsv41 picks the
  layout of a --gguf-dir folder (both whole: the pinned one), packs the five files with the Engram as --engram, and builds the pack again when it
  was made from the other layout's files. The converted Engram (same name and size, another hash) is accepted beside the pinned one.
- strata_control/dsv41_sets.py, models.py: the app's check and `strata models import` know the six-shard names, the re-published shard 1, and
  that a folder with both whole layouts of one set is that set once.
- strata_cli/launch: --model-path and the guided screen name what is missing in the layout that is there.
- tools/dsv41/pack_gguf.py: --engram as the sixth shard of a six-shard split; the pinned release's Engram serves the six-shard shards too.
- Tests for each; docs/DEEPSEEK41.md.

Builds on the cherry-picks of #4348 (integer tensors in the GGUF reader) and #4632 (StackArc3's six-shard work, option A), which this
commit reworks so the four-shard pin and files keep working unchanged.

Related: #4237, #4256, #4632.
2026-10-30 00:32:32 +01:00
StackArc3 c4e975cc7a Accept ISTA-DASLab's six-shard DeepSeek V4.1 Flash GGUF layout
On 27 October 2026 ISTA-DASLab replaced the four-shard IST-DAS builds on
the Hub's main with six-shard builds (llama.cpp b11870+ layout), and
--model-path with those files failed with "...-00001-of-00004.gguf is
missing". The v0.2.7.1 pin still serves the old files.

- Pin a419d74c: five shards per build named of-00006 and the shared
  Engram as the sixth. The Engram was converted in place (same name and
  size, new hash).
- pack_gguf.py counts an --engram file as the sixth shard only if it is
  one file, not already among the shards, number count-1 of the same
  split, and holds the single tensor per_layer_token_embd.weight.
- An Engram whose finish mark names an old hash is fetched again in the
  same run. One whose mark has no hash is hashed: a match is kept and
  marked, a mismatch is fetched again. A user's own old one is refused
  with the reason.
- A pack from an earlier run is rebuilt unless it names this run's files.
- Six-shard only; four-shard files are ignored and stay on disk. The
  first-release name tables are now empty. No engine code changes.

Related: #4237, #4256.
2026-10-30 00:16:56 +01:00
lu123bing c15a90b28c gguf reader: size the integer tensors, so a complete DeepSeek first shard is accepted and a cut one refused
BLOCK_GEOMETRY had no row for I8/I16/I32/I64, which GGML_TYPES names and which IST-DASLab's DeepSeek-V4.1
first shard uses for the Engram hash parameters (tools/dsv41/pack_gguf.py). The preflight in front of setup
and the app reads "no geometry" as "unsupported file" (strata_control/gguf_preflight.py:165), so a complete
file was refused as "cut short or another version" or as "a format Strata does not read" (#4256).
setup.check_shards sized such a tensor at 0 bytes (setup.py:3324), so a shard cut inside one passed as whole.

The rows are the ones tools/dsv41/ista_intake.py:112 already uses for these types.
2026-10-30 00:08:56 +01:00
Niko1221 870aedd9e1 [Engine] SYCL: the prompt attention batch is budgeted where it is allocated, and stays 32 by default
The scratch of the FP32 prompt attention is batch x chunks x n_head x 258 floats (about 0.41 GB at 32 and 1.65 GB at 128 on
Flash-Next), independent of the prompt chunk.  Prefill::init allocated it from STRATA_ATTN_BATCH (128 since 09524eb47) while the
estimate that sizes the expert cache and the prompt path's loan (bytes_needed_impl) still counted 32, so about 1.2 GB was never
reserved and a single 32 GB Arc Pro B65 with the config setup writes failed its first request ("device buffers for a chunk of 512
tokens do not fit (2098 of 32656 MiB free)").  Both now use one function (attn_batch_for), and the batch is capped at the chunk's
token count, since a chunk never runs more queries than it has.

The default goes back to 32: with the budget honest, 128 would take 1.2 GB from the expert cache on every Intel card for a
1.1-1.3% faster prompt on two B65s.  STRATA_ATTN_BATCH still sets it (1..1024), and is now counted.
2026-10-29 18:13:26 +01:00
Niko1221 36f6663c25 Version 0.2.8: Terminal and app 0.2.8, engine and Control 0.1.54 (every Qwen and DeepSeek package, Metal and SYCL included) 2026-10-29 18:04:23 +01:00
Niko1221 1bf946eafa Merge f-028-monitor: the Monitor keeps the app's own GPU readings where the engine has none (AMD on Windows), reads Windows AMD temperature, and drops non-GPU display chips on Linux (#3727) 2026-10-29 18:04:10 +01:00
Niko1221 5e8f47cfff Monitor: AMD cards keep their readings while an engine runs, Windows AMD temperature, no remote-console chip as a GPU (#3727) 2026-10-29 17:33:56 +01:00
Niko1221 18200cffaf Merge f-028-arcb65-pipe: pipelined B-series (two Battlemage stages) is the default 2026-10-29 17:25:33 +01:00
Niko1221 cf059188ca Merge f-028-arcb65: SYCL lane-cell prompt attention for Battlemage only, and the Arc B-series work 2026-10-29 17:22:06 +01:00
Niko1221 b119f40f08 window_limits_test: setenv/unsetenv for MSVC (the Windows test build stopped on it) 2026-10-29 17:22:02 +01:00
Niko1221 8d5be82566 docs: two Arc B-series cards decode with the pipelined windows by default
INTEL_ARC.md: one bullet with the numbers (2x Arc Pro B65: decode +17% to +23% over the serial --spec 4 setup, 5 pairs of 5),
what stays as before, and the two switches; one row in the tested cards table.  MULTI_GPU.md: the Default line of
"Pipeline windows" now names the Intel Arc case beside AMD RDNA4 and NVIDIA.
2026-10-29 16:53:58 +01:00
Niko1221 74e1f86442 Setup and the launcher write --spec 2 for exactly two Arc B-series cards
On two Battlemage cards the engine pipelines the two cards' verify windows by default, and then answers as a 2-token window
whatever --spec says (pipelined --spec 4 equals serial --spec 2 in 100 of 100 agent requests; serial --spec 4 differs in 3).
So the generated config says --spec 2 there: the same answers, and about 20% faster than the old --spec 4.

strata_control/intel_spec.py holds the rule: the cards are found by PCI device id (the xe driver and an id in 0xE200-0xE2FF,
the id the engine's architecture enum comes from), never by name.  It changes only setup's own default "--spec 4", only
with the draft layer on, and only for exactly two such cards.  A --spec in "user_args", any other value, one card, an
A-series card in the pair, three cards and "mtp": false are left alone.  Applied in three places: sycl/setup_intel.py when it
writes the run config of a layer split (the copy in "args_written" follows, so a rerun does not take it for a hand edit),
the launcher's derive_config (before the person's --set options, which win), and the Intel server's engine_args at every
start (a config an older setup wrote).  Tests in tests/sycl/test_intel_two_card_spec.py.
2026-10-29 15:52:37 +01:00
Niko1221 a6fd788567 [Engine] SYCL: --pipeline-windows 2 is the default on exactly two Arc Battlemage cards
When --pipeline-windows is not given, the engine turns it on for --serve with a layer split of exactly two stages whose
GPUs are both Battlemage (Xe2) cards by architecture id (strata::intel_gpu_gen), with the MTP drafter (--spec 2 or more),
no --batch, no --peer-device and no helper caches.  A single card, an Arc A-series card, a mix, three or more stages
and any other vendor stay serial.  --pipeline-windows N and STRATA_PIPELINE_AUTO=0 override it, and one log line says it
is on.  The condition is a host-only function (strata/pipeline_auto.hpp) with a test that needs no GPU.

Measured on 2x Arc Pro B65: a 1 h mixed serve (1,082 requests) with 0 errors, 0 token differences against the serial run
of the same requests and flat VRAM; decode +17% to +28% over the serial --spec 4 default.  The pipelined windows answer as
a 2-token window, so setup and the launcher write --spec 2 for such a config (next commit).
2026-10-29 15:52:37 +01:00
Niko1221 32f0c44f08 Merge f-028-par 2026-10-29 15:11:01 +01:00
Niko1221 93dd9b421d Merge f-028-macturns 2026-10-29 15:11:00 +01:00
Niko1221 92a867be17 Merge f-arc-cpumsg 2026-10-29 15:11:00 +01:00
Niko1221 a3d6298297 Merge f-pr4366 2026-10-29 15:10:59 +01:00
Niko1221 b82d1d4567 Merge f-028-tools-p1 2026-10-29 15:10:59 +01:00
Niko1221 3b8dc8fa35 Merge f-028-app-p1 2026-10-29 15:10:59 +01:00
Niko1221 d76b50681a Merge f-028-nerazim: DeepSeek stop ids (test_dsv41 stop-id assertion follows the fix; the arch_from_tokenizer cherry-pick is eng-p1's) 2026-10-29 15:10:55 +01:00
Niko1221 77287f13ec Merge f-028-eng-p1: engine P1 fixes 2026-10-29 15:10:41 +01:00
Niko1221 95563ff292 Merge f-028-defaults2: route prior off by default, tool_call_recovery on 2026-10-29 15:10:41 +01:00
Niko1221 5c8bcf4307 docs: windows of 16 rows and the four-card AMD profile (BATCHING.md)
Which builds take 16 rows and why, how the 8-row build stays main's code, what was tested (real 16-row windows,
identity on seven boxes), the four-card profile with its measured effect per setting, and the opt-out.
2026-10-29 15:06:10 +01:00
Niko1221 4fdb563a34 Four-card AMD serving profile: 32 slots in 4 groups, split 13,26,39, chunk 2048 (setup and launcher)
For exactly four Radeon AI PRO R9700 cards (at least 28 GB each, hip, no --peer-device) running Qwen3.8-Flash-Next
IQ3_S, setup.py and the launcher fill in what the person has not chosen: parallel 32 with --batch-groups 4, the layer
split 13,26,39 (the output head costs about four layers on the last card), --prefill 2048 (in place of the "auto" setup
writes), and STRATA_QSA_SLOTS, STRATA_BATCH_SELF_COMMIT and STRATA_SLOT_COPY_DIRECT in the run config's env. A setting
of the person's own always wins; STRATA_FOUR_CARD_PROFILE=0 or "four_card_profile": false turns it off. The prompt cache
stays on (agent follow-up turns wait 0.8 s for the first token instead of 1.5 s).

Through the launcher's configuration on the 4 x R9700 box (total tok/s, warm round, parallel 4 before): 1 request 66.6 ->
67.9, 8 at once 156.6 -> 261.2, 32 at once 155.4 -> 369.6, four 7.9K prompts out 24.3 -> 35.0 and in 1892 -> 2706.
batch_test with the profile's flags: 32 of 32 slots identical to solo; a 25 minute soak of 32 clients: 0 errors.

serve/server.py no longer says "at most 8 requests" for a layer split (each pipelined group holds its own slots).
Tests: tests/terminal/test_amd_four_card.py.
2026-10-29 15:06:10 +01:00
Niko1221 9d3c8c3664 Verify and batch windows of up to 16 rows: HIP builds default to 16, narrow builds unchanged
A pipelined group of a layer split is its own window, so 32 slots on four cards were four groups of 8 and nothing
larger could run. The window limit is now a build option (STRATA_WINDOW_MAX_T = auto | 8 | 16; auto is 16 for HIP,
8 for CUDA; SYCL and Metal never set it): include/strata/kernels/window_limits.hpp.

A window of up to 8 rows runs exactly the kernels it ran before (HIP gfx1201 window 8 against main: 3096 of 3097
kernels identical, the other a debug variant; CUDA sm_86 and sm_60: identical or renamed but for a few prefill kernels
NVVM compiles differently, with identical results). The code for 9 to 16 rows is only reached by a wider window:
- per-row arrays of the verifier, the expert source tables (128 / 256 entries) and the remote / peer expert tables follow
  the window; CPU expert jobs stay at 8 tokens (a 9..16-row group is two jobs over the same expert);
- the hyper-connection read is made in slices of 8 rows (its Q8_0 launch would otherwise skip rows);
- the grouped expert kernels run a second pass (SKIP template argument) instead of a 16-entry shared array (51 KB,
  over the 48 KB static limit of CUDA);
- resident_plan runs on 128 threads (up to 8 rows, as it was) or 256; MMVQ, bf16 GEMV, shared expert and MoE combine
  take 9..16 columns only in a wide build; the sm_60 packed path runs wide calls in passes of 8;
- STRATA_P4_TIMING and STRATA_P4_TRACE (include/strata/program/pipe_probe.hpp): opt-in timing and trace of the
  pipelined windows, off unless set.

Tests: src/core/window_limits_test.cpp (target window_limits_test). Real 16-row windows (--batch 64 on four stages,
--batch 16 --batch-groups 1) decode the tokens they decode alone.
2026-10-29 15:06:02 +01:00
Niko1221 061c542010 Metal DeepSeek: stop leaking Metal buffers and command buffers (16 -> 3 tok/s after a few requests)
The engine's host threads have no autorelease pool, and Metal parks one extra
reference per new MTLBuffer, command buffer and encoder in the calling thread's
pool.  Nothing drained it, so every buffer Context::alloc made (a 16 KB job
array per expert matvec, ~90K a request) and every stream command buffer and
encoder stayed alive after free(): +1.3 GB, +90K Metal regions and a slower
queue per request, until a big Mac slowed to a crawl.

- Context::alloc and the fallback wrap: create the buffer inside an
  autoreleasepool (DS4 runtime only; the Qwen build is untouched).
- strata::gpu runtime: a pool around command-buffer, compute-encoder and blit
  creation; the compute encoder is retained and released in end_encoder.
- Event marks: an event recorded again, or destroyed, before it was waited on
  now drops its command buffer token (ds4_ctx_drop / Context::drop_mark).
- route, combine and sparse_attn free the stand-in buffers they allocate for
  absent inputs.
2026-10-29 13:16:33 +01:00
Niko1221 f2bf7a3ce3 [Engine] SYCL: the lane-cell prompt attention default is for Battlemage (Xe2) cards only
On an Arc A750 the lane-cell score phase read a 4,106-token prompt 0.7% slower than the unchanged kernel (three
interleaved runs against the published 0.1.52 engine; with STRATA_ATTN_LANECELL=0 the branch matched it), while it is
+5% on the Arc Pro B65.  The default now follows strata::intel_gpu_gen(): the runtime's architecture enum first, the
device name only as a fallback.  STRATA_ATTN_LANECELL=1/0 still forces either way on any device.
2026-10-29 12:47:50 +01:00
Niko1221 6124c35e39 [Engine] SYCL: native_expert_bench reports microseconds, not milliseconds
Problem: the timing lambda measured milliseconds per iteration and returned them, while the printout labels the value
"us" and derives its GB/s as `bytes / us / 1e3` - so every number this bench has printed was 1000x too small in time
and 1000x too large in bandwidth.  A launch that moves 19.7 MB in ~140 us was reported as 0.1 us and 148,713 GB/s, which
is why the tool looked broken rather than mislabelled.  The wall clock always agreed with the real work (200 iterations
618 ms, 2,000 iterations 3,566 ms).

Result, 2x Arc Pro B65, shard 1 (Qwen3.8-Flash-Next IQ3_S experts, 10 groups), before and after the one-line fix:

| case | before | after |
| --- | ---: | ---: |
| 2 tokens | 0.1 "us", 148,713 "GB/s" | **143.4 us: gate/up 84.5 us (148 GB/s), down 59.4 us (78 GB/s)** |
| 8 tokens | 0.3 "us" | **340.8 us: gate/up 217.4 us (58 GB/s), down 126.0 us (37 GB/s)** |

and the model's own layers at two tokens: layer 30 iq3_xxs/q2_0 143.6 us, 31 iq3_xxs/iq4_nl 140.4 us, 32 iq2_s/iq4_nl
135.5 us, 33 iq3_xxs/iq4_nl 140.2 us - gate/up 131-149 GB/s, down 78-168 GB/s, results bitwise equal to the reference in
every case.

Approach: return microseconds (`ms * 1e3 / iters`), which is what the printout, the minimum-of-three logic and the
bandwidth formula already assume.  No kernel, no timing path and no measurement logic changed.

Validation: the same run before and after; the wall-clock scaling that exposed the bug; bitwise equality retained in
all four model layers and both token counts.  Not run: CUDA/HIP (this file is the port's own), other shards.

Limits: this fixes the units only.  The numbers are per-call costs with the bench's own synthetic groups, so a rate taken
from it still has to be matched to the real window (48 calls per layer step).
2026-10-29 12:45:36 +01:00
Niko1221 8475011ac1 [Engine] SYCL: native_expert_bench refuses to time a launch that wrote nothing
Problem: the bench fills its output with 0xff and then compares the mode against the reference, so a launch that
returns without enqueueing leaves both sides bitwise equal on an untouched buffer and the tool reports "0 differing"
and a 0.1 us timing - a measurement of nothing that looks like a very fast kernel.  The 0xff fill is NaN as a float, so
"no finite value in the reference window" is exactly that case.

Result, 2x Arc Pro B65, shard 1 layer 20 (`iq3_xxs/q2_0`, 10 groups, 2 tokens): the guard does *not* fire - the window
does write finite values, so this defect is not what makes the timing wrong.  The timing is still 0.1 us per launch
against a 19.7 MB working set (0.3 us at 8 tokens), which scales with work but is 20-60x too fast for the launch to be
waited for; the remaining cause is in the timing loop's wait, not in the launch.  That stays an open port item.

Approach: the guard only refuses and reports; it changes no timing path and no kernel.

Validation: builds and runs on the box; the run above is the only one taken.  Not run: CUDA/HIP (this file is the port's
own), and the timing itself is still unproven - do not use its numbers.

Limits: this is a diagnostic, not the fix.  `native_expert_bench` numbers remain unusable until the wait is found.
2026-10-29 12:45:36 +01:00
Niko1221 89253935db [Engine] SYCL: refuse a --mtp-q4 draft head when the Q6_K head is stored as planes
Problem: `--mtp-q4 head` builds the Q4_0 draft head by gathering the subset and dequantizing it row by row, but with
`STRATA_Q6_PLANAR` (the SYCL port's default for Q6_K heads) the source is stored as 16-byte-aligned planes and the
gather copies planes as if they were rows.  The result is a draft head that proposes nothing at all, with no error:
decode falls from 73.8-75.6 tok/s to 35.3 tok/s (the non-speculative rate) and the DONE line reports "0 of 0" drafts.

Result, 2x Arc Pro B65, Qwen3.8-Flash-Next IQ3_S, 128 tokens of a 483-token chat prompt, one warm request each:

| configuration | decode | drafts |
| --- | ---: | --- |
| `--mtp-q4 head`, planar head (before) | 35.3 tok/s | 0 of 0 |
| `--mtp-q4 head`, planar head (after) | refused with "needs the packed Q6_K head layout (STRATA_Q6_PLANAR=0)" | - |
| `--mtp-q4 head` with `STRATA_Q6_PLANAR=0` | 69.5 tok/s | 54 of 63 |
| default head (no `--mtp-q4`) | 74.9 / 75.6 tok/s | 55 of 63 |

Approach: a guard in `make_q4_head` that refuses the option when the head really is planar, so the engine says why
instead of running with speculation silently disabled.  The packed-layout path is unchanged and still works, which is
what the measurement above shows.

Validation: the guard message appears in the engine's own output; `--mtp-q4 head` with the packed layout still produces
54 of 63 drafts; the default path's tokens are identical to the pre-change reference (`compare_tokens.py`, no
difference) and its rate is unchanged.  Not run: CUDA/HIP (the file is the port's own copy and the guard is behind
`STRATA_NATIVE_EXPERTS`), a non-Q6_K head, `--batch-mtp`.

Limits: this makes a broken opt-in fail loudly; it does not make the 4-bit head fast.  With the packed layout it is
*slower* than the Q6_K head (69.5 against 74.9), so the option is still not worth enabling.
2026-10-29 12:45:36 +01:00
Niko1221 e318f71e75 [Engine] SYCL: copy the prompt path's grouping tables instead of reading them from mapped memory
Problem: the prompt path's per-layer grouping tables - `[ids | slot | src]`, T x K int32s each, ~245 KB per layer at a
2,048-token chunk - live in mapped pinned host memory, and on the mapped arm the GPU reads them there, once per layer.
On a PCIe-attached pair that is 48 rounds of host-memory reads per chunk, and the port already carried the A/B arm to
copy them instead (`STRATA_GROUP_COPY=1`) without a measurement deciding between them.

Result, 2x Arc Pro B65, Qwen3.8-Flash-Next IQ3_S, `--pipeline-windows 2`, three warm repetitions, same build, back to
back:

| case | mapped arm | copy arm | gain |
| --- | ---: | ---: | ---: |
| prefill 4,106 tokens (AOT) | 1,303.2-1,303.5 tok/s | **1,332.8-1,337.5** | +2.5% |
| prefill 16,380 tokens (AOT) | 1,617.8-1,622.7 | **1,657.2-1,665.2** | +2.5% |
| prefill 10,938 chat prompt (AOT) | 1,575.6-1,578.4 | **1,613.8-1,616.8** | +2.5% |
| decode, chat prompt, 128 tokens | 74.9 / 75.6 | 74.9 / 75.6 | unchanged |

Approach: the default flips to the copy arm for this port; `STRATA_GROUP_COPY=0` restores the mapped one.  Both arms
allocate nothing new when the buffer is already big enough, and the copy arm reuses the host vectors the code already
keeps for exactly this.

Validation: the token stream is identical between the two arms at both prompt sizes and on the decode run
(`scripts/compare_tokens.py`: first difference none), and identical to the pre-change reference (the rc-p16k run of
round 42).  Not run: CUDA/HIP (the file is the port's own copy), a host-shared-memory device (an iGPU, where the mapped
arm may be the better one - hence the switch), a 32K prompt.

Limits: +2.5% on prefill only; the decode path does not use these tables.
2026-10-29 12:45:36 +01:00
Niko1221 bf8d88b9dd [Engine] SYCL: a pipelined window never self-commits, so --pipeline-windows 2 is token-exact
Problem: `--pipeline-windows 2` (the two-card window overlap) ran but produced different tokens from
`--pipeline-windows 1`.  Ten rounds of switches, splits and modes narrowed it to the two alternating verifiers, and a
state dump (STRATA_PIPELINE_DUMP: a hash of the first stage's GDN recurrence and conv history after every window)
found the moment: identical after the prompt, different after the first decode window.

Root cause: the serial path lets a one-token window advance the GDN state inside the window and skips the commit
graph for it (`record_window` + `commit`).  That is safe there because a serial window never runs before its
predecessor's verdict.  A *pipelined* window may: stage 0 runs window K+1 speculatively while window K's verdict is
still open, so a self-commit lands on state that is not final yet - and the first decode window of every request is a
single token.

Fix: a per-verifier switch (`set_self_commit_off`) that the pipelined verifiers set.  With it `record_window` does not
self-commit and `pl_commit_async` always queues the commit graph - the configuration that already measured
token-identical (`STRATA_ONE_TOKEN_COMMIT=0`) but kept a global switch.

Result on 2x Arc Pro B65, Qwen3.8-Flash-Next IQ3_S, same build, back to back:

| case | pw 1 | pw 2 | tokens |
| --- | ---: | ---: | --- |
| short prompt, 256 tokens, spec 4 | 56.3 / 56.1 tok/s | **59.6 / 61.4** | identical |
| 4K context, 128 tokens | 55.6 | **60.3** | identical |
| `--spec 2 --spec-min-p 0`, 64 tokens | 49.2 / 50.3 | **59.6 / 63.2** | identical |
| 512 tokens, one run | - | **60.6** | - |
| prefill 4,106 tokens | 1,244.8 / 1,242.0 | 1,253.8 / 1,251.3 | identical |
| prefill 16,380 tokens | 1,548.1 | 1,561.1 | - |

So decode gains 7-9% at the default window and 21-26% at two-token windows, and the prompt path is unchanged or
marginally faster.

Validation: tokens identical in all four decode cases above (`scripts/compare_tokens.py`); the serial path's tokens are
unchanged against the pre-change reference.  The state dump still reads one window behind in the pipelined arm (an
ordering artifact of where it copies), so the token comparisons are the evidence, not the dump.
Not run: CUDA/HIP (the files are the port's own), a 32K-context prompt, an overnight soak.

Limits: `--pipeline-windows 2` stays opt-in.  It costs 278 MiB of card 0's expert cache (the second verifier and the
GDN snapshots) and its gain depends on the window size, so the default remains `--pipeline-windows 1`.
2026-10-29 12:45:35 +01:00
Niko1221 7356aa1404 [Engine] SYCL: print the pipelined commit's count when a state dump is requested
The pipelined state dump showed the first multi-token window leaving the GDN state one row ahead of the serial path,
so the next question was whether the commit count was wrong.  With STRATA_PIPELINE_DUMP set, `pl_commit_async` now
prints `n_keep`, `T`, `pos0` and whether the one-token skip applies, which answers it: the counts are right.

```
commit n_keep=1 T=1 pos0=18 self_skip=1      (stage 0)
commit n_keep=1 T=1 pos0=19 self_skip=1
verdict p=19 T=1 a=0 B none
commit n_keep=1 T=1 pos0=19 self_skip=1      (stage 1)
commit n_keep=2 T=2 pos0=20 self_skip=0      (stage 0)
commit n_keep=2 T=2 pos0=20 self_skip=0      (stage 1)
verdict p=20 T=2 a=1 B launched ON
```

So the multi-token window commits two rows in both stages, exactly as the verdict says, and the extra row must come
from the *window's own* self-commit: `record_window` self-commits when `T == 1 && !batch_rec_ && !g_qfuse() &&
one_token_self_commit()` (verify.cpp:844), and the transition from a self-committed single-token window to the first
graph-committed window is where the state gains the row.  That is the next thing to diff - the dump currently hashes
the recurrence and the conv history together.

Debug only: the print needs a state dump to be requested and changes no behaviour.
2026-10-29 12:45:35 +01:00
Niko1221 11d337537c [Engine] SYCL: the pipelined commit skips the graph for a one-token window
Problem: the serial commit knows that a one-token window advances the GDN state itself (`record_window`) and skips the
commit graph for it; `pl_commit_async`, which the pipelined windows and the pipelined prompt reader use, ran the graph
anyway.  A one-token window therefore advanced the state twice.  The first decode window of a request is always one
token, which is why `--pipeline-windows 2` produced different tokens from `--pipeline-windows 1`.

Evidence: the state dump added in the previous commit (STRATA_PIPELINE_DUMP, hash of the first stage's GDN state after
every window) was identical for both arms after the prompt (`pos 18 sum 1952.13774 hash b97305876cf73d2c`) and
different after the first decode window (`pos 19` 3222.063 in the pipelined arm against 2815.93065 serial).  With
`STRATA_ONE_TOKEN_COMMIT=0` - which makes the serial path take the graph as well - the two arms produced **identical
tokens**, which pinned the asymmetry to this one branch.

Result: with the skip, the two paths' states agree after the first and second windows (`pos 19` and `pos 20` hashes
equal) and the tokens are identical up to and including token 5; the first difference moved from token 4 to token 6.

Validation: `--pipeline-windows 1` tokens are unchanged (identical to the reference), and the prompt path, which also
commits through `pl_commit_async`, did not move (4,106 tokens 1,239.7-1,248.9 tok/s, 16,380 tokens 1,552.1, the same
as before the change).  Not run: CUDA/HIP (the file is the port's own copy), a 32K-context prompt.

Limits: this does not make `--pipeline-windows 2` token-exact.  A second defect remains: after the first *multi-token*
window the pipelined state is one row ahead of the serial one (`pos 22` in the pipelined dump equals the serial
`pos 23`), so the commit count and the window's own advance still disagree somewhere for T > 1.  The path stays
opt-in and the shipped default is `--pipeline-windows 1`.
2026-10-29 12:45:35 +01:00
Niko1221 478964ecaa [Engine] SYCL: a state dump for the pipelined windows, and the two defects it already shows
`--pipeline-windows 2` runs, is faster (57.7 tok/s against 51.3; 68.1 against 45.9 with a forced window size) and
produces different tokens.  Rounds 16-21 eliminated every switch and mode: the speculation (THETA), the prestaging,
the agreement chain, the snapshot overlap, a fixed window size, the split point (a one-layer stage 1 still differs)
and the elastic KV (off by default).  What is left is the two alternating verifiers themselves.

This adds the instrument for the next step: STRATA_PIPELINE_DUMP=<file> appends a hash and the sum of the first
stage's GDN state (recurrence state plus conv history) after every window.  It is debug-only - it waits for the
stage's stream - and off unless the variable is set.

It already shows one defect on its own: in the pipelined path the same position of a second request does not hold the
same state as the first request's (`pos 18 windows 2 sum 3669.18189 hash 5f718405585185d9` against
`pos 18 windows 1 sum 1952.13774 hash b97305876cf73d2c`), so a request does not start from a reset sequence state.
That is separate from the token divergence, which reproduces on the very first request of a fresh process
(`compare_tokens.py`: first difference at token 4).

The serial path needs the same dump before the two can be diffed element by element; that is the next step, and
`--pipeline-windows 1` (the shipped path) is unaffected by this commit.
2026-10-29 12:45:35 +01:00
Niko1221 09524eb479 [Engine] SYCL: the prompt attention's query batch is 128 by default
Problem: `attn_batch` was 32, a value chosen for the shuffle-reduction score phase's vector-matrix kernel.  The
lane-cell phase is now the prompt default, and it wants more queries per launch.

Result, same build, back to back, 3 warm repetitions each (2x Arc Pro B65, Qwen3.8-Flash-Next IQ3_S, 2,048-token
chunks):

| prompt | batch 32 | batch 128 | gain |
| ---: | ---: | ---: | ---: |
| 4,106 tokens | 1,227.9-1,235.1 tok/s | 1,240.9-1,247.6 | +0.9% |
| 16,380 tokens | 1,582.2-1,584.2 | 1,604.9-1,605.8 | +1.3% |

Batch 512 is another 0.2% on the 16K prompt but takes four times the scratch (400 MB per stage instead of 100 MB),
so 128 is the trade.  The expert cache keeps its slots: 10,253 either way at 4K.

Approach: the default of the `STRATA_ATTN_BATCH` switch (added earlier this branch) moves from 32 to 128; the
variable still overrides it in 1..1024.

Validation: 64 generated tokens are identical to the serial reference (`scripts/compare_tokens.py`, first
difference: none).  Not run: CUDA/HIP (untouched - the switch is the SYCL port's), a card small enough for the
100 MB of extra scratch to cost expert-cache slots.

Limits: the gain is attention-bound and small; it is the tuning of the phase that landed in the previous commit, not
a new path.
2026-10-29 12:45:35 +01:00
Niko1221 949f77c0a8 [Engine] SYCL: prompt attention takes the lane-cell score phase, +5% on a prompt read
Problem: the prompt path runs `qsa_decode_attn_batch` (the tensor-core kernel is opt-in and 2.9x slower here), and
attention is 21% of a 4,106-token prompt's GPU timeline and 24% of a 16,380-token one.  The port already had the
`STRATA_ATTN_LANECELL` score phase - one cell per thread, no shuffle reductions, scores bit-identical to the
shuffle reduction - but only behind an environment variable, so nobody got it.

Result, same build, same box, back to back, 3 warm repetitions each (2x Arc Pro B65, Qwen3.8-Flash-Next IQ3_S,
`--spec 4`, 2,048-token chunks, nothing else on the GPUs):

| prompt | shuffle reduction | lane-cell | gain |
| ---: | ---: | ---: | ---: |
| 4,106 tokens | 1,167.4-1,171.9 tok/s | 1,226.3-1,235.3 | +5.2% |
| 16,380 tokens | 1,456.3-1,457.4 tok/s | 1,524.3-1,526.2 | +4.7% |
| decode (256 tokens, 18-token prompt) | 56.5 / 57.0 | 56.6 / 57.0 | unchanged |

Approach: the choice follows the query count.  The prompt path hands the kernel a batch of queries (`attn_batch`, 32
by default), the decode window hands it the window's few rows (at most eight per call), and the few-query case is
1.4% slower with the lane-cell phase (55.9-56.3 against 56.6-57.0 tok/s), so `n_q >= 16` takes it.
`STRATA_ATTN_LANECELL=1/0` forces either way, so the old path is one variable away.

Validation: 64 generated tokens from the gated build are identical to the same prompt's tokens from the serial
reference (`scripts/compare_tokens.py`, first difference: none); the lane-cell phase's scores are the shuffle
reduction's bits by construction (one cell per thread, same order).  Not run: CUDA/HIP (untouched - the change is in
`sycl/src/kernels/cuda/qsa_decode_attn.dp.cpp`, the port's own copy), the parity bench on the gated build (it forces
its own arms), a 32K-context prompt.

Limits: measured on two Arc Pro B65 with a fully resident expert cache; the gain is attention-bound, so a prompt
short enough to be dominated by the expert GEMMs will show less.
2026-10-29 12:45:35 +01:00
Niko1221 7e0f719903 [Engine] SYCL: a layer-split stage must be a GPU of the primary device's backend
`--batch N` (the concurrent serving slots) loads the OpenCL adapter as well, so on a two-card `xe` box
dpct::device_count() then reports five devices: the two Level-Zero GPUs, the CPU, and the OpenCL view of the same
two cards.  An automatic split took every index above 0, so it tried to put a stage on the CPU and the engine died
before serving:

    layer split across 5 GPUs: CUDA0, then CUDA1,2,3,4 (split auto)
    layer split, CUDA2 weights: the weight arena does not fit (1466 MiB needed, 0 MiB of 160876 MiB free on that card)

The auto-filled split now only takes GPUs of the primary device's backend, so `--batch 2` with `--layer-split auto`
starts on the same 25/23 split as a plain run.  An explicit `--layer-split K` / `--split-device` is unchanged.

Measured on 2x Arc Pro B65, Qwen3.8-Flash-Next IQ3_S, 64 new tokens per slot:

| slots | window | rows/window | aggregate |
| ---: | ---: | ---: | ---: |
| 1 (speculative stream) | 44.4 ms | 2.35 | 57 tok/s |
| 2 | 40.6 ms | 2.00 | 49 rows/s |
| 4 | 58.1 ms | 4.00 | 69 rows/s |
| 8 | 88.7-89.3 ms | 7.00 | 78-79 rows/s |

So a batch window costs about 18 ms + 10 ms per row, and eight slots (the most that fit the VRAM) reach ~79
tokens/s of aggregate decode - above the single stream's 57, below the 100 the two-card budget would need.
2026-10-29 12:45:35 +01:00
Niko1221 f7570e23e2 [Engine] SYCL: make the prompt attention's query batch a switch, and measure it
The prompt path runs the decode-shaped FP32 attention kernel `attn_batch` queries per launch (the tensor-core
kernel is opt-in and 2.9x slower here, see qsa_prompt_attn_parity).  32 was chosen for a vector-matrix kernel;
on 2x Arc Pro B65 more queries per launch give it more to overlap.

Measured on Qwen3.8-Flash-Next IQ3_S, 16,380-token prompt, second request in the process (STRATA_PREFILL_TIMING):

| attn_batch | qsa attn | prefill |
| ---: | ---: | ---: |
| 32 (default) | 2,572 ms (23.0%) | 1,294 tok/s |
| 64 | 2,492 ms | 1,309 |
| 128 | 2,448 ms | 1,316 |
| 256 | 2,422 ms | 1,315 |
| 512 | 2,409 ms (22.0%) | 1,319 |

So the switch is worth ~2% and the default stays 32 (the difference is inside the run-to-run spread on one pair of
runs); STRATA_ATTN_BATCH=512 is the measured best for a long prompt.  The kernel itself is the problem: it is
1-4% of the card's fp16 peak at long context, and the XMX port is correct (2e-6 against FP64 in
qsa_prompt_attn_parity) but 3x slower, using 122,880 of 131,072 bytes of local memory for its 64-cell layout
(mode 32 halves that to 86,784 and is slower still).
2026-10-29 12:45:34 +01:00
Niko1221 d56565ce39 [Engine] SYCL: the grouped-expert bench reports a device-side launch failure instead of silently timing nothing
The bench's queue is created with an exception handler, which swallows a failing kernel: the timing then reads
"0.3 us per group of 10 experts" because nothing ran, and the bit-for-bit comparison passes trivially (both
buffers stay at the 0xff fill).  Every wait now goes through wait_and_throw() and prints the what() of a failure.

Verified on the Arc box: a run with real geometry prints dn=10 and non-zero outputs, so the kernel does run; the
per-mode timing of a single small group is still under one expert's memory traffic and needs the mode/phase
semantics checked before its GB/s numbers are quoted.
2026-10-29 12:45:34 +01:00
Niko1221 da3df90eb8 [Engine] SYCL: build the grouped-expert bench, and port it to SYCL
sycl/src/kernels/native_expert_bench.cpp was in the tree without a target, so it never compiled - and it still
carried raw CUDA calls (cudaMalloc, cudaMemcpy, cudaMemsetAsync, cudaFree, cudaEventRecord/ElapsedTime). It is now
a target under STRATA_NATIVE_EXPERTS and uses USM allocations, queue copies and a host clock on its own in-order
queue, so the grouped native expert modes can be timed and compared bit for bit on real GGUF rows at the decode
window's shape (G experts x T tokens).

Run: native_expert_bench <shard1.gguf> <layer> <G> <T> <ref mode> <mode> [iters]

Measured on the Arc box (layer 0 of the IQ3_S pack = iq3_xxs gate/up, iq4_nl down): every mode is bitwise equal to
the reference, G=480 x T=6 reports gu 7.7 us and down 5.3 us for the whole group - the absolute numbers still need
the mode's own semantics checked (a single group's 0.3 us is below one expert's memory traffic, so that path is
returning early).
2026-10-29 12:45:34 +01:00
Niko1221 16ccaf6150 [Engine] SYCL: the verifier's stage profile reads its stamps on its own stream
STRATA_VERIFY_PROFILE collects GPU timestamps per layer step into device memory, but the readback ran on the
device's default queue: neither ordered after the window's work on cs_ nor necessarily this stage's device. That
is why the stage table was only ever printed in the eager mode, which stamps from the host after draining the
device - so its numbers include launch overhead and gaps and cannot attribute kernel time.

* collect_profile() reads the stamps on cs_ (under OnDevice(device_)) and waits there.
* The window prints the table from the GPU's own clock every N windows:
  STRATA_VERIFY_PROFILE=1 STRATA_VERIFY_PROFILE_EVERY=32, without the eager drain.

Measured on 2x Arc Pro B65, Qwen3.8-Flash-Next IQ3_S, decode window T=4, per stage (24 layers) and per window:
total 20-24 ms; of that the routed experts ("VRAM hits") 5.0-6.6 ms in the GDN layers and 1.3-3.0 ms in the QSA
layers, the dense q8/qkv gemv 2.0, hc-read1+router 1.9, shared+quant 0.96, out-proj 1.1, z 0.97. The eager table
used to overstate hc-read0 and the experts by 2-4x because it timed the host's waits.
2026-10-29 12:45:34 +01:00
Niko1221 11a72289d2 [Engine] SYCL: --pipeline-windows runs again (three DPCT migration defects)
The two-card window overlap never ran on the Intel engine: the first poll of
every pipelined window returned "verify: error".

* Verifier::done() lost CUDA's cudaErrorNotReady branch. The port's
  sycl_event_query() returns 1 for "still running"; the migrated code treated
  every non-zero value as an error. A pending window is not an error now:
  flush the queue every 2 ms and time out after 20 s, as the CUDA original does.
* A drafter completion event was used as a barrier on the other card's queue.
  SYCL rejects a cross-context dependency ("Cannot submit to a queue with a
  dependency from a graph that is associated with a different context"). On two
  cards the host waits for the event; the same card keeps the barrier.
* The first card's second verifier (ver_b) took whatever device was current at
  init; it is created under OnDevice(0) now.

Diagnostics: queue_wait_diag() names the queue, its device and the SYCL message
instead of "error"; an unqueryable completion event says so; and with
STRATA_VERIFY_DEBUG=1 every verifier prints its device and streams.

Verified on 2x Arc Pro B65, Qwen3.8-Flash-Next IQ3_S:
* --pipeline-windows 1: tokens identical to the serial path (64 tokens on a
  short prompt, 32 after a 4K prompt, two runs each); a short prompt read drops
  from 206 ms to 118 ms (-43%).
* --pipeline-windows 2: runs now (it crashed before), ~8-20% faster decode, but
  it does NOT produce the serial path's tokens yet - the decode overlap and its
  GDN rollback still need work on this backend. Not a default.
2026-10-29 12:45:34 +01:00
Niko1221 43f32b3046 Server: only a one-token end marker is a stop id; pieces of <|im_end|> no longer end a DeepSeek reply ("Nerazim" came out as "Neraz", #4576)
Service took the ids of "<|im_end|>" and "<|endoftext|>" as its stop ids whatever the tokenizer made of them. DeepSeek-V4.1's tokenizer
has neither as one token: it cuts them into '<', '|', 'im', '_end', '|', '>' and 'end', 'of', 'text'. A server run without the family
named (a hand-written config, a gateway) therefore ended the reply in front of the first "im", "|", ">", "end", "of" or "text" it
wrote, and the real end token (id 1) did not end it. Nerazim is N, er, az, im: the reply stopped at 'im' and the client got "Neraz".
The engine and its --eos 1 were fine; nothing was cut by the stop-string hold-back (StopMatcher gives its held tail back at the end).

end_token_ids() keeps a marker only where it is a single token, and falls back to the tokenizer's own end token (special_ids "eos").
Qwen's tokenizers are unchanged. The family guess from the previous commit still names the right parser and DSML for DeepSeek.

serve/test_stop_holdback.py: the end ids of both tokenizers; StopMatcher over every split of the text and one character at a time
(Qwen and DeepSeek stop lists, words ending like the start of a stop string); the Detokenizer plus StopMatcher byte by byte; and the
OpenAI and /v1/messages routes, streamed and whole, with and without the family named: no stop string (whole reply), a stop string that
never comes (the held tail is given back), a stop string (cut exactly there).
2026-10-29 10:26:56 +01:00
Niko1221 3fcb2106d0 serve: tool_call_recovery is on by default; "tool_call_recovery": false is the off switch
The tool-call forms the model drifts into (<parameter=NAME> as the opener, JSON inside <tool_call>, a bare <function=NAME>, a second call in one
wrapper) are read as the call when the tool is declared; anything else stays text. On the 1,462-turn drift corpus 98.3% of turns read as intended
with it and 92.9% without; the specimens of the tool corpus read the same with it on or off.

- Service.tool_call_recovery starts True; tool_call_recovery_setting(cfg) is the config rule (absent = on, a non-bool still stops the start).
- Tests: a fresh Service reads the drifted call without being told; the config rule; the old on/off switch test is unchanged.
- docs/DETAILS.md: the config row says on since 0.2.8.
2026-10-29 10:25:07 +01:00
Niko1221 20a63cf0f0 Residency-biased routing (STRATA_ROUTE_PRIOR) is off by default again on 12 GB NVIDIA cards (#4402)
0.2.7 turned STRATA_ROUTE_PRIOR=1.5 on by itself on a discrete NVIDIA card of 13 GiB or less. Reports and our own 0.2.7 agent check (4.5 of 10
tasks with it on, 5.6 with it off) tie it to repetition loops and weaker results in long agent sessions, so the owner took the default back
(2026-10-29). With nothing set the engine now routes exactly as 0.2.6 and as STRATA_ROUTE_PRIOR=0 did; STRATA_DEFAULTS=026 is unchanged.

- sm120_series.hpp: route_prior_default() answers 0 on every card; route_prior_resolve() is the pure rule the engine reads (a set value wins on every
  card); kRoutePriorSmallCard stays the 1.5 opt-in value.
- verify.cpp: g_route_prior() uses it; the "on by default on this card" start line is gone.
- Expert settings / Terminal: "Faster answers on 12 GB NVIDIA cards" is now an Off-by-default row that writes STRATA_ROUTE_PRIOR=1.5; the words
  say it is faster on 12 GB cards but can repeat itself in long agent sessions (nine languages). A 0.2.7 file with STRATA_ROUTE_PRIOR=0 reads as Off.
- Tests: arch_defaults_test (default table over 12 cards x hip x integrated x 026, explicit opt-in values), speed-switches.test.mjs and
  tests/terminal/test_speed_switches.py.
- docs/DETAILS.md: both places that described the 0.2.7 default.
2026-10-29 10:25:07 +01:00
Niko1221 72a2b77057 Server: a config that names no "arch" but ships DeepSeek's tokenizer is served as deepseek41 (#4137)
serve/server.py run on its own with a hand-written config (no "arch": "deepseek41") gave DeepSeek-V4.1 the Qwen defaults: the stop ids
of <|im_end|> and <|endoftext|>. The DeepSeek tokenizer splits those strings into pieces, so a "|" or ">" in a reply ended it
(finish_reason stop at token 19 in front of a Markdown table, or right after "## 3. Recom"), and the real end token (id 1) was not a
stop id, so it came out as the text <|end▁of▁sentence|>. Reproduced on 2 x RTX A6000 with the reporter's request: the config with the
arch answers 730-1161 tokens six times out of six; without it, stops at 192-436 tokens, one of them right after "## 3. Recom".

The tokenizer's own pre-tokenizer name ("deepseek3") now picks the family when the config names none; a named arch is never changed.

(cherry picked from commit b18292d702)
2026-10-29 10:21:33 +01:00
Niko1221 cfa197e21a Qwen: the automatic expert cache leaves the reserve it was asked to keep - the ~245 MiB the load allocates after it are priced (#4474)
--expert-cache auto sized the cache from the VRAM free after the weights, less --vram-reserve-mib, the draft head and the other priced
buffers. What the load allocates after the cache and no setting prices - the CUDA libraries the prompt path and the verifier warm up
(cuBLAS handles and workspaces, lazily loaded modules) and the verifier's small buffers - came out of the reserve, so the card ended
about 240 MiB under it: 459 MiB free at --vram-reserve-mib 700 on an RTX A4000 16 GB, 457 MiB on an RTX A6000 48 GB, under the 512 MiB line
the serve check warns about at every request (#2065). Measured with STRATA_TRACE=1: -164 MiB at the prompt path and -80 MiB at the verifier
beyond the draft head, the same with --prefill 512, 2048 and auto, on both cards, and the same on v0.2.6 (so not new in 0.2.7).

auto_cache_slots() (include/strata/core/auto_cache.hpp, tested in src/core/auto_cache_test.cpp) now leaves kLateAllocMib = 256 for it,
unless the card could not keep min_slots (#496) after it, and not under STRATA_DEFAULTS=026. An explicit --expert-cache N and
--vram-reserve-mib N are untouched; the cache is ~100 slots (about 1.8% on a 16 GB card) smaller.
2026-10-29 00:19:50 +01:00
WenyuanXu 22fb183cf6 DeepSeek engine: a layer split's chunked prompt no longer deadlocks - the host waits for another group's flag before it queues the device-side wait (2x H100, IST-DAS-2.25bpw)
On a two-GPU layer split, every prompt on the chunked path stalled: a device's wait kernel spun for a
hand-off while that device's thread blocked in its first cuBLAS call, and the other device's thread too.
host_wait_at_least polls the same host-mapped flag (the kernels' signed flag - seq test) before the four
cross-group waits of the chunk path, so the kernel finds the flag up. The GPU work and its order do not
change.
2026-10-29 00:05:59 +01:00
Niko1221 01d6f7b995 serve: an engine left behind by an earlier run is waited for until it has given its memory back (#4051)
The server ends an engine an earlier run lost (strata_control/procs.py) before it
starts the next one, and waited 10 s after the kill.  An engine with tens of GB
of pinned memory takes longer to give it back, so the new engine's memory guard
saw 50 GiB still in use and loaded a 40 GiB arena into 10 GiB.  The wait is now
up to 120 s, with a progress line every 10 s; one still there after that is said
in plain words (what it is, why starting now is risky, what to do) and the start
goes on: nothing is refused, the engine waits for RAM itself and warns.

Also documents what the parser repairs (#4078 #4427 #4077) and the arena RAM wait
in docs/DETAILS.md.  Tests: serve/test_leftover_engines.py and a fake-clock case
in tests/terminal/test_procs.py.
2026-10-29 00:01:54 +01:00
Niko1221 07d5ecfacb engine: wait up to 20 s for the RAM the expert arena needs, then load with a plainer warning (#4051)
An arena load with 10 GiB available for a 40 GiB arena sent a 60 GiB host into
memory reclaim until it was reset.  The check that is there warns only (a
cgroup limit below the arena still stops the start), and its own example is an
engine that is still exiting.  Before the warning, the load now waits for the
RAM to come back, re-reading it every 0.25 s for up to 20 s, and says so in the
log every 5 s (STRATA_ARENA_RAM_WAIT_S changes the seconds, 0 loads at once).
If it is still short the load goes on as before, with the numbers; below half
of the arena the warning says the whole PC can stall in reclaim and names a
systemd MemoryHigh= as the way to keep one program from taking all the RAM.
Nothing is refused or capped.

detail::wait_for_ram is the loop with its sampler and sleeper passed in, tested
in file_expert_source_test (test_wait_for_ram).  expert_source.cpp passes
g++ -std=c++20 -fsyntax-only against the CUDA 13.1 headers; the engine itself
was not built or run for this change.
2026-10-29 00:01:54 +01:00
Niko1221 b18292d702 Server: a config that names no "arch" but ships DeepSeek's tokenizer is served as deepseek41 (#4137)
serve/server.py run on its own with a hand-written config (no "arch": "deepseek41") gave DeepSeek-V4.1 the Qwen defaults: the stop ids
of <|im_end|> and <|endoftext|>. The DeepSeek tokenizer splits those strings into pieces, so a "|" or ">" in a reply ended it
(finish_reason stop at token 19 in front of a Markdown table, or right after "## 3. Recom"), and the real end token (id 1) was not a
stop id, so it came out as the text <|end▁of▁sentence|>. Reproduced on 2 x RTX A6000 with the reporter's request: the config with the
arch answers 730-1161 tokens six times out of six; without it, stops at 192-436 tokens, one of them right after "## 3. Recom".

The tokenizer's own pre-tokenizer name ("deepseek3") now picks the family when the config names none; a named arch is never changed.
2026-10-28 23:59:46 +01:00
Niko1221 d71132738e serve: damaged tool-call arguments, a stray closing think tag and a missing </function> no longer cost the call (#4427 #4077)
#4427: tool calls came back as text or with a wrong argument.
- A JSON array or object argument with a slip in it (a trailing comma, single
  quotes, a bare string between two members, brackets left open) is read as
  written instead of reaching the client as a string that fails its schema; the
  reporter's todo_write array with "pending", "in_progress" in one object is the
  test.  Valid JSON is untouched; text that is no array or object stays text.
  The repair is counted (/metrics strata:tool_args_repaired_total) and logged.
- A parameter's </parameter> followed by </tool_call> ends the value: the call
  whose </function> was left out is a call.
- The same holds for a call written inside the thinking, and one whose output
  ends on </function> inside the thinking; a following opener confirms an
  omitted </tool_call> there too, as in the answer.
- The call announced while streaming and finished by the end of the output
  carried empty arguments in its final event (read by MCP and the Responses
  assembler): it now carries the parsed ones.

#4077: after the reasoning ended at a closing think tag, one more at the start
of a line in the answer is the stray: the model quoted the tag while thinking.
It is dropped and the answer's code tracking starts over, so a call that follows
is not taken for a quote by a fence or backtick the reasoning left open.  The
reasoning after the stray is still sent as the answer (a stream cannot take it
back); the call arrives.  Logged and counted.

Adds serve/test_tool_corpus.py (49 specimens: the reporters' examples, the
Qwen3.8-Flash-Next form, the drift shapes, DeepSeek DSML; each whole, in pieces
of 1, 3 and 7 bytes and cut in two at every byte, streaming on and off) and
serve/test_tool_routes.py (Chat, Messages and Responses, whole and streamed).
2026-10-28 23:55:33 +01:00
Niko1221 84c7f43358 CUDA 12 driver floor: setup takes it from the engine package, not from a constant (#4247, #4188)
setup.py said the CUDA 12 engine 'works with driver 528 or newer' (525 on Linux), NVIDIA's minor-version line, while the signed feed's cuda12
packages (built with CUDA 12.9) carry min_driver 575.51 / 576.02 and strata update refuses them below that: a V100 PC on driver 556-560 was told
the engine would work and then that the feed had no engine for it.

strata_control/driver_floor.py reads the floor from the installed cuda12 package, else the signed feed (once, 10 s at most, only in a setup run
that needs the CUDA 12 engine), else the figure the published packages carry. Setup warns with that number (recommend, never force: a locally
built engine may still run); the only stop left is NVIDIA's own line, under which no CUDA 12 program starts. docs/OLDER_GPUS.md says the same.
2026-10-28 23:53:00 +01:00
Niko1221 012bea51f8 Packaged Terminal: setup.py without CMakeLists.txt, and strata install hands its options to setup (#4188)
source_version() read CMakeLists.txt, which the packaged Terminal does not carry: prebuilt_bases() raised FileNotFoundError and ended the setup.
It answers '0' now when there is no source tree, prebuilt_bases() then looks at the latest release only, and an installed engine is not called
stale for want of a version to compare with.

strata install passed its tail to setup.py through argparse.REMAINDER, which does not take a tail that starts with an option the parser does
not know (--source, --family): 'unrecognized arguments', or the value split from its option. The options of setup.py now go behind a --
before the parse; --update and --help stay strata's own.
2026-10-28 23:53:00 +01:00
Niko1221 5596cb2c5b launch: do not point at an engine log that was never written (#3714)
When the runtime refused the start because another host still owned the profile ('already has a live managed runtime'), no engine existed
and no log was written, but the error ended with 'The engine's log has the reason: <path>' and the screen showed an empty folder. A busy
profile now gets its own sentence (the other server, and strata launch --stop), and a log that is not on disk is no longer named, here or
in the sentence after an engine that stopped while loading.
2026-10-28 23:52:42 +01:00
Niko1221 587a474433 Qwen: the PLE residency policy gives its lock back when the lock is what leaves the automatic conversation cache too little RAM (#4518)
The 0.2.7 policy locks the 26.8 GiB PLE table in RAM at start when table + expert arena + 8 GiB fit. What the rest of the load takes
afterwards (KV pools, pinned staging, host buffers of two GPUs) is not known then: on a 128 GB two-GPU PC it left under 5 GiB and the
automatic conversation cache turned itself off, a regression against 0.2.6, which kept 8192 MiB.

When the cache is automatic and the lock came from the policy (not --ple-io ram), the cache is sized first: if unlocking the table
would give the cache more room, PleTable::unlock() hands the lock back (the pages stay cached, the OS may reclaim them), the log says
so, and the cache is sized from the RAM then free. --conversation-cache-mib N and an explicit --ple-io ram are untouched.
ple_lock_blocks_cache() is the decision, unit-tested in src/core/ple_residency_test.cpp.
2026-10-28 23:41:53 +01:00
Niko1221 35c0c9a8ae DeepSeek engine: slot_load() writes each tensor-parallel member's window ring and compressor group under its own device (#4350)
slot_load() selected the lead layer's device once and then copied into every member of a tensor-parallel group; the members' buffers
sit on their own GPUs, so on two identical cards (tp 2) the second member's copy ran with the wrong device current. A failed copy
makes slot_load() fail, and slot_take() then deletes the saved conversation. Every other per-member loop (restore, reset) already
selects the member's device; slot_load() now does too.
2026-10-28 23:41:53 +01:00
Niko1221 c7a0cef13a Conversation cache: parking an idle --batch slot no longer drops the main session's copy with the shared-prefix checkpoint (#4420)
A batch slot's chain holds one checkpoint (the one its admission copied in). park_slot() went through park_source(), whose
drop_superseded() (#342) removes a parked entry when the outgoing chain holds that entry's deepest checkpoint; the main session's
copy of the same conversation qualifies, and the root checkpoint at the end of an agent's system prompt went with it. The next
overlapping request then read the whole 5 K-token prefix again (323 ms in 0.2.6, 4130 ms in 0.2.7).

ConversationCache::supersedes_richer() says when the park would drop an entry that has a checkpoint the slot's chain lacks; park_slot()
then leaves the slot unparked (it is overwritten, as in 0.2.6) and says so. A slot with no such parked copy is parked as before.
The predicate and its test come from the report (shenmintao, #4420).
2026-10-28 23:41:44 +01:00
Niko1221 ea73eab442 DeepSeek engine: the auto RAM store no longer reads a discrete GPU's pinned buffers as unified memory (#4147)
store_gb_now() took any drop of 4 GB or more in the free RAM between the first estimate and the end of the weight upload for a
GPU that shares the PC's RAM, and subtracted a context-sized reserve (27 GB at 256K). On a discrete pair (2 x RTX 3090) the
pinned staging buffers and the page cache cause the same drop, so a 256K context shrank the store from 48 GB to 15-18 GB and
decode fell from 9-12 to 3-4 tokens a second (a regression since 0.2.4.2).

The decision is now store_gpu_shares_ram() (ds4_experts_rule.hpp): the RAM must have dropped AND a device must report
cudaDevAttrIntegrated. Strix Halo, the DGX Spark's GB10 and the Apple path keep the old rule; a discrete card never counts.
Unit test in tests/dsv41/experts_rule_test.cpp.
2026-10-28 23:41:26 +01:00
Niko1221 5ef90cb96b App host: DeepSeek launch arguments are built from what strata-dsv41 takes (#4358 #4359 #4505 #4507 #4504)
The Qwen engine's --vision stands alone, the DeepSeek engine's is a pair (--vision on|off). Taking out only the name left the value behind
(--eos 1 off --dspark auto: unknown option off, exit 2). A DeepSeek model keeps its pair whole now.

The Settings field 'GPU memory for other apps' wrote --vram-reserve-mib and the resource preference wrote --pool-workers into a DeepSeek
model's arguments; strata-dsv41 takes --reserve-mib and has no --pool-workers. Before a load, the arguments and the person's saved options
of a DeepSeek model are built from the engine's option list (src/dsv41/ds4_main.cpp, checked by a test): the reserve is renamed, anything
else is left out with its value and said in the load's log. The tune operation no longer writes the resource preference into a DeepSeek file.
2026-10-28 23:37:47 +01:00
Mike 3e1141edf0 GUI: ask for the longer wait only when all three of a Control's ceilings agree (#3459 #4191)
The app's ceiling probe read one file. The second symptom of this branch - `direct runtime intent has an invalid timeout`,
on a build whose CLI validator was already fixed - is the reason it now reads the operation record too: a start wait is
bounded by management, by the record written before the start runs, and by the runtime, and a Control can raise any of
the three on its own. The app asks for more than 600 s only when a Control's own sources hand both of the first two to
the runtime's ceiling, so a half-raised Control keeps the wait it will accept instead of failing the load. Anything the
app cannot read, or a line that still spells out 600, keeps today's 600 s.

Checked against the Control folders on this PC and against the released sources in git: v0.2.5 and v0.2.7 both carry
`MAX_START_WAIT = 1800` with both gates at 600, so the app asks them for 600 s - which is what they take; the patched
tree and the staged 0.1.52 component ask 1720 s for a 180 GB model; the installed 0.1.49.2 component asks 600 s. The
marker for the record is `"wait_s": (` rather than `wait_s`, because the record names that key in its field lists long
before its range table, and the first match there would have pinned the app to 600 s forever.

The packaged test drives the four Control shapes (nothing, runtime only, CLI only, all three) so each half-raised
combination is a case, not a guess.
2026-10-28 23:33:57 +01:00
Mike 293044e561 control: the operation record takes a long start wait too, and the tests walk the whole CLI path (#3459 #4191)
A local build of the previous commit got further and said `direct runtime intent has an invalid timeout`. A start wait is
checked three times before the engine is reached, and 27cbbfe6 raised only the runtime's: management.runtime_action (fixed in
the previous commit), journal._validate_direct_intent, and runtime.start itself. The record is written before the start runs,
so its own (0, 600) for runtime.start and (1, 600) for startup_wait_s stopped a long wait that the layer above had just
accepted.

The record now reads both ceilings from the runtime that runs the wait, the way the CLI validator does. A wait above them is
still refused, and stop and verify keep their own 600 s, which is what runtime.stop and runtime.verify take.

The tests are the reason this was found twice by hand instead of once here: the CLI test from the previous commit faked
runtime.start, so it stopped at the first gate and passed while the record still refused the wait. One test now walks
management -> journal -> runtime.start with only the engine itself faked, at 300, 600, 601 and MAX_START_WAIT, and says which
wait failed to arrive; another pins each kind's ceiling on the record directly. Reverting the journal change fails three of
them with the quoted message; with it, all four pass. StartWaitLimit keeps its records in a scratch folder.
2026-10-28 23:33:57 +01:00
Mike 5af539691d GUI: ask the installed Control for the start wait it actually takes, and let the CLI take the runtime's ceiling (#3459 #4191)
The version gate of the first commit was wrong, and a local build of it said so on the first load of a big model:
`Start wait must be finite seconds in 0..600`. Control validates a start wait twice and the two ceilings disagree.
`runtime.start` takes `MAX_START_WAIT` (1800, 27cbbfe6), but `strata runtime start --wait` - the command the app uses -
checks the wait in `management.runtime_action`, where 27cbbfe6 left it at 600. It refuses the start before the engine
is reached, so no released Control (v0.2.4 through v0.2.7, Control 0.1.45 to 0.1.52) takes a wait above 600 s however
it is numbered: a version gate could not tell which Control would take it.

`management.runtime_action` now takes the ceiling from the runtime it hands the wait to, for both start and restart,
which is what 27cbbfe6 had already done for the direct `runtime.restart` path. The default stays 300 s and every wait
at or below 600 s is unchanged. The app reads the ceiling out of the Control it found - the same source-read it does
for the settings fields and /v1/responses - rather than guessing from a version, so it keeps 600 s on the Control
people have today and takes the longer one as soon as that Control is updated. A Control it cannot read falls back to
600 s, which is what the app asks for now.

Tests: the CLI path gets the ceiling test that only existed for `runtime.start` before, and the packaged load test
drives the three Control shapes (no ceiling, runtime 1800 with a CLI stuck at 600, CLI following the runtime) instead
of a version string.
2026-10-28 23:33:57 +01:00
Mike 0e72aff563 GUI: the start wait follows the model's size, as strata launch does, and never asks an older Control for more than it takes (#3459 #4191)
The app waited a flat 300 s for any model, so a big model on a slow disk ended with `startup_wait_expired` while the engine
went on reading: #4191 reports it at about 180 GB and says editing 300 to 600 in this file fixed it on 0.2.1 but needs
several files now, and #3459 is the same stop on Windows after the tuning stage. Main solved the Terminal's side for
itself (27cbbfe6, #3949) and left this file with 300 s, or the config's engine_ready_s clamped to 600 s.

That clamp was written the same day as `MAX_START_WAIT = 1800`, on a branch whose Control still refused more than 600 s —
its message says "at most the 600 s Control allows". Control now takes 1800 s, so the clamp's reason has gone: it is the
only reason the app cannot use the size figure the Terminal already uses.

- startWaitFor(config,entry,model) takes the model's own size where the app has it (the installed model's bytes on disk,
  else the catalog's download figure) and applies main's shape: ten minutes, about eight more seconds per GB above 40 GB.
  A model nobody can size keeps the 300 s it always had, and a config's engine_ready_s is a floor, never shortened.
- The ceiling comes from the Control this app talks to, not from a guess: 600 s before Control 0.1.50, 1800 s from there.
  Control rejects a wait above its own ceiling before it starts anything, so an older Control keeps today's behaviour.
- Where the start now waits longer than the server's own engine wait (900 s), the run config gets `engine_ready_s` above
  it, as `strata launch` does: the server ends the engine when its wait runs out, so a start that waits longer than the
  server can never win. It is only ever raised, and only in the launch's own copy of the config.

No new variable, no change to Control or the Terminal, and nothing to set: the wait follows the model, as main chose for
the Terminal. The app's outer five-minute health loop is not the load wait in packaged mode (Control has already reported
ready), so it is left alone.

Validation: Linux, Node v22.23.1, against upstream/main 5e8a4cca on the same machine. `npm test` in apps/host: 1227 tests,
1120 pass, the same 2 failures as on upstream/main (component-updates "an older host left running" and timeskip-real
"Stop ends the job"). The two new tests cover 180 GB at 600 s on Control 0.1.45, 1720 s on 0.1.50 with the server readying
after it, a small model keeping 300 s, the 1800 s ceiling, and setup's 3600 s for DeepSeek being honoured and never
shortened. The 11 version and size cases were checked against the formula directly, including an unknown Control version,
which falls back to the conservative 600 s. Not run: a real load on a real PC, Windows, macOS.
2026-10-28 23:33:57 +01:00
Tsury b72f56e02a serve: only a whole quoted <function=NAME> opener keeps a parameter value open (#4078)
A bare `<function=` in prose or code (no name, or a name not followed by a
parameter or close) counted as a quoted call, so every later </parameter>
was skipped: the call came out as content, or never finished when streaming.
Adds the review's two values to test_quoted_call_values.
2026-10-28 23:33:10 +01:00
Tsury 77794e3400 serve: a string argument that quotes a tool call or reopens its parameter stays exact (#4078)
A parameter close inside a call that the value itself opened (more
<function= than </function> before it in the value), or one followed by the
same parameter name again, is part of the value. Prose between two calls
still ends the first call. Test: serve/test_quoted_call_values.py.
2026-10-28 23:33:05 +01:00
Günter b5aa1d94e0 Update test start: skip DeepSeek models when choosing the model to test with
discover_run_config picked the first installed model by id. With a DeepSeek and a Qwen model installed, "deepseek-..." sorted first, and the
test start ran the engine under test (strata.exe) with DeepSeek's options: "unknown argument: --eos", server_exited, engine 0.1.50 refused
("did not start correctly on this computer"). A DeepSeek model runs on strata-dsv41, which has its own package, so it is left out; with only a
DeepSeek model installed the update has no model to test with, as on a first run.
2026-10-28 23:31:38 +01:00
吴佳浩Alben 2f9c6fc35c Fix #4209: a saved preset's lower-case model tag reaches setup as its key
strata_cli/launch/settings.py saves the model into a preset/choices file lower-case ("--model iq3_xxs"),
and LaunchRunner.setup_args() forwarded it unchanged. setup.py's --model choices are the MODELS table's own
keys ("IQ3_XXS", "Gyro-S"), so argparse refused the argument: starting a saved preset with any choice that
differs from the installed config (--vision, --context, --kv) reran setup and stopped there. Typing the same
name on the command line worked, because cmd.py normalises it through Catalog.resolve().

setup_args() now resolves the pair through the same Catalog.resolve() and keeps the old value when it
cannot. The except is deliberately broad: the resolver reads the model table, and a table that cannot be
read here must not turn a launch that worked into a traceback - the values then go on to setup unchanged,
exactly as before. The new test drives that case with a resolver that raises.
2026-10-28 23:31:20 +01:00
Niko1221 c3326979fc docs/INTEL_ARC.md: the CPU and model-folder paragraph in plain words 2026-10-28 23:15:08 +01:00
Niko1221 b2dbd9420d dsv41 serve: the cache settle runs in the idle between requests (WP-8's option 1)
The prompt-side settle is 800 ms because it sits inside the prompt's own time, and for a run with no next request
(the CLI, a one-shot tool call) that is the right price.  In a server it is not: an agent's turn ends, it runs its
tool for a second or ten, and that time is free for the swap rounds the *next* answer needs.

Measured why it matters (job d18-attrib, 2026-10-28): the same candidate engine with the settle at 6,000 ms
decodes a 256-token code answer at 10.92 / 11.14 tok/s, at 800 ms at 9.82 / 9.62 - 12% paid for the prompt.
The idle settle is meant to give that back without returning the 6 s to the prompt.

How: Model::settle_idle(cancel) runs cache_settle's swap rounds on its own thread, without the RAM store preload,
for at most DS4_SETTLE_IDLE_MS (6,000) and stopping between rounds when cancel is set.  The serve loop starts it
at the top of each iteration (nothing in flight), and cancels and joins it the moment a line arrives, before any
request work touches the cache.  DS4_SETTLE_IDLE=0 turns it off; the prompt-side budget is unchanged.

Not measured yet: the benefit needs a serve-path harness (two GEN requests with an idle between them).  On the CLI
the thread is never started, so no CLI number moves.
2026-10-28 17:00:25 +01:00
Niko1221 7a80b3a96b Say plainly why a CPU without AVX-512 stops the engine: the pack folder without native_experts.txt, not the CPU, is the cause; doctor warns before the start, the launcher gives the next step, the SYCL engine names the folder like the CUDA one does 2026-10-28 15:37:12 +01:00
Niko1221 d52b73dc65 DS4_FAT_ROWS follow-up to the port: launches through strata_ds4_launch (the Mac build), off on Metal, only for the types f16_rows decodes (no trellis, no Q5_K / Q6_K down: a request would have stopped on 'not decoded'), at least 17 rows (the light kernels own the small experts) 2026-10-28 15:13:37 +01:00
Lyes SBAHI 0c5b157fbd DeepSeek engine opt-in: the busy experts of a prompt take FP16 GEMMs, not the tiles (DS4_FAT_ROWS=N, default 0: byte-identical path, the packed tiles run exactly as before). An expert of N rows or more has its weights decoded to FP16 once (iq::f16_rows, the values the tiles multiply), the rows gathered (gather_f16), cublasGemmEx, clamped SwiGLU in FP16 (swiglu_f16); SetArgs skip_ge leaves those experts out of the tiles. The tiles decode an expert's weights again for every 64 of its rows; a popular expert of a long chunk has hundreds. Measured on the RTX 5090 with the IST-DAS 2.25 bpw pack, v0.2.7 base, DS4_FAT_ROWS=64: a 13K-token prompt 1,400 -> 1,950 tok/s, a 5.4K prompt 1,065 -> 1,181, decode unchanged (28-33 short, 47-49 warm). 3x V100, Maya-XS: +6-8% on 8K and 32K prompts. The same products, the sums' order differs: greedy output can diverge after the first rounding (wording equivalent); KL vs the reference 0.276, the stock path 0.285 (the same band, measured on Maya-XS; not yet re-measured on this base). 2026-10-28 15:07:48 +01:00
Niko1221 c697e859e9 dsv41: the matrix-core prompt attention (DS4_FAST_ATTN) on by default for gfx1151 on Windows
The switch was on for gfx12 only, and ds4_kernels.cu said why: "gfx11 and gfx1151 (Strix Halo) are untested
and keep the scalar kernels".  Measured on Strix Halo 2026-10-28 (2.00 bpw, 32K prompt, the synchronised
chunk profile): the sparse-attention stage is 43.3% of the prompt's GPU time and runs at 2.3 TFLOPS
effective; with DS4_FAST_ATTN=1 the stage falls from 38,340 ms to 6,976 ms and the prompt from 101.6 s to
70.8 s - +43.6%, 322.5 -> 463.0 tok/s - with the same greedy first token (27408,).

The plan's D-P3a had closed this on +1.6 / -0.2 / +1.2 / -2.8% end to end, which is what 4K-8K shows; at
32K the term is five times its 4K size and the switch is the whole difference.

fast_attn_default() takes the gfx1151-on-Windows flag as its own argument (the existing three-argument calls
in the test are updated), ds4_kernels.cu decides it from the device name once, and experts_rule_test covers
the new cell: gfx1151 Windows on, 0.2.6 arithmetic off, DS4_FAST_ATTN=0 off, =1 on anywhere.
2026-10-28 14:04:56 +01:00
Niko1221 2ea46ded73 dsv41: the post-prompt cache settle's budget 6,000 -> 800 ms on every platform
The settle sits inside the prompt's own time: prompt: N tokens in X ms includes it, so a 6 s budget was
6 s of every DeepSeek first token.  Measured on Strix Halo 2026-10-28 (p4k fixture, three interleaved rounds
each, --dspark off): 6,000 ms -> 153.1 tok/s, 800 ms -> 190.0 (+24.1%), 0 -> 197.4 (+29.0%); the decode of
the 256-token answer that follows pays 2.9% at 800 ms and 3.8% at 0.  800 ms keeps 1,412-1,436 swaps against
the 6,000's 987-1,046, so it is still moving the hot experts in, it just stops paying for the last 5 s.

An agent turn's answer is 50-300 tokens (where the swaps buy nothing) and its first token is the 800 ms, so
this is the plan's WP-8 option (2) without the server-loop work of the idle settle (option 1, still the better
answer and not done here).

DS4_SETTLE_MS still overrides it (0 = off, any value in ms).
2026-10-28 14:04:52 +01:00
Niko1221 20bba22af7 gfx1151 on Windows: a 16,384-token prompt chunk, and the docs for the two new prompt defaults
- src/program/generate.cpp: the gfx1151 Windows prompt chunk goes from 8,192 to 16,384.  The prompt reads every expert once per
  chunk, so a bigger chunk reads them fewer times: 32K 1,250.6-1,260.0 -> 1,286.3-1,307.9 tok/s, 64K 1,188.4-1,191.0 ->
  1,234.3-1,238.2 (+3.0% / +3.9%, interleaved pairs); at 4K and 8K the chunk is the prompt and nothing changes.  The buffers'
  reserve grows to ~10.9 GiB and the cache keeps all 24,576 slots (84.7 GiB free at a 64K context).  STRATA_GFX1151_CHUNK=0
  keeps 6,144; auto:32768 is another ~2% at 64K and stays opt-in.
- docs/STRIX_HALO_WINDOWS.md section 4: what 0.2.8 turns on for gfx1151 on Windows (the five-switch matrix-core set and the
  prompt keeping its own buffers), the measured prompt table with the opt-outs, the chunk.
- docs/STRIX_HALO.md section 5: the Windows gfx1151 exception to 'the set is opt-in on an integrated Radeon'.

Numbers: Qwen3.8-Flash-Next IQ3_S, engine 0.1.52, prompts 4K to 64K, x1.59 to x1.72 over 0.2.7; first-token KL 0.0014 mean
(bar 0.0228); the own-buffer path is bit-exact (KL 0.00000 against the lending engine with the same switches).
2026-10-28 11:15:18 +01:00
Niko1221 1493781e91 gfx1151 on Windows: the matrix-core prompt set by default, and the prompt keeps its own buffers
Two prompt defaults for Strix Halo (gfx1151) on Windows, measured on the strix box with the
0.1.52 engine and Qwen3.8-Flash-Next IQ3_S (ids prompts of 4,095 / 8,190 / 11,999 tokens,
interleaved pairs, one engine at a time):

1. The matrix-core prompt set (HIP_WMMA, PF_GEMM, PA_FAST, HC_UPMIX, PF_FUSED) is on by
   default, as it has been on the Radeon 780M since 021-minipc: 4K 688.8 -> 1,192.8 tok/s,
   8K 787.0 -> 1,283.5, 12K 750.4 -> 1,256.5 (x1.63 to x1.72).  SELECT_WMMA stays out
   (x1.003 at 8K on the f-024-strix-pp branch); PF_F16 was measured and adds nothing on
   this pack, so the bit-changing surface is the five switches the branch gated.
   First-token KL against the 0.2.7 default 0.00210 / 0.00039 / 0.00214 at 4K / 8K / 12K,
   top-1 the same, noise floor 0.00000 (the accepted HC_Q8 bar is 0.0228 mean).

2. The prompt path keeps its own buffers instead of lending expert-cache slots.  A lent
   slot's expert is re-read from the host arena during every chunk at ~0.5 GB/s here (the
   arena is unregistered in UMA mode): 823 slots = 2.75 s of a 3.3 s 4K prompt in the
   issuer thread.  With own buffers every expert stays resident (24,576 slots, 'experts
   streamed 0') and the prompt is 8.5% (4K) to 8.9% (8K) faster.  STRATA_PREFILL_BORROW=1
   keeps 0.2.7's lending, STRATA_GFX1151_DEFAULTS=0 the whole block; --prefill auto keeps
   the 8,192-token chunk the gfx1151 rule chose instead of falling back to 2,048.

arch_defaults_test: the Windows gfx1151 expectations and the new opt-outs.
2026-10-28 11:15:18 +01:00
Niko1221 46c152a131 Merge f-0271-macds: the Mac signing kit signs and notarises the DeepSeek engine folder (--engine) 2026-10-28 09:11:14 +01:00
Niko1221 78b99a6605 packaging/mac: sign-and-notarize.sh --engine signs and notarises the Mac DeepSeek engine folder (Developer ID, hardened runtime, manifest updated, signed folder tar) 2026-10-28 09:10:05 +01:00
Niko1221 55a557bb6d Merge f-0271-macds: the Mac reads DeepSeek's Engram rows with two threads (identical words run to run); DEEPSEEK_MAC.md numbers from that build 2026-10-28 06:20:11 +01:00
Niko1221 ed1e5546c2 metal: the decode-only Engram cap did not hold (reverted); DEEPSEEK_MAC.md and the plan's numbers from the two-thread build, with the prompt-speed cost 2026-10-28 06:19:41 +01:00
Niko1221 bc6c32555b metal: the Engram rows are read with two threads (more gave different words run to run; the cause is not found yet); a failed Engram row read is reported 2026-10-28 05:33:46 +01:00
Niko1221 a9686a900a Merge f-0271-macds docs (DEEPSEEK_MAC.md ranges and the measurement note) 2026-10-28 03:09:29 +01:00
Niko1221 b90c8a2172 docs/DGX_SPARK_LINUX.md: the not-tested line matches the measured four and eight chat slots 2026-10-28 03:09:29 +01:00
Niko1221 979639e9cd docs: DEEPSEEK_MAC.md says the speeds were measured beside another program and how the expert share is set 2026-10-28 01:04:00 +01:00
Niko1221 756c00fbf6 docs: DEEPSEEK_MAC.md ranges from the runs before and after the buffer-mapping fix 2026-10-28 01:03:23 +01:00
Niko1221 9258e4086e Merge f-0271-macds (DeepSeek on the Mac) into 0.2.7.1: setup.py keeps the Mac sentence and adds the DGX Spark one for Linux ARM64 2026-10-28 00:33:44 +01:00
Niko1221 09c18d3e98 Tests: the feed tests know that unknown platforms are skipped, and the install.sh release-path test gives a Linux ARM64 host its own feed file 2026-10-28 00:15:56 +01:00
Niko1221 58f244ab1e The feed parser skips what it does not know: an engine, DeepSeek engine, Control or app entry for an unknown platform, backend or archive format is left out with one plain line instead of refusing the whole feed 2026-10-28 00:15:56 +01:00
Niko1221 39c5534ed3 Linux ARM64 reads its own signed feed file (strata-feed-linux-aarch64.json): a Control up to 0.1.52 refuses a feed with a platform it does not know, so the aarch64 entries stay out of strata-feed.json 2026-10-28 00:15:56 +01:00
Niko1221 2a220c1d1c metal: a kernel's buffer binding never runs past the allocation that holds it (sparse_attn asked for one row more than the window buffer: a second GPU mapping of the same pages read stale rows; short prompts and follow-up turns answered differently on every run); the Mac engine build script always runs ninja 2026-10-28 00:11:30 +01:00
Niko1221 87a755bf1e README and install guide: the DGX Spark (Linux ARM64) row, pointing at the DGX Spark guide 2026-10-27 22:53:16 +01:00
Niko1221 3939193dd3 mac: the setup test question keeps the engine's own prompt window (a 16-token window gave a wrong first word on Metal) 2026-10-27 22:22:57 +01:00
Niko1221 864afc7421 mac: the DSpark message says it is not supported on a Mac yet; --dspark on warns before loading (engine and launch) 2026-10-27 21:58:12 +01:00
Niko1221 d50f6a77ed metal: the DSpark drafter stays off on a Mac unless asked for (its quantised Markov bias has no Metal kernel; the setup test stopped with kernel launch with no Metal dispatch) 2026-10-27 21:55:37 +01:00
Niko1221 5729a6e50a docs: DEEPSEEK_MAC.md with the numbers of the 0.2.7.1 build 2026-10-27 21:50:39 +01:00
Niko1221 48c8e85843 mac plan: measured numbers and expert share of the 0.2.7.1 build (68-69 GB on a 128 GB Mac) 2026-10-27 21:47:09 +01:00
Niko1221 4daa83e75f docs/DGX_SPARK_LINUX.md: the 61-task quality suite with the sm_121 defaults on and off 2026-10-27 21:34:05 +01:00
Niko1221 5cfe96ee6a docs: DeepSeek on the Mac in MAC.md, MODELS.md and README 2026-10-27 21:23:06 +01:00
Niko1221 845941f522 metal: cand_blocks ran the block maxima and returned before the block selection: contexts past 16K tokens read an unwritten candidate list (needle lost at 17K+) 2026-10-27 21:17:42 +01:00
Niko1221 e3ed743f21 metal: the expert share leaves room for other programs' anonymous memory (a 60 GB Qwen server beside the engine froze the M5 Max twice) 2026-10-27 20:54:49 +01:00
Niko1221 c7980a3fe4 docs/DGX_SPARK_LINUX.md for 0.2.7.1: the Terminal install, the packages, the settings, the memory, the speeds and the quality of the sm_121 defaults as measured on the release candidate 2026-10-27 19:36:46 +01:00
Niko1221 1a937829a5 Engine and Control 0.1.53 (the CMake project version, for the linux-aarch64 packages of 0.2.7.1)
Only the engine / Control number: the app, the Terminal and the Metal engine keep their versions here; the release bump of the other files
is the release's own commit.
2026-10-27 18:30:33 +01:00
Niko1221 773fc4e228 dgx_concurrent: --parallel N and --distinct (a prompt and a hidden code for every client, the answers checked: no error, readable, its own code and nobody else's) 2026-10-27 18:30:16 +01:00
Niko1221 732e4bfb60 DeepSeek on a Mac: drafter off in the config and the plan, per-set measured lines, plan words for a Mac that holds every expert 2026-10-27 18:07:26 +01:00
Niko1221 52ee842dbf test: the Mac launch list carries the two DeepSeek sets 2026-10-27 17:55:49 +01:00
Niko1221 b0e897b41f Tests: the terminal suites know a linux-aarch64 host (fixture Terminal named for the host; HIP-only tests skip where there is no HIP engine); pins and locks tests list the new target 2026-10-27 17:47:10 +01:00
Niko1221 6a51355051 metal runtime: the DeepSeek queue/residency/barrier rules sit behind STRATA_DS4_RUNTIME; strata_ds4 compiles its own copy of mtl_rt.mm and gpu/runtime.cpp, the Qwen engine keeps the 0.2.7 code 2026-10-27 17:46:26 +01:00
Niko1221 8f7d53ccd5 SetArgs: the Metal fields exist only in Metal builds (the CUDA/HIP kernels take the struct by value) 2026-10-27 17:38:12 +01:00
Niko1221 2cb47c34c2 DeepSeek on a Mac: install plan (text only, 8K, the two sets), tests 2026-10-27 17:32:29 +01:00
Niko1221 232fa357fd SetArgs: the Metal fields go last, so the CUDA/HIP kernels' parameter layout (and SASS) is exactly v0.2.7's 2026-10-27 17:30:28 +01:00
Niko1221 828fc80004 mac package script: --no-picture-reader (the DeepSeek package is text-only on a Mac) 2026-10-27 17:27:05 +01:00
Niko1221 1fca685d6b Mac DeepSeek engine package: strata-dsv41 entrypoint, configure+build in the script, build id and version in the binary 2026-10-27 17:25:26 +01:00
Niko1221 cb32d8758d dgx_gate_ref.py: the 24-prompt identity gate against a recorded answer file (the handover's quality24 JSON) 2026-10-27 17:22:14 +01:00
Niko1221 fa370e46d1 setup.py: DeepSeek on a Mac goes through install_dsv41 (Control mode); launch passes --feed/--dsv41-package on a Mac; model-list tests 2026-10-27 17:18:07 +01:00
Niko1221 2e7af09a86 DeepSeek on a Mac: the fit plan and the model list (dsv41_fit._plan_mac, strata_mcp._mac_deepseek) 2026-10-27 17:16:15 +01:00
Niko1221 b4814a9fb1 dgx_smoke: the setup check is read case-insensitively (setup prints ARM64) and reports the GPU line 2026-10-27 17:15:51 +01:00
Niko1221 d551ab0282 DeepSeek Metal port: DS4_FAKE_CPU_US reads its own value (build fix found by the HIP build) 2026-10-27 17:13:08 +01:00
Niko1221 43a328d8b6 Packaging docs for linux-aarch64; test_install_scripts_env knows install.sh now installs on Linux ARM64 2026-10-27 17:12:36 +01:00
Niko1221 e6fd914616 The Terminal on a GB10: its GPU is found, listed and planned as one unified-memory pool
nvidia-smi gives a GB10's memory.total as [N/A]. tools/strata_mcp.py's own NVIDIA probe (the fallback of the launch hardware page)
dropped the card; it now counts it as setup.py's gpus() does (arm64 only). strata doctor --live accepted no card with that figure; on arm64
it lists the card with no memory figure (x86 still refuses it). The DeepSeek fit plan for a unified-memory GPU with no BIOS carve-out
(GB10) no longer speaks of Windows, a BIOS and a Radeon, and quotes the numbers measured on the Spark. The Windows Strix Halo plan text
is unchanged (tests).
2026-10-27 17:12:36 +01:00
Niko1221 694e711708 The DGX Spark benchmark family and guide (from the owner's handover, 2026-10-26)
tools/dgx_bench.py, dgx_warm.py, dgx_compare.py, dgx_quality.py, dgx_quality24.py, dgx_needle.py, dgx_turns.py, dgx_longctx.py, dgx_128k.py,
dgx_kvresident*.py, dgx_concurrent.py and dgx_smoke.py (the acceptance gate in one command; its data folder is now an option and its scratch
files live in a folder of their own) and docs/DGX_SPARK_LINUX.md.
2026-10-27 17:12:36 +01:00
Niko1221 8544420180 nv_defaults_test: the sm_121 table, its two off switches (STRATA_SM121_PERF=0, STRATA_DEFAULTS=026) and that the other tables are untouched 2026-10-27 17:12:36 +01:00
Niko1221 f436dba81b The linux-aarch64 platform: engine packages, Control (Terminal) and the signed feed for the DGX Spark
- strata_control/targets.py: host_platform() names Linux ARM64 'linux-aarch64' (as uname and Tauri do; a Mac stays macos-arm64) and the
  cuda backend is committed to there (nothing else is: no HIP, SYCL or CPU engine for arm64 Linux)
- strata_control/feed.py: linux-aarch64 is a feed platform for engine, dsv41 and control packages (and a Tauri target name, no app is built)
- packaging/package_terminal.py: the Terminal for linux-aarch64 with python-build-standalone 3.12.15 aarch64-unknown-linux-gnu (SHA-256 from
  the release's SHA256SUMS); packaging/lock_wheels.py + locks/runtime-linux-aarch64-cp312.txt: its hash-pinned wheels (the same 23 packages
  as the x86_64 lock; platformdirs resolved to 4.13.6 against 4.13.0 there)
- packaging/make_local_feed.py: --dsv41 for the DeepSeek packages, linux-aarch64 terminals; packaging/install.sh installs on Linux ARM64
  and says the engine is for the DGX Spark (GB10) only, instead of refusing
- packaging/build_cuda_linux_aarch64.sh: the release recipe (strata + strata-dsv41 + the reader, the engine package and the separate
  DeepSeek package, sm_121 + compute_121, driver 580.65, build id naming the commit)
- tests: test_linux_aarch64.py, the Spark rows in test_release_matrix.py

Nothing changes for another platform: every edit is a new table row, a new branch for aarch64 Linux or a new file.
2026-10-27 17:12:36 +01:00
Niko1221 552c56f7a7 setup.py on a DGX Spark: unified-memory GB10, its engine from the signed feed, the measured run-config settings
- gpus(): a GPU whose memory nvidia-smi does not report (GB10) on arm64 is counted as Strix Halo's is: the RAM less the OS share
  (UMA_OS_LEFT_GB), uma True, dedicated_gb 0, so the low-RAM and fit rules do not count the same memory twice. #3622 listed the whole RAM
  as VRAM beside the RAM.
- cpu_info(): an arm64 CPU is named from lscpu (Cortex-X925 + Cortex-A725), not 'unknown CPU'. The PCIe probe is skipped for a
  unified-memory GPU (nvidia-smi's 1.0 x1 there means nothing).
- get_prebuilt(): on arm64 Linux the ready-made engine is the linux-aarch64 package of the signed feed (engine_from_feed); without one,
  setup compiles as #3622 had it do always. setup no longer forces --build on arm64.
- Step 7 on a GB10 (is_gb10: uma, sm_121, arm64 Linux, nothing else): --mmap-experts (one copy of the experts, all 24,576 cache
  slots), --prefill auto:32768, --mtp-q4 all, --mtp-hnorm stream, each measured on one GB10 with the answers unchanged; --draft-vocab
  en is printed as a tip, not set.
- The CUDA repository key URL is sbsa on arm64; the DeepSeek-engine-missing message names the Spark build script on arm64.
Tests: test_setup_arm64.py (unified-memory shape, is_gb10, lscpu name); test_setup_pins.py skips its x86-zip Engine tests on arm64 Linux.
2026-10-27 17:12:36 +01:00
Niko1221 e3d8823e6c DeepSeek on arm64 Linux: the same two compile fixes as the Metal branch (amd_family() on a non-x86 CPU, iq256x stubs in portable.cpp)
Taken verbatim from 756380301 so the two branches merge without a conflict.
2026-10-27 17:12:36 +01:00
Niko1221 9a398bd5bf sm_121 (GB10, DGX Spark) speed defaults and the fused prompt tile
nv_defaults.cpp gets an sm_121 table beside sm_86 and sm_60: STRATA_PF_FUSED=1 (fused IQ prompt kernels), STRATA_PREFILL_NVFP4=w4a4
(NVFP4 prompt MMQ with FP4 activations), STRATA_HC_UPMIX=1 (fused hyper-connection read), STRATA_SPEC_COST=1 (cost-aware MTP window).
Measured on one GB10 with paired A/Bs (docs/DGX_SPARK_LINUX.md); a switch the user set is kept. STRATA_SM121_PERF=0 turns the table
off, and so does the 0.2.7 master switch STRATA_DEFAULTS=026. Unlike the sm_86 / sm_60 tables these are not bit-exact (the fused prompt
path and the FP4 activations round differently; 24 of 24 greedy answers were identical), so the start line does not call them exact.

moe_fused_iq.cu: on a GB10 the work-item rule never reached its 128-row tile (the launch average is above 112 rows an expert at every
prompt size), so pick_ww takes the 128-row tile from 65,536 routed rows a launch up (+3% prompt at 16K and 32K; 4K keeps 64).
STRATA_PF_TILE_DEBUG=1 prints the choice. Other parts take the line they took before.
2026-10-27 17:12:36 +01:00
Niko1221 4e06c84b91 DeepSeek engine on an integrated NVIDIA GPU (DGX Spark): arm64 build and placement from MemAvailable
- ds4_gg_cpu.cpp: amd_family() on a non-x86 CPU read no variable the rest of the function uses; it now reports no AMD family
- exl3_cpu.cpp: kGemmK moved above the AVX-512 section, as exl3_cpu_gemm_scratch() sizes by it on every CPU
- ds4_model.cu: uma_free(): on an integrated NVIDIA GPU on Linux cudaMemGetInfo counts only MemFree, while the model files' page cache
  holds most of what the kernel can still give back. The placement, the CPU-lane warm-up and the vision tower size their room from
  MemAvailable less DS4_UMA_HEADROOM_GIB (default 6) when that is larger. A discrete GPU, HIP and Windows are not touched.
  On a GB10: 12,163-12,226 of 15,744 experts in graphics memory where cudaMemGetInfo's free placed 11,961.
2026-10-27 17:12:36 +01:00
Niko1221 f262b81b28 Engine for the DGX Spark: the arm64 CUDA build on top of #3622
#3622 made the strata target compile on aarch64. This adds what a Spark build needs beyond that:
- the trellis dispatcher (tq_cpu.cpp, its scalar rows) is part of the arm64 kernel library, as it is in the Metal build, so the
  DeepSeek engine's CPU lane links; ds4_tq_cpu.cpp names the AVX units on x86-64 only
- a STRATA_PORTABLE build on arm64 sets ggml-cpu's -march to armv8.2-a+dotprod+fp16+i8mm (STRATA_ARM64_ARCH) instead of the build
  host's; GGML_NATIVE OFF alone would leave plain armv8-a and no NEON dot products
- the x86 kernels' parity programs and benches are not configured on arm64; the test suite defaults off there (-DSTRATA_BUILD_TESTS=ON
  still builds what builds)
- spark/build_engine.sh: the reproducible build of strata, strata-dsv41 and the picture reader into an engine folder, with the CUDA
  libraries beside them (RUNPATH $ORIGIN)

Every change is behind STRATA_CPU_X86 being OFF or inside a new file; the x86 and Metal builds do not take it.
2026-10-27 17:12:36 +01:00
Niko1221 56ab127c87 DeepSeek Metal port: keep the CUDA/HIP paths as in 0.2.7 (chunk-path hc mix through cuBLAS, async embedding upload, no extra h16 rows, no diagnostics on hot paths, type_on_chunk true off Metal) 2026-10-27 17:07:21 +01:00
Niko1221 5ed8ae03be launch header: exl3.cu includes it for every backend; exl3_cpu kGemmK only off x86 2026-10-27 17:01:02 +01:00
Niko1221 756380301b DeepSeek on Metal: compile fixes on the 0.2.7 tree (huge pages, wmma include, DeviceProp.integrated, iq256x stubs) 2026-10-27 16:49:46 +01:00
Niko1221 314a6ecf69 metal: the host sources no longer list iq256_portable.cpp (portable.cpp answers for non-x86 hosts) 2026-10-27 16:45:06 +01:00
Niko1221 8477811197 Merge f-dsv41-mac (DeepSeek on Metal) into v0.2.7
The 0.2.7 DeepSeek changes (persistent-grid IQ kernels, int8 rounding batches, hc_mix_split with a block counter, head columns,
chunk-path knobs) are kept; the Metal engine's hooks sit on top of them: every kernel launch is strata_ds4_launch(), the rounding
batch ends in the Metal multi-quantiser dispatch, the head stays at 4 columns a pass on Metal (8 moves the KL), and the macOS
parallel expert prefetch is macOS-only.
2026-10-27 16:44:27 +01:00
Niko1221 5e8a4cca7d Release notes 0.2.7: update, tray and speed lines confirmed on the release packages; Strix Halo guide names the prompt used for the 4K row 2026-10-27 14:54:33 +01:00
Niko1221 a42cc3d7df Version 0.2.7: Terminal and app 0.2.7, engine and Control 0.1.52 (every Qwen and DeepSeek package, Metal included; both engines changed since 0.2.6)
Same 15 files as the 0.2.6 bump (issue template, AGENTS.md, CMakeLists.txt 0.1.52, desktop and host package files, Cargo.toml and the strata-desktop entry in Cargo.lock, SettingsUpdates comment, qa1-storage test, metal 0.1.52-metal, build_hip_linux.sh PKGVER 0.1.52, strata_cli PRODUCT_VERSION, the two version tests), plus the Strata-0.2.7-<file> names in the md files.
2026-10-27 09:13:22 +01:00
Niko1221 5a843fd841 docs: release notes 0.2.7 - two-card pipelined default (owner choice a), DeepSeek pictures by default, the shard 1 setup fix 2026-10-27 09:13:21 +01:00
Niko1221 e6d5d76010 Merge f-027-dsvis: DeepSeek reads pictures by default (owner yes), and setup accepts ISTA's re-published shard 1
The engine's own tower (319 MiB) through strata launch and setup (--vision off still turns it off); setup fetches
the pinned 972 MB mmproj; the server sends pictures to the tower. Checked on 4x R9700, 1x R9700 and a real
launch on 2x A6000 (download, pack, picture answer over HTTP); 0/10 stalls, KL unchanged. Also: a newer shard 1
from --model-path (ISTA's 2026-10-26 chat template) is accepted after its own size and SHA-256 check.
2026-10-27 09:10:53 +01:00
Niko1221 2526f2ff99 Merge two-card batch default (owner choice a): two NVIDIA cards pipeline by default
Pipelined groups beat the serial default at 2 and 4 streams on 2x A6000 (+17/+22%) and 2x A4000 (+7..12/+17..20%);
one stream equal, -4% on A4000 IQ3_XXS. STRATA_BATCH_GROUPS_NV_SERIAL=1 or STRATA_DEFAULTS=026 gives serial.
2026-10-27 09:10:53 +01:00
Niko1221 94d7aab293 setup: accept the DeepSeek shard 1 with ISTA's 2026-10-26 chat template from files already here
ISTA re-published (and patch_chat_template.py rewrites) the first shard of each set with a larger chat template, so a shard 1 on disk can be 11 KB
larger than the pinned revision's. setup refused it (size mismatch; the GGUF preflight does not read DeepSeek tensors, so it was not taken as a
whole own file). The pinned revision stays the download; the newer shard 1 is accepted by its own size and SHA-256 with --gguf-dir.
2026-10-27 07:18:19 +01:00
Niko1221 1bd17d59d9 launch: the model state's DeepSeek check tolerates a catalog without the strata table (test_model_folder_case) 2026-10-27 06:22:02 +01:00
Niko1221 c451698d6a DeepSeek pictures: the Terminal's pictures step knows the tower is part of the engine (no reader lookup, no GPU/CPU reader choice), and a DeepSeek start without pictures answers a picture request with its own 400 wording 2026-10-27 06:12:25 +01:00
Niko1221 1e617c3f64 DeepSeek pictures by default: setup fetches the vision tower's weights (mmproj-BF16.gguf, pinned) with the shared files, packs them with the model (pack_gguf --mmproj) and rebuilds a pack made without them; --vision no leaves all of it out; the picture-reader parts of the launcher's model state skip DeepSeek (its tower is part of the engine); the installed-config description says pictures on for a --vision on DeepSeek config; tests 2026-10-27 05:49:37 +01:00
Niko1221 9b177f3080 DeepSeek reads pictures by default: strata launch and setup pass --vision on (the engine's own 319 MiB tower, no separate reader), the server sends a picture to that tower as raw RGB in the file of a GENI line (serve/ds_pictures.py: decode with Pillow, both sides a multiple of 42, at most 1024 image tokens, the template's placeholder expanded to the span), --vision off keeps pictures off and the old 400; the plan, the catalog and docs/DEEPSEEK41.md say pictures on; tests for the size rule, the config switch, the prompt expansion and the server route 2026-10-27 05:41:48 +01:00
Niko1221 4c7bce3ad1 generate: the gated-proposal check reads use_df2 only where the DFlash2 drafter is built (the Metal build has none; #3416 broke its compile) 2026-10-27 05:26:53 +01:00
Niko1221 8dce24a0a1 docs: release notes - the two-card batch default marker uses the owner's labels (a) pipelined by default, (b) pipelined only when every expert fits, (c) leave serial; the serial against pipelined numbers are batch scheduling without drafting 2026-10-27 04:33:31 +01:00
Niko1221 12d0f0abe8 docs: release notes 0.2.7 - the engine items from f-027-gh-eng filled in (layer-major prefill on by default for a two-card CUDA split, pipelined drafts opt-in, sm_80 and sm_89 defaults, audit fixes, opt-in switches), markers closed except the owner's two-card batch choice 2026-10-27 04:32:24 +01:00
Niko1221 ac19fecc06 Merge release-notes update: route prior 1.5 wording with the agent-loop caveat 2026-10-27 04:28:46 +01:00
Niko1221 6ca60e9a0d Merge f-027-gh-eng: GitHub engine items for 0.2.7
P0 #3911 #1503; P1 fixes; file-02 items; 22 PR ports (authors kept) incl. #3779 (sm_80 defaults under
STRATA_DEFAULTS=026) and #4121 (STRATA_PIPED_MTP opt-in); audit fixes #3602 #3948 #3638 #3797 #2409; owner
decisions #3935 (DeepSeek sm_89 packages), #3844, #3453 (sm_89 tables on), #3976 (layer-major prefill on by
default for a 2-card CUDA layer split; revert 1e41fa269 to undo). Final gate: sm_60, SYCL, z440 sm_86 CPU 38/38
GPU 23/23, boston CPU 24/24 GPU 23/23, owner-PC MSVC CPU 36/36 GPU 20/20, snapshot 10/10 on z440 and boston.
2026-10-27 04:28:36 +01:00
Niko1221 c41bd44de7 docs: route prior 1.5, final verdict of the 80-task check on the RTX 3060 (no copy or code errors; agent loops may finish slightly less often, within the run-to-run spread), speed and the off switch 2026-10-27 04:28:28 +01:00
Niko1221 447460e8cc native experts: the window-invariant reference rows are used only where they match the linked ggml dot (#3416)
native_arithmetic_parity failed on an Intel i9-13900K (AVX2 + AVX-VNNI, ggml built native)
in five of its eight configurations: "gu=16 down=20 ff=96 nt=2 token=0". Not the VNNI row
path: STRATA_NO_AVXVNNI=1 changes nothing, STRATA_NO_IQ4NL=1 passes. The IQ4_NL down
reference (iq4nl_reference_rows) differs from that ggml's one-token dot on the scalar tail
block of a row with an odd number of blocks (ff=96 is three): that ggml does not contract
`sumf += d * sumi` into an FMA, the reference lets the compiler do it. On the other
machines tested the two agree, so it is a property of the ggml build, not of the CPU.
With an unfused tail in the reference all eight configurations pass on that machine.

The invariant path now checks each reference once against the linked ggml-cpu on synthetic
blocks before using it (iq4nl_reference_ok for IQ4_NL with 2, 3, 5 and 20 blocks;
iq512_reference_ok for types 18, 21 and 22 with SwiGLU, on AVX-512 CPUs). On a mismatch
it prints one line and the invariant path uses ggml's own one-token dot per token: slower
on such a build, never different. The Default path is not touched; only the opt-in
STRATA_SPEC_PROB=1 decode reaches this code.

Tests: native_arithmetic_parity, eight configurations, on the i9-13900K (4,866,048
comparisons, with the fallback line), z440 and the owner PC (see the series notes).
2026-10-27 04:18:06 +01:00
Niko1221 ec8e405909 batch groups: two NVIDIA cards pipeline by default; the serial group is opt-in (STRATA_BATCH_GROUPS_NV_SERIAL=1, STRATA_DEFAULTS=026)
0.2.1 made a layer split over exactly two NVIDIA cards with --batch and no --batch-groups run one serial group (measured
then on 2x RTX A4000, IQ3_XXS: +12..16%). With the 0.2.7 code the pipelined groups are faster at 2 and 4 streams on
every box and quant measured, so the default goes back to the pipeline.

Measured (tools/serve_load.py, --batch 4, 256 tokens per request, interleaved pairs, total tok/s, pipelined / serial):
- 2x RTX A6000, IQ3_S, all experts in VRAM: 1 stream 101.7 / 101.7; 2 streams 125.7 / 107.4 (+17%); 4 streams 171.4 / 141.0
  (+22%). The v0.2.6 binary on the same box: serial default 104.9 and 137.2, --batch-groups auto 124.6 and 169.6.
- 2x RTX A4000, Q2_0 pack (17037 of 24576 pairs cached, 4 clean pairs): 2 streams 80.2-82.5 / 75.0-75.5 (+7..9%, 4 of 4);
  4 streams 110.8-111.5 / 94.8-95.3 (+17%, 4 of 4); 1 stream 63.6-68.7 / 68.2-70.3.
- 2x RTX A4000, IQ3_XXS (13056 of 24576 pairs cached, 3 pairs): 2 streams 73.2-73.7 / 65.8-66.5 (+12%, 3 of 3); 4 streams
  100.5-100.7 / 82.9-83.6 (+20%, 3 of 3); 1 stream 58.4-60.5 / 60.7-62.6 (serial about 4% faster, 3 of 3).
The single stream runs on the solo path in both; only the A4000s show the 4%.

What changed: batch_groups.hpp has a pure rule nv_serial_wanted(env value, STRATA_DEFAULTS=026). STRATA_BATCH_GROUPS_NV_SERIAL
set to anything but 0 asks for the serial group, an explicit 0 wins over STRATA_DEFAULTS=026, and with the variable unset or
empty STRATA_DEFAULTS=026 gives v0.2.6's serial group. An explicit --batch-groups (a number or auto) still wins. Three or more
cards, AMD builds and --batch-mtp/--adapt-async handling are unchanged. The serial message names the opt-in.

Tests: batch_groups_test (the rule over the env values and 026, two cards pipeline, the existing serial cases). Real engine on
2x A6000 (IQ3_S), no --batch-groups: the engine says "2 groups of 2 slots" / "2 groups of 1 slot"; with 4 and 2 slots, greedy,
greedy+seeded sampled, slots finishing at different lengths and a cancel in the middle of a draft, every slot's tokens equal its
solo decode and the pipelined build's. A long-prompt slot differs from the pre-merge base build in the same way with the flag
and without it (it is the parent's prompt path, not this change).

Not covered: no run on AMD or on three cards; the 4% single-stream cost on the A4000s is measured on one box.
2026-10-27 04:17:17 +01:00
Niko1221 5ac66e5005 release notes 0.2.7: layer-major prefill (on by default on a CUDA layer split) and pipelined drafts filled in 2026-10-27 04:05:02 +01:00
Niko1221 e40843dade Merge f-027-int2 (c7c8c0d0a) into f-027-gh-eng
# Conflicts:
#	docs/release-notes-0.2.7.md
2026-10-27 04:04:41 +01:00
Niko1221 5cfa395476 Merge f-027-gh-eng-pm (#4121 MTP drafts in pipelined slots, opt-in STRATA_PIPED_MTP) 2026-10-27 04:04:21 +01:00
Niko1221 f656a721a6 docs: STRATA_PIPED_MTP on 2x RTX A4000 (Q2_0): =1 is the setting to use; =2 is slower than no drafts at 4 streams there (#4121) 2026-10-27 04:02:38 +01:00
Niko1221 42890ca79f metal: stubs for the layer-major prefill kernels (moe_shared_init, moe_scatter_add): the Mac build shares prefill.cpp, the walk is never taken there (#3976) 2026-10-27 03:29:12 +01:00
Niko1221 37e4832ab3 layer-major prefill: on by default for a CUDA layer split, STRATA_PREFILL_LAYER_MAJOR=0 and STRATA_DEFAULTS=026 turn it off (#3976)
THIS COMMIT ALONE MAKES THE WALK A DEFAULT. Drop it and the layer-major prefill stays
an opt-in (STRATA_PREFILL_LAYER_MAJOR=1) with the chunk-major path as the default.

Rule (include/strata/core/sm120_series.hpp, pure, tested in src/core/arch_defaults_test.cpp):
- prefill_layer_major_default(hip, stages, v026): on for two or more stages on CUDA,
  off on one GPU, in a HIP build, and under STRATA_DEFAULTS=026 (qwen_defaults_026());
- prefill_layer_major_resolve(env, ...): STRATA_PREFILL_LAYER_MAJOR=0 / 1 (any text
  atoi reads) wins in both directions, over the rule and over STRATA_DEFAULTS=026.
Every decline of the earlier commits stays and runs in lm_prepare as before (HIP,
image prompts, DFlash2 taps, KVA, stop_at_chunk, a pending force_chunk, a peer GPU,
APR / NVFP4 / WMMA / fused layers, STRATA_EXPERT_NIBBLE, no VRAM, no pinned RAM):
such a prompt reads chunk-major with nothing but a log line. When the walk is on by
default (nothing set) the VRAM margin is 512 MiB instead of 96 and the super-chunk
target 32768 tokens (the size measured) instead of 8192; "a short prompt" is not logged.
STRATA_PREFILL_LM_TOKENS and STRATA_PREFILL_LM_MARGIN_MIB still win.

Measured (z440, 2x RTX A4000 16 GB, Qwen3.8-Flash-Next IQ3_XXS, experts streamed, 5
interleaved pairs, time to first token, medians): 8K 2.8x, 20K 4.8-5.7x, 32K 5.1-5.5x
faster (141 s to 26-28 s); decode 38.6 vs 38.4 tok/s; needles 12 of 12 found in both
arms. The speed-up is larger than the report's 2x because --prefill auto picks 512-token
chunks there. The first-token KL numbers are in the gate notes (3976-kl.sh).

Not covered: one GPU (stays opt-in), HIP, SYCL, Metal, three or more stages, a card
with more VRAM than the pack needs (where the experts are VRAM-resident and the
chunk-major path has little to re-read).
2026-10-27 03:28:36 +01:00
Niko1221 1c2efd789a Merge f-027-gh-eng-d again (9d85432b8: QFUSE one-token self-commit) 2026-10-27 03:12:14 +01:00
Niko1221 0d4b2cdd43 Merge f-027-gh-eng-lm up to 0c7de9868 (#3976 layer-major split prefill, opt-in: STRATA_PREFILL_LAYER_MAJOR=1) 2026-10-27 03:12:05 +01:00
Niko1221 789d83903b STRATA_QFUSE=1: a one-token window did not self-commit, but commit() skipped the commit graph as if it had (#3948)
Problem. #3948: with STRATA_QFUSE=1 the verified text changes and MTP acceptance drops, on CUDA. The QFUSE kernels are bit-exact
(gr_parity, gdn_parity, 10,500 random windows each on sm_86 and sm_120, after 91a4b8a67), yet the engine's greedy text still differed
from QFUSE off on every prompt tried. Model run, l620 (RTX PRO 5000, sm_120), Qwen3.8-Flash-Next IQ3_S, MTP --spec 6, identity-gate
flags, 4 prompts of 4K and 12K tokens, 96 greedy tokens: QFUSE on differed from off on 4 of 4 (the off-vs-off control was identical).

Cause. A window of one token advances the GDN state and the indexer tail itself ("self-commit") so that commit() needs no commit
graph. Verifier::record_window decides this with
`T == 1 && !always_publish_ && !batch_rec_ && !g_qfuse() && one_token_self_commit()`: with STRATA_QFUSE the fused GDN output
kernel takes the q8_1 destination and the window does not self-commit. Verifier::commit() skipped the commit graph with
`last_t_ == 1 && !always_publish_ && one_token_self_commit()`, without the QFUSE term. So with QFUSE on, every accepted one-token
window left the recurrent state (and the indexer tail) one token behind. Windows of one token are the ordinary case when the drafter
offers nothing, which matches the report: fewer drafts offered, lower acceptance, different text, and an unchanged text where
no window of one token occurs. On HIP (where QFUSE is the default on gfx1151) one_token_self_commit() is off unless
STRATA_ONE_TOKEN_COMMIT=1, so the two sides agreed there.

Change. commit() takes the same condition: `... && one_token_self_commit() && !g_qfuse()`. Without STRATA_QFUSE nothing changes.

Result (same l620 run). QFUSE on with this change against QFUSE off, same build: logits md5 and the 96 greedy tokens byte-identical
on all 4 prompts (IDENTITY PASS); the build without the change differs on all 4. The default path (QFUSE unset) of the merged head
against the f-027-int2 head (2a2264c21): first-window logits and greedy tokens byte-identical on the same 4 prompts.

Not covered. Serve answers and draft counters (3948-qfuse.sh step 2, SERVE=1), sm_86, and the reporter's 40-prompt set.
2026-10-27 03:11:15 +01:00
Niko1221 9c624c75a9 Merge f-027-gh-eng-d (#3602 #3948 #3638 #3702 #3797 #2409 #3844: audits, fixes and tests)
# Conflicts:
#	src/core/verify.cpp
#	src/kernels/cuda/fused_gr.cu
2026-10-27 02:48:36 +01:00
Niko1221 8dd297d0f1 PLE table: STRATA_PLE_CHECK=1 names the first row that decodes to an implausible value and says whether the delivered bytes are the file's (#3602)
The report asks for the first data mismatch between the GGUF row bytes and what the direct reader delivered, and for an early
check of the gathered values; neither exists. Its trace stops at "the embedding has 96 non-finite values" without saying whether
the reader gave a wrong row or the table holds one.

With STRATA_PLE_CHECK=1 (a tester switch, off by default; one static flag read per gather call when off) every direct-mode decode (read_row, collect,
gather_batch, also from the prefetched slots) checks the row's 160 values. For a value that is not finite or beyond 1e5 (an IQ4_NL
row cannot decode beyond 8.3e6; real PLE values are orders smaller) it re-reads that row from the file with a buffered read and prints
one line to stderr (at most 8 per table): the row, its file offset and page offset, the value, and either "the delivered bytes ARE the
file's bytes" or "THE DELIVERED BYTES DIFFER FROM THE FILE from byte N" with the first 8 bytes of each. That separates a reader
fault (the bug) from a damaged table. gates/3602-ple-direct.sh sets it in its direct arms.

Checked: ple_reader_test --selftest on z440 with the switch on prints the "ARE the file's bytes" line for the synthetic table's random
rows (which decode to huge values); with it off the output and the bytes are unchanged (ple_reader_selftest passes).
Not checked: the "DIFFER" branch has no natural trigger (that is the point); it was read, not run.
2026-10-27 02:10:24 +01:00
Niko1221 7d1016f3f4 QFUSE: the fused q8_1 producers divide the way the separate quantiser does; they gave a different code at about one element in a million (#3948)
Problem. #3948: with STRATA_QFUSE=1 the verified text changes and MTP acceptance drops (RTX 3080, sm_86). The report suspected the
group counter or the nat_xq_ offsets of fused_gr.cu; the audit and the new two-group test (previous commit of this stream)
rule those out. What the new test found instead, on an RTX 5070 (sm_120, CUDA 13.0, MSVC), over 10,500 random windows of two groups: in
about 1 run of 400 one q8_1 code of the fused image differs by exactly one from native_quantize_q8_1's, in every mode (group-relative,
window-indexed, direct, graph replay). The scale/sum pair and the other codes agree.

Cause. native_mmvq.cu, which holds native_quantize_q8_1 (the quantiser every consumer of a q8_1 image was written for), is
compiled with --use_fast_math (CMakeLists.txt, the native_* list), so its `amax / 127` and `x / d` are the fast full-range division
(div.full.f32, up to 2 ulp off). fused_gr.cu (the hyper-connection read's tail, gr_up_q32_kernel) and verify_kernels.cu (the GDN
output norm, gdn_q8_1_store) are not in that list and divide with IEEE rounding. roundf(x / d) then lands on the other side of a .5
at the rare element whose quotient is next to a boundary. One changed activation code per ~10^6 values is about one per few
hundred tokens at 80 hyper-connection reads of 2560 values per token: enough to move a near-tie in the verify, and the
draft (which quantises with the separate kernel) disagrees with the verifier a little more often, which lowers acceptance. That
matches the report (acceptance 0.755 against 0.79, different text, and an unchanged text where no draft is made).

Change. include/strata/kernels/q8_1_finite.hpp: q81_div(a, b) is `a / b`, except in a translation unit that defines
STRATA_Q81_FASTMATH_DIV (CUDA device code only, never HIP), where it is the same PTX division the fast-math file gets (div.full.f32).
q8_1_quant and the scale use it. fused_gr.cu and verify_kernels.cu define the macro; nothing else does, so every other kernel,
the separate quantiser and every HIP build compile to what they did. STRATA_QFUSE is off by default on CUDA (it is on by default on
gfx1151, which is HIP and unchanged), so the default path is bit-identical.

Result (RTX 5070, sm_120): gr_parity's two-group window test: 10,500 runs with 1,500 random inputs for each T, all modes, zero
differences (before: dozens); the whole of gr_parity, gdn_parity and gdn_parity with STRATA_GDN_SPLIT=1 pass.

Tests. gr_parity: the two-group test prints the first differing bytes (block, field), and STRATA_GRP_REPS=N / STRATA_GRP_KEEP_GOING=1
repeat it longer and count every failure. gdn_parity: the q8_1 destination check uses its own stream (native_quantize_q8_1 refuses the
null stream, which crashed the test on Windows), unbuffered stdout and STRATA_GDN_PARITY_TRACE=1.

Not covered. sm_86 (the report's card) has not run the new test yet (z440 was reserved for other jobs); the cause is not
architecture specific. The model run (gates/3948-qfuse.sh) is still owed: QFUSE on against off must now be byte-identical, and the
draft counters equal.
2026-10-27 02:02:46 +01:00
Niko1221 e0d47034df Presets: a value outside the range the tuner tries is kept with a warning, not dropped (#3844)
Problem. #3844: --spec 5 measured +6.1% decode on an RTX 5060 Ti, but a saved preset that carries it was left out entirely
("outside the safe values [3, 4]"), and an imported one was refused. The tuner's BOUNDS are the values it tries; they are
not a finding that a value is harmful. The never list (--spec 6 and wider: 4% to 16% slower, --expert-cache 0, ...) is.

Change (Terminal / Control; touches strata_control/tune_knobs.py, strata_control/presets.py, strata_cli/tune_commands.py and
serve/server.py).
- tune_knobs.check_setting_parts(args, env, ctx) returns (never, bounds). check_setting is the two lists together, in the
  order it had, so the tuner still skips --spec 5 and its tests are unchanged.
- presets.overlay applies a preset whose only problems are outside BOUNDS and returns info["warnings"], one short line each
  ("--spec 5: not tried by the tuner, kept because it is your setting"). A never-list setting still means "not applied".
  presets.create and import_preset refuse only never-list settings.
- serve/server.py prints each warning as "[strata] warning: ..." when the preset is applied; strata_cli tune_commands
  _effective adds "warnings" to its payload.

Tests. tests/tuning/test_presets.py: a preset with --spec 5 is saved, applied (the flag becomes 5, the input config is not
modified), warns once, and an in-range preset has no warnings; the never list still refuses --spec 6; import of --spec 5 works.
tests/tuning/test_autotune.py: --spec 5 stays in the tuner's refused list. python -m pytest tests/tuning: 274 passed;
tests/terminal -k "tun or preset or overlay": passed.

Not covered. The app's Details text does not show the warning yet (it reads the payload key above).
2026-10-27 02:02:46 +01:00
Niko1221 8c0301484e Conversation cache: skip a restore only when a card would have to create its copy stream and has no room for it (#2409)
Problem. #2408 / PR #2409: restoring a large parked conversation on a layer split ended the server with a CUDA out of memory
on a later card. The PR checks the free VRAM of every card against --vram-reserve-mib before a restore. Its own review
points out that a loaded card is always below that reserve, so the check would skip nearly every restore on a split (and
on one GPU). It is not ported.

What a restore allocates. conversation_snapshot_restore writes into the session's existing buffers (pools(st), the GDN
state, the indexer tails); refresh_residency only launches kv_stream_reset or copies into the existing ring. The elastic K/V
growth (kvg_ensure) allocates, but only on one GPU. The one thing a restore can create on a card is the calling thread's
private copy stream for it (conversation_copy.hpp private_stream), which it takes from the pool of finished threads when
there is one (#3350). A failed restore is fatal by contract (the session may be half written). So the need is one stream
and one event, and only for a card whose stream does not exist yet (a snapshot loaded from disk, or a restore on a thread
that never copied on that card).

Change. conv_cache.hpp: kRestoreFirstUseMib (64) and restore_fits(free, stream_ready, n, need): a restore is refused only
for a card that has no copy stream yet and less than 64 MiB free. conversation_copy_stream_ready(device) says whether a
card has one. generate.cpp: before the restore, each card of the run is checked; a snapshot that would not fit goes back to
the cache and the prompt is read again, with a log line. A card that has its stream is never refused, however full.
No sample (a failed cudaMemGetInfo) means the restore goes ahead as it did.

Tests. conv_cache_test: the predicate (cards with a stream are admitted at any free size; the edge at the need; no
sample / no cards / no need). conversation_copy_streams_test: copy_stream_ready before and after the first copy, in the pool
after a thread ends, and the measured cost of a card's first copy stream and event (printed; the test requires under
16 MiB, so 64 MiB leaves a wide margin). conversation_snapshot_test: after the first restore, six more restores of two
images leave the free VRAM where it was.

Not covered. The pure predicate and the generate.cpp code build on z440 (CUDA 13.0); the GPU tests are listed in the stream
notes. No model run: gates/2409-restore-guard.sh (a held-VRAM start on a layer split) shows the skip line and an
unchanged answer on the second turn.
2026-10-27 02:02:46 +01:00
Niko1221 2a9a67c174 Later-card VRAM: audit fixes for failed allocations, the reserve arithmetic as a tested helper, a per-card out-of-memory report (#3638 #3702 #3797)
Problem. On a layer split the later cards are left with very little VRAM after loading (#3702: 68 MiB on the last card; #3638:
268 and 666 MiB, then every batched prompt read of more than ~400 tokens ends non-finite after hours; #3797: the weight arena
fails with 320 MiB free and the message does not say who holds the rest). The window-graph eviction (12b0bce95) and the copy
stream pool (#3350) are in already; this is the rest of the code audit of what is allocated after load.

Audit. The table of every lazy allocation after load (what, when, size, how a failure is handled) is in
phaseA-tests/notes-027-gh-eng-d.md. Bugs found and fixed here, each small:
- verify.cpp capture / capture_batch and mtp.cpp finish_capture: the cudaGraphUpload and its sync were only printed. The
  upload is a warm-up, but a failure (no VRAM) stayed as the thread's pending error and the next kernel wrapper's
  cudaGetLastError ended the engine. The error is now cleared and the first launch uploads the graph.
- verify.cpp capture_commit and mtp.cpp finish_capture: the commit graph called cudaGraphInstantiate directly (no
  eviction of older layouts, unlike the window graphs) and reported the status of the end-of-capture call, which is success,
  so the message read "no error". capture_commit now goes through instantiate_evicting and reports the instantiate's status.
- instantiate_evicting instantiates into a local and sets the member only on success (what a failed instantiate leaves in
  its output is not specified, and a non-null member means "captured").
- prefill.cpp carve: after a failed take of the shared region the sub-allocators had no base and each cudaMalloc'd its
  buffer (hundreds of MiB in all) on the card that had just run out. carve now returns at once; the caller reports the
  failure as before.
- generate.cpp: a failed set_stage_helper / set_peer is reported and the run goes on, but its CUDA error stayed pending;
  cleared.
Found and not changed: relend() re-plans only the first participant (#3638 comment). Read: it runs only from the VRAM
command, which is off on a layer split, so it cannot touch a later stage's loan. Other candidates (ignored cudaEventCreate for
the pipelined fences, unchecked expert copies, cudaFuncSetAttribute flags set before success) are in the notes.

Changes for the report of #3797 and the reserve (#3702).
- include/strata/program/gpu_report.hpp: on a failed weight-arena allocation (first card and each stage) the engine now
  also prints, for every card of the run, the free and total memory and the largest other programs that hold memory on it
  (NVML, loaded at run time; where the driver does not list them it says so). Text only; the failure is the same.
  tests/program/gpu_report_test.cpp pins the text.
- include/strata/program/stage_reserve.hpp: the arithmetic of stage_room (what the split keeps free on a card) moved out of
  generate.cpp unchanged; tests/program/stage_reserve_test.cpp compares it with the old expression over a grid. The file
  also holds a PROPOSAL for a larger window term on a later card (96 MiB + 6 MiB per further window size + 48 MiB, 174 MiB
  for --spec 4) which nothing calls: the default placement is not changed. gates/3702-later-reserve.sh measures it.
- The low-VRAM warning of a later card now adds how many MiB the plan meant to keep free there and how far off that was.

Tests: gpu_report_test, stage_reserve_test (host only, built on z440).
Not covered: no model was run. What the ~700 MiB the plan does not count on the last card is (window graph sets, kernel code
loads) needs the run in gates/3702-later-reserve.sh.
2026-10-27 02:02:46 +01:00
Niko1221 11b0490228 tests: STRATA_QFUSE parity for a window of two groups and for the GDN q8_1 destination (#3948)
Problem. #3948 reports that STRATA_QFUSE=1 changes the verified text and lowers MTP acceptance on an RTX 3080 (sm_86); the
control (QFUSE off against off) was noisy too. The reporter read the code and suspects the per-group counter in
fused_gr.cu gr_q8_tail (not reset when a graph replays with another T) or the nat_xq_ offsets of a window of two groups.

Audit (no model run). The counter is reset by the block that finishes the group (atomicExch before it reads the 32 values),
so a launch leaves it at zero whatever T is; two blocks own a 32-column group in every kernel that calls gr_q8_tail, the
32-column kernel (gr_up_q32_kernel) quantises on its own without a counter; the fence pattern is the usual
threadFenceReduction one. The two destinations are consistent with their consumers in verify.cpp: the attention half's
xq_ is indexed from the group's first token (the projections read it from row 0), the expert half's nat_xq_ by the window's
token (the experts read it from tb). The GDN side (verify_kernels.cu gdn_q8_1_store, rows from t_out_begin) matches its
consumer too. I found no defect by reading. One thing the tests below will show if it matters: nvcc ignores the
`#pragma clang fp contract(off)` in gdn_q8_1_store, so on CUDA the ds.sum field could differ by one ulp from the separate
quantiser (the q8 bytes would not).

Tests (synthetic data, no model).
- gr_parity: the engine's G = 2 call pattern - the window split at (T + 1) / 2, two fused_gr_read_multi calls in a row on
  one stream with the same counters, group-relative destinations (xq_) and window-indexed ones (nat_xq_), direct and as a
  graph replay; fresh random inputs for every one of 200 repetitions for each T = 2..8; after every run both groups' q8_1
  bytes equal native_quantize_q8_1 of `mixed`, nothing outside the written ranges changed, and all counters are back at zero.
- gdn_parity: gdn_step_norm_multi with a q8_1 destination for every T and t_out_begin (verify and commit calls): the bytes
  equal native_quantize_q8_1 of the output rows, the outputs and the state are the ones of the call without it, and
  nothing is written past the rows. A second ctest, gdn_parity_split, runs all of gdn_parity with STRATA_GDN_SPLIT=1 (the
  4-blocks-a-head step, which has its own output-norm kernel).

Not covered. The reporter's model run (QFUSE on/off, MTP, 40 prompts): gates/3948-qfuse.sh. The tests were built on z440
(CUDA 13.0, sm_86); their GPU run is listed in the stream notes.
2026-10-27 02:02:46 +01:00
Niko1221 ebe3383a3f PLE reader: a failed read no longer leaves the next ticket waiting for ever; stress test for the direct table reader (#3602)
Problem. #3602 reports a deterministic all-logits NaN with --ple-io direct on IQ3_S (Windows, two GGUF shards) where
--ple-io mmap passes; a diagnostic build found non-finite values already in the gathered PLE embedding of one token. The
bytes the model got were not the file's (the largest finite value, 7.9e6, is what a random fp16 scale gives an IQ4_NL
row). This commit audits the reader and builds the test the report asks for; it does not claim to have found that
fault.

Audit (no model run). src/ngram/ple_reader.cpp, src/platform/direct_file.cpp and the consumers (PleTable gather_batch /
prefetch_rows / collect, prefill's double-buffered ple_gather, verify's windows): rows are copied and cached under the
reader's mutex after a complete read, short reads are checked per row, straddling rows get a two-page read, the offset
arithmetic is 64-bit, the set-associative cache and the slot buffers are only touched under the lock, and prefill's two
host buffers are ordered by the ple_copied events. I found no way to hand out a row before its read completed or from a
stale buffer.

One real bug, in the error path. After a failed read the I/O worker parks for good (worker_loop), but issue() went on
queueing jobs, so the next collect() waited for them for ever. issue() now returns a ticket with no jobs once the reader
has failed, and collect() returns the error at once (and drops the ticket). Same change in the SYCL copy
(sycl/src/ngram/ple_reader.cpp).

Tests.
- src/ngram/ple_reader_stress_test.cpp (ctest ple_reader_stress): 7 table shapes (header 0/192/4000/4051, rows of 1, 90,
  110, 160 and 4096 bytes, with and without bytes after the table) x 15 reader configurations (io thread or caller,
  in-flight 1/2/3/64, row cache of 0, 16 and 100000 rows, injected completion delays, keep-alive), four client threads
  with one to three tickets outstanding collected in any order; every delivered row is compared byte for byte with a
  buffered read of the file; collected buffers are compared again later (a late write would show); rows on every page
  phase, the last rows, out-of-range rows; the reader after a failed read (the file is cut short under it); with --big a
  5 GiB sparse table with rows around 2^31 and 2^32 and at its end.
- table_is_consistent_under_concurrent_use in src/ngram/ple_reader_test.cpp: one PleTable used by a prompt thread
  (big gather_batch), a decode window (prefetch_rows, gather_batch) and single gathers at once, against the mapped reader.

Results (z440, Linux, g++ 13, reader + direct_file + test built alone): plain build with --big OK; ASan+UBSan OK. The
error-path test against the unchanged reader hangs (the test exits 3 after 20 s); with the change it passes. ThreadSanitizer
flagged a data race that was a mistake in the test (the delay changed from a second thread in the caller-thread mode,
which has no lock); fixed in the test.

Not covered. The NaN itself: it needs a run on the reporter's configuration (gate script gates/3602-ple-direct.sh). The
Windows run of the new test is listed in the stream notes.
2026-10-27 02:02:46 +01:00
Niko1221 ba8b2bcb9c Merge f-027-gh-eng-a (#3622 ARM64 / DGX Spark port, #3806 Gyro-L TQP5/6/7)
# Conflicts:
#	cmake/ggml_tq_overlay_test.cmake
#	src/core/verify.cpp
2026-10-27 01:59:31 +01:00
Kevin Read 38b06316ef Gyro-L: read the pair-trellis types TQP5/TQP6/TQP7 (ggml 147-149) on the CPU, CUDA and the MMQ prompt path (#3806)
Gyro-L (agentionai's 50 GiB file) stores its experts as a pair trellis: 128
values per block, a fp16 scale and 64 steps of K bits (K = 5, 6, 7), two
weights per step taken from the first pair of the TQ2_T codebook entry the
step's state selects. The engine refused the file because it did not know the
three type ids. This adds them next to the existing trellis types 144-146
without changing those.

What changed:
- gguf_reader.hpp / tools/gguf_reader.py: names and block sizes (42 / 50 / 58 bytes).
- tq_cpu.*, tq_states.hpp, tq_avx2.cpp: tq_supported / tq_kbits / tq_block_bytes
  for the new ids (the bytes per block are 2 + 8K for the pair layout, 2 + 4K
  for the others), a scalar pair kernel (the reference) and an AVX2 pair kernel
  that is bit for bit equal to it; AVX-512 uses the AVX2 pair kernel.
- iq_kernels.cu: TqInfo, the lane decode (tqp_states8, a byte window), the
  dot product, the decode-once Split, the fp16 dequantizer and the format lists
  for 147-149. They are reached only by a pack that holds these types.
- moe_mmq.cu, strata_ggml_tq_overlay.cmake, third_party/ggml-tq/tq.cuh,
  ggml-common-tq.h: the MMQ prompt path for the three types (overlay
  enumerators, traits, tile loader branch, instances).
- native_expert_parity: names tqp5-7 and skips its synthetic check, with a
  message, on a ggml that has no trellis traits.
- docs/GYRO.md: the "not supported" sentence now says how Gyro-L is read.

Changes to the contributor's patch:
- tq_cpu.cpp called the AVX2 pair kernel without the x86 guard the rest of the
  file has, which breaks the Metal build (it compiles tq_cpu.cpp without
  tq_avx2.cpp): the pair rows go through one helper that falls back to the scalar kernel.
- moe_mmq.cu named GGML_TYPE_TQP5-7 under STRATA_MMQ_TQ, but the public
  agentionai llama.cpp has the older trellis types and not these: a new
  STRATA_MMQ_TQP (set for the overlay and for a tree that has the tqp instances)
  guards them, so that tree still builds. The HIP overlay lists get the three
  instances too, and ggml_tq_overlay_test checks them (and sets the CMake
  policy its IN_LIST needs).
- native_expert_parity: the skip test compared a type-name pointer to null, but
  ggml_type_name of an id past the table reads past it (the test crashed on
  an upstream ggml); it compares against GGML_TYPE_COUNT.
- docs/GYRO.md keeps the plain description and leaves out the contributor's
  measured speeds (we have not repeated them).

Tests added: src/kernels/cpu/tqp_cpu_test.cpp (no ggml, no model: the scalar
kernel against a bit-by-bit reference from the format definition, every ISA
equal to scalar for 1-8 tokens and for gate/up rows, the type predicates for ids
0-299 against the old formulas), the pair types in tq_pascal_bench (fp16
dequantizer bit for bit against the definition, decode kernels within 3e-2,
decode-once == per-column), tools/test_gguf_tqp.py, and native_expert_parity
tqp5/6/7 registered when the ggml tree has the tqp instances.

Checked (no model): tqp_cpu_test passes on x86 (AVX2 on z440; AVX2 and
AVX-512 on the owner PC with MSVC); tq_pascal_bench on an RTX 5070 (sm_120,
MSVC build): 0 failures, the TQP dequantizer matches the definition in all
bits, and the checksum lines of TQ2_T/TQK6/TQK7 are byte-identical to the
build without this change. Builds: CUDA sm_86 (gcc), sm_60 (CUDA 12.9),
sm_120 (MSVC), SYCL, and the CPU files for aarch64.

Not covered: the contributor's own parity runs need a ggml tree that carries the
types (not public here), and the 50 GiB model run (load check, coherent
answer, 32K prompt, MTP, Gyro-S/M identity) is still to be done. Metal, SYCL and
HIP have no kernels for the types; HIP was read, not built.
2026-10-27 01:57:57 +01:00
Niko1221 1e57a924d0 Merge f-027-gh-eng-m (#3416 gated MTP: opt-in proposal law, window-invariant CPU dispatch, draft policy)
# Conflicts:
#	docs/DETAILS.md
2026-10-27 01:48:34 +01:00
Keyan Hu ca17d0f5cd spec: a gated request's lookup policy learns from committed rounds only, DraftPolicy choose() unchanged otherwise (#3416)
Problem (KeyanHu-git): the gated proposal needs the choice between the MTP window and a
lookup window to be fixed by the history, not by the draws of the rounds it will verify.
Freezing the policy for the whole request (the first commit) does that, but also freezes
the cost table, so a repeat-heavy request kept the window sizes it inherited. In the
author's diagnostic workload the frozen policy chose mean lookup windows of 5.87 and 3.38
on two repeat requests; with this change 4.31 and 4.05 (from the PR text; not
reproduced here).

Change: the request's copy of the DraftPolicy (source_policy) gets three calls, used only
when gated_proposal is on:
- observe_committed(): the round's accept and probe counts, applied at once. A lookup
  window counts as a probe of its size (tried_), so a size whose cost is only guessed is
  tried kProbes times, not every round.
- defer_cost(end, t, ms): the round's time is held. A time covers the window and the next
  MTP draft, so it depends on every Philox counter up to
  max(p + T - 1, p + a + 1 + S_mtp).
- apply_costs(committed): applies, in order, the held times whose end is behind the
  committed position, with the same update observe() makes (including the STRATA_SPEC_COST
  clamp), in the context bucket the round ran in; delayed_n_ keeps a held time from
  counting as a second probe.
choose() counts the probes as cost_n_ - delayed_n_ + tried_.

Port note: the DraftPolicy on the current tree keeps its costs per context bucket
(STRATA_SPEC_COST), so tried_, delayed_n_ and the held times carry the bucket. A gated
request does not run with STRATA_SPEC_COST (it falls back to the older rule), so its
copy stays in bucket 0.

Default path: unchanged. choose() reads two new terms; on any path that never calls the new
functions tried_ and delayed_n_ are +0.0 and cost_n_ - 0.0 + 0.0 == cost_n_ exactly.
Checked by driving the old DraftPolicy (namespace-renamed copy of f-027-gh-eng) and the
new one with the same random call sequences (observe, observe_chain, observe_probs,
set_context, seed_costs, set_robust, choose, chain, keep_draft; 400 sequences of 3000
steps): 25,983,538 bitwise comparisons of picks, costs and rates, 0 mismatches; one
observe_committed call makes the same harness report 60,528 mismatches. generate.cpp:
nothing calls the new functions when gated_proposal is false.

Tests: draft_policy_test (the PR's added assertions, on the bucketed class) and
spec_admission_test pass on z440 and on the owner PC.
Not covered: the sampling argument for the delay boundary (that a held time cannot steer
a window through a counter that is drawn again) is the author's; a unit test cannot show
it, only the distribution run (gate script 3416-spec-prob-dist.sh, arm NEWLK) can.
2026-10-27 01:45:36 +01:00
Keyan Hu 499e963593 native experts: window-invariant CPU arithmetic for STRATA_SPEC_PROB decode, Default dispatch unchanged (#3416)
Problem (KeyanHu-git): the rejection rule of the previous commit judges each row against
the target's probabilities p. For a native (IQ) pack the CPU experts round a token's rows
differently depending on how many tokens share its expert in the window (the grouped
kernels against ggml's one-token dot, #152). So p of one row moved with the window width,
and the same sampled text could depend on the draft length. On random blocks a grouped
Default call differs from the token alone in 700 to 12000 of about 18000 output floats
for the IQ gate/up and IQ4_NL down formats.

Change: NativeArithmetic { Default, WindowInvariant }, fixed for one pool batch.
native_gu_rows / native_down_rows become thin wrappers of one template with the arithmetic
as a parameter. The Default instance keeps every old branch (the new conditions are
compile-time true there). WindowInvariant gives each token the bits of the one-token call
on this build and CPU: kernels that already match it stay grouped (NVFP4, Trellis and
Hadamard rows, KQ256 when ggml runs its AVX2/FMA dot); the IQ rows that differ use
iq512_reference_gu_rows (types 18, 21, 22: the same decode, ggml AVX2's accumulation
order, one weight decode per group) or iq4nl_reference_rows (IQ4_NL down), and anything
else falls to the one-token dot per token. ExpertPool::run_split_multi_native takes the
arithmetic and expert_pool_dispatch_multi passes it; generate.cpp uses the invariant
pool only when the MTP drafts probabilistically (mtp.prob(), i.e. only with
STRATA_SPEC_PROB=1). Prefill, greedy decode and every other decode run Default.
The pool picks the entry point with one compare and a direct call per chunk.

Default path (STRATA_SPEC_PROB unset): unchanged, shown by running the old and the new
native_gu_rows / native_down_rows (Default) on the same random blocks: 61 format, width
and amplitude lines (IQ2_XXS/XS/S, IQ3_XXS/S, IQ4_XS, NVFP4, Q4_K, Trellis with and
without folded Hadamard, IQ4_NL / Q2_0 / Q5_1 / Q8_0 down; widths 1 to 8; row sub-ranges),
output hashes identical in 10 settings of the environment switches on z440 (AVX2) and
in 13 on the owner PC (Zen4, AVX-512, MSVC), including STRATA_NO_IQ512, STRATA_IQ_GATHER
and STRATA_IQ_MT_MIN=1. g++ codegen of the AVX-512 row kernels is not byte-identical for
5 of 87 functions (inlining and register allocation of the same source); the AVX-512
kernels were run only under MSVC.

Tests: native_arithmetic_parity, eight ctest configurations (default, AVX, KQ256,
IQ_MT_MIN=1, no512, ggml only, NVFP4 AVX2, NVFP4 ggml), registered in CMakeLists.txt and
sycl/CMakeLists.txt. It compares the invariant entry points at every group width 1..8
with the one-token calls, bitwise, for the IQ, NVFP4, Q4_K/Q5_1/Q8_0, Trellis and
Hadamard formats. All eight pass on z440 and on the owner PC; the existing CPU expert
tests (iq_avx2_parity, iq_avx1_parity, iq512_gather_parity, pool_tasks_test,
nvfp4_cpu_parity, router_dot_parity, q2_bitplane_parity) pass.
Not covered: an ARM CPU (not built here), an AVX-512 machine with fast gathers (Sapphire
Rapids), the layer-split call site, and any model run.
2026-10-27 01:45:35 +01:00
Keyan Hu f4612b1add spec: STRATA_SPEC_PROB=1 keeps the confidence gate in the proposal law, opt-in (#3416)
Problem (KeyanHu-git, refs #2028, #2591): with STRATA_SPEC_PROB=1 the default
--spec-min-p gate tests the token drawn from the draft head's distribution q, but the
verifier still judged the draft against the unfiltered q and treated a failed gate as a
plain sample from p. That shifts the output distribution: with p = (0.5, 0.5),
q = (0.875, 0.125) and threshold 0.5 it emits (0.5625, 0.4375).

Change: the gate is part of the proposal. Keep q's mass on the tokens that pass the gate
without renormalising (mu); a draw that fails the gate becomes NULL. An offered token D
is accepted with min(1, p(D)/mu(D)); a rejection and NULL draw from the normalised
positive part of p - mu; a position with no attempted proposal is a plain sample from p.
Serve and the CLI build the same packet (core/gated_proposal.hpp) and give it to the
verifier explicitly (Verifier::set_spec_proposal, sample_tokens_proposal). The explicit
call has no plain-p fallback: an invalid packet row is reported, not sampled around.
STRATA_SPEC_PROB_GATE=top keeps its normalised-q rule.

Default path (STRATA_SPEC_PROB unset): unchanged. mtp.prob() is false, so gated_proposal
is false, the new draft-result argument is null, GatedDraftLimit does nothing,
Verifier::run takes the old branch (set_spec_q clears the measure flag), and the
sampler kernel's old instance is the old code with the new branches compiled out.
Read hunk by hunk; no hunk runs a new branch or a new allocation per token. The
default-identity model run is written as a gate script and not run yet.

Port notes against the PR as submitted:
- Unsupported combinations (parallel or multi-device decode, --lookup-chain,
  --adapt-swaps, PCIe share, --spec-min-p not finite, STRATA_SPEC_COST, DFlash2,
  STRATA_SPEC_T_CYCLE, --spec-corrupt, Metal) used to be an error that ended the
  request or the server. They now warn once and keep the older rule, so setting the
  variable never stops a run. --adapt-swaps 96 and the PCIe share are on by default, so
  the exact rule needs --lookup-chain 0 --adapt-swaps 0 --pcie-frac 0.
- The lookup source of a probabilistic window is chosen by a copy of the policy fixed at
  the request start, without asking whether the MTP's first guess agrees. Learning from
  committed rounds inside a request is the policy commit of this series.
- The CLI loop on the current tree has the out-of-memory window retry, DFlash2 and
  STRATA_SPEC_T_CYCLE; the gated path does not use the retry (a narrower window would
  change the room the packet was built for). Serve keeps merged first windows
  (STRATA_ADMIT_MERGE_FIRST) at their length.
- docs/DETAILS.md: two short bullets instead of the PR's long paragraph (that paragraph
  no longer exists on this tree). bench/results and the PNGs are not ported.

Tests: gated_proposal_test and spec_admission_test (default, pick, top, exact-top-only)
pass on z440 (CPU, g++ 13) and on the owner PC (MSVC). On the owner PC's RTX 5070
(sm_120, MSVC build, synthetic data, no model) spec_proposal_parity, spec_verify_parity and
sampler_parity (default, one-block and old-kernel configurations) pass. Also built:
CUDA sm_86 on z440 and boston, SYCL on P1006 (engine and host tests). The Metal path is
not built here.
Not covered: a model-level distribution test (chi-square of the first tokens against
plain sampling) and the identity run with the variable unset. Both are written as gate
scripts (3416-spec-prob-dist.sh, 3416-default-identity.sh) and not run.
2026-10-27 01:45:35 +01:00
Niko1221 9ac278e124 docs: release notes - split serving (#4000) filled from docs/SPLIT_SERVE.md: opt-in, exact, slower than the layer split on two A6000, credit to MarcoTool 2026-10-27 01:40:17 +01:00
Niko1221 ccf18ea143 Merge f-027-split: split serving (#4000), opt-in, first version
Prefill on one card, decode on another, session handed over through RAM; exact token for token (state hash match at 4K
and 16K on 2x A4000 and 2x A6000). Nothing changes unless split_serve / --split-serve is set. On two alike cards it is
slower than the layer split on every workload measured (docs say so); it can only pay on dissimilar cards. Idea and
manual setup by MarcoTool (#4000).
2026-10-27 01:39:21 +01:00
Niko1221 3480bf2b8c Split serving: docs with the measured results, check tool no longer hashes the state in speed runs
Measured on 2x RTX A6000 (IQ3_S): slower than the layer split on every workload;
exactness checks pass (Q2_0 on 2x A4000, IQ3_S on 2x A6000).

Idea and manual setup by @MarcoTool in #4000.
2026-10-27 01:38:32 +01:00
Niko1221 597df5e601 docs: the KVA opt-in is split 36, final (the split 24 variant is a 0.2.8 study); release notes marker closed 2026-10-27 01:35:28 +01:00
Niko1221 1b89043157 docs: the 1.5 route prior check - all five tasks the grader failed were read: three were grader flags, two were the long-winded answers 2026-10-27 01:29:32 +01:00
Niko1221 129739440a docs: release notes 0.2.7 draft, audit fixes of the engine review and the decision markers brought up to date 2026-10-27 01:28:23 +01:00
Niko1221 8191cb91b4 docs: the route prior stays at 1.5 after the 61-task check on the RTX 3060 (DETAILS.md and the 0.2.7 release notes)
No copy, code-constant, JSON-spelling, retrieval or unanswerable defects (retrieval 12/12, JSON 3/3, unanswerable 9/9, extraction equal to off); two long-winded answers, which the runs with the prior off also show two or three of per run. Story +15%, code +37% against off.
2026-10-27 01:26:22 +01:00
Niko1221 fb8f7d9df6 docs: release notes 0.2.7 draft (every merged item and the owner decisions of 26-27 October, measured numbers per box, markers for what is still pending) 2026-10-27 01:25:46 +01:00
Niko1221 df20d77845 Docker docs, compose defaults, scripts and the file test name the 0.2.7 image tags (0.2.7-cuda13 and the other flavors) 2026-10-27 01:25:46 +01:00
Niko1221 9e20319069 layer-major prefill: declines on STRATA_EXPERT_NIBBLE builds (#3976)
f-027-gh-eng gained STRATA_EXPERT_NIBBLE (4-bit expert slots, the sm_60 experimental
build): only run_impl gathers such a layer for MMQ. The layer-major walk would read
the slot as GGUF blocks, so lm_prepare now declines when any layer is converted and
the prompt takes the chunk-major path. Compiled out of every build without
STRATA_EXPERT_NIBBLE_BUILT.
2026-10-27 01:13:13 +01:00
Niko1221 bd8a254453 Merge f-027-gh-eng into f-027-gh-eng-pm (#4121 on top of the #4010 pipelined adaptive rounds) 2026-10-27 01:12:47 +01:00
Niko1221 0959545e21 Merge f-027-gh-eng into f-027-gh-eng-lm 2026-10-27 01:12:00 +01:00
ferstar a4c41a92ee Build the engine for ARM64 (DGX Spark / GB10) without changing the x86 build (#3622)
The engine did not build on aarch64: the CPU expert path assumed the x86 AVX
kernels, the CPUID probes and the x86 spin/fence intrinsics. After this change
the strata target compiles on aarch64 (CUDA host code, native GGUF packs).
On x86 nothing is selected differently.

What changed:
- CMake: STRATA_CPU_X86 is decided from CMAKE_SYSTEM_PROCESSOR (an unset
  processor stays the x86 build). On aarch64 src/kernels/cpu/portable.cpp
  stands in for the AVX kernel sources; native experts run on ggml-cpu's NEON
  dot products. -ffp-contract=off on aarch64. STRATA_ISA_FLOOR is ignored with
  a warning there.
- include/strata/platform/cpu_relax.hpp: strata_cpu_pause / strata_store_fence
  map to _mm_pause / _mm_sfence on x86 and to yield / dmb oshst on aarch64.
  verify.cpp, pool.cpp, session.cpp, mtp.cpp and graph.cpp use them.
- expert_layout.cpp: the CPUID/XCR0 probes compile on x86 only and report no
  AVX on other CPUs.
- generate.cpp: --cpu-oracle-q8-0 is rejected on a non-x86 CPU.
- setup.py: nvidia-smi's "[N/A]" memory on GB10 (unified memory) takes the
  pool size from /proc/meminfo on ARM64 only; the CPU probe no longer needs
  the x86 "flags" line; setup warns that the ARM64 engine is compiled on the
  PC and which packs work.

Changes to the contributor's patch, found while moving it onto the 0.2.7 line:
- portable.cpp had no iq512x_rows / iq512x_supported / iq512_gather_on (added
  to the x86 kernels after the PR was written): the aarch64 link would fail.
- The Metal build (Apple silicon) shares these files. It keeps its barrier
  (dmb ishst), its plain router loop (its bf16_rows_dot_multi is a stub), its
  Metal start-up block in generate.cpp and its "unknown" CPU name.
- The CPU side of the GPU doorbell: strata_load_fence() after the spins that
  wait for a flag the GPU raised (dmb ishld on ARM, nothing on x86 and on
  Apple silicon), so the data written before the flag is not read early.
- A card whose memory nvidia-smi does not report stays unlisted on x86 (the
  new fallback is for ARM64 only).
- tests/terminal/test_setup_arm64.py.

Checked: with the x86 CUDA build (sm_86, gcc 13 -O3, z440) the objects of
verify, session, mtp, graph, expert_source, pool, expert_layout and generate
are byte-identical before and after (the x86 wrappers are macros for the same
intrinsics). Every C++ file of the strata target compiles for aarch64 with
clang, and the symbols of the CPU kernel library link up (syntax and symbols
only: no ARM machine, no ARM run). The Metal-shared files compile for
aarch64-macos with STRATA_GPU_METAL.

Not covered: no ARM hardware here. The tests that include x86 headers
(iq_avx2_parity and the other AVX parity tools) and the DeepSeek target do
not build on aarch64; the strata target does not need them. Memory ordering
on ARM is reasoned, not run. The Q2_0 and Gyro packs need the x86 kernels.
2026-10-27 01:11:53 +01:00
Niko1221 2651c4a96b Merge f-027-int2 (2a2264c21) into f-027-gh-eng
# Conflicts:
#	tools/calibrate.py
2026-10-27 01:11:52 +01:00
Niko1221 2579635138 sm_80 defaults of #3779 (wide dense dequant, MMVQ-IL sm_80 tables) follow STRATA_DEFAULTS=026: back to the 0.2.6 kernels and the shared table; explicit =1 and =0 still win; arch_defaults_test checks the rule; release-notes line 2026-10-27 00:58:42 +01:00
Niko1221 1e278a280d Merge f-027-gh-eng-n (#3779 sm_80 tuning: wide dequant, MMVQ-IL sm_80 tables and the opt-ins)
# Conflicts:
#	CMakeLists.txt
#	docs/MMVQ_IL_TABLE.md
#	src/kernels/cuda/native_mmvq.cu
#	src/kernels/mmvq_il_parity.cpp
#	src/program/generate.cpp
2026-10-27 00:57:59 +01:00
linxuhao 56d98f164e docs: the sm_80 defaults and the opt-in sm_80 switches of #3779, STRATA_GRAPH_PRIO and STRATA_MMVQ_IL_PF (#3779, #3712)
DETAILS.md gets two sections: the decode switches (GRAPH_PRIO, MMVQ_IL_PF) with the
two sm_80-only defaults named, and the prompt switches (PF_FUSED_NT / BATCH / F16 /
WS, GDN_TC, PLE_COMPACT / PLE_COPY_STREAM, EMBED_VRAM, DEQUANT_SIDE) with what is
bitwise and what is rounding-level. MMVQ_IL_TABLE.md gets the sm_80 row of the table
list and a paragraph on the hoisted-load tables (kIlPf_80 / kIlPf58_80, Q6_K only) and
on measuring a card with mmvq_il_parity --bench --emit-table --pf 0,3,4.

Rewritten from the PR's text: every figure is labelled as the reporter's (one CMP
170HX; we have no sm_80 card and measured none of them), and the prompt-switch
section is new (the PR documented only the decode switches and the conversation
cache).

Original docs by linxuhao (PR #3779, commit 6d6f0f39).
2026-10-27 00:52:53 +01:00
linxuhao 8b95bffb0b Decode: opt-in STRATA_MMVQ_IL_PF=1 hoisted-load, exact-integer interleaved GEMV kernels for Q6_K (#3779, #3712)
Problem: the interleaved small-batch matvec (native_mmvq_il) has one weight block
in flight per warp: its four `if (kbx < bpr)` sections each sit behind their own
branch, so on a GA100 (ncu: 432 GB/s on the qkv Q6_K matrix) the loads of a group
are not issued together.

Change, opt-in (every variant is bitwise native_mmvq): native_mmvq_il_pf_kernel
issues every weight load of a group (4 virtual warps x R rows) before any of the
group's dots, with the addresses clamped to the row's last block for a tail, and
with the ring variants the next group's loads before this group's dots. The
per-lane accumulation order is the plain kernel's. P2's forms replace
conversions by exact ones where the value is a small integer: (float)p becomes
p + 1.5 * 2^23 minus 1.5 * 2^23 for |p| < 2^22, and the saturating byte subtract
of Q6_K's codes (0..63, so nothing saturates) becomes two instructions. Q4_K,
Q5_K, IQ4_XS and Q6_K have the variants; Q3_K (the Q2_0 pack's type) keeps the
plain kernel.
- STRATA_MMVQ_IL_PF=1: the measured table (kIlPf_80 / kIlPf58_80: Q6_K only, read
  only when the card is sm_80; on every other card 1 does nothing);
  =2..6: one variant for every type and shape (A/B only, any CUDA card).
- Unset or 0: the plain kernels through the same launch code (the variant argument
  is 0, and the added code is one cached environment read and a switch).
- CUDA only; HIP has no interleaved path.

Tests: mmvq_il_parity --pf a,b,.. bit-checks each listed variant against
native_mmvq for every type and shape, T = 1..8 and rows 0/1/2/4, and with --bench
times them interleaved (PFBENCH lines) and prints the joint (variant, rows) table
in the source form of kIlPf_<cc>. New ctest entry mmvq_il_parity_pf runs it with
--pf 0,2,3,4,5,6 on whatever card is present.

Reporter's numbers (linxuhao, CMP 170HX): decode +1.4 % (two reference / on / reference /
on sets) over the plain kernels; with STRATA_GRAPH_PRIO decode +1.3 % in their
per-feature pair (4K prompts +3.9 %, 16K +1.2 %, 52K +0.2 % in the same pair, which
the reporter reads as noise). Q4_K, Q5_K and IQ4_XS variants tie the plain kernels
there. Theirs, not measured here.

Original change by linxuhao (PR #3779, commit 116992ab).
2026-10-27 00:52:53 +01:00
linxuhao c2d3fe5000 mmvq_il_parity: the L2-residency arms (--hot / --gap / --persist / --ab), the clock warm-up and MMVQ_IL_TMIN (#3779, #3712)
Harness only; no engine code. The bench part of mmvq_il_parity times the
interleaved matvec cold (24 copies of the weights rotated past the L2), hot (one
copy), hot with other weights' reads in between (--gap MB, what a window's GEMVs do
between a prefetch and its consumer), and with the timed weights in a persisting L2
set-aside (--persist); --ab runs the five arms per case in one process, because
the clocks ramp over the first ~100 ms of a run. A 150 ms warm-up of launches runs
before the first timed case, and MMVQ_IL_TMIN bounds T from below.

The parity part is unchanged: without --bench the same checks run in the same
order (every rows choice against native_mmvq, bitwise). The bench code is CUDA only
(the test is built with STRATA_ENABLE_CUDA); the persisting-L2 calls are left out
under STRATA_HIP_GFX906.

Original change by linxuhao (PR #3779, commit 1f4b8139).
2026-10-27 00:52:53 +01:00
linxuhao 0049fa73ff Decode window: opt-in STRATA_GRAPH_PRIO=1 gives the window graph's kernel nodes their stream's priority (#3779, #3712)
Problem: the decode window graph runs every kernel node at the launch stream's
priority, so the one-block indexer kernels of the side branch wait behind the
queued blocks of the big q GEMV, and the shared expert competes with the routed
experts.

Change, opt-in (bitwise by construction: no kernel changes, the same launches on
the same inputs; only the dispatch order moves): with STRATA_GRAPH_PRIO=1 the
side mixer's branch stream (GDN gates, QSA indexer chain) is created at the
device's highest priority, cs_ and side2_ in the middle, the shared expert's
stream at the lowest, and the window graph is instantiated with
cudaGraphInstantiateFlagUseNodePriority. If a driver did not copy the stream
priority into the nodes, the capture-time notes (the branch forks and the
priority streams' tails) set the attribute per node. One log line per captured
window prints the node priority histogram. A card whose priority range is 0..0
changes nothing.

CUDA only: the members and the three methods sit under !STRATA_USE_HIP &&
!STRATA_HIP_GFX906 && !STRATA_GPU_METAL; graph_prio_on() is false there.

Merged by hand with #3702 (the window-graph eviction): instantiate_evicting now
takes both keep_t and the flags, and the retry after freeing other window
sizes keeps the flags (the merged code would otherwise have dropped the node
priorities on that retry).

With the variable unset: every stream is created as before and the capture-time
notes return at once.

Tests: none of its own (a priority is a scheduling hint with no value to compare).
The window graphs are exercised by the engine; see the checklist for the
reporter's greedy-answer identity check.

Reporter's numbers (linxuhao, CMP 170HX, IQ3_S, --spec 3, 3K context, interleaved in
one session, each arm with STRATA_QIL=1): reference 125.3 / 126.6, GRAPH_PRIO
127.8 / 129.0, reference 125.0 / 126.8 tok/s, +1.8 %. Greedy 11 of 11 identical. In
the later per-feature pair together with STRATA_MMVQ_IL_PF: decode +1.3 %. Theirs, not
measured here.

Original change by linxuhao (PR #3779, commit 68c487c3).
2026-10-27 00:52:53 +01:00
linxuhao 0e63d59c01 docs: conversation cache copies on a slow host link, STRATA_CKPT_ASYNC and the park switches (#3779, #3712)
One section in DETAILS.md for the switches the four conversation-cache commits
add: what each copies where, its VRAM and host memory cost, that they stay off
on HIP, gfx906 and Metal, and the measured times.

Rewritten from the PR's section: the times are labelled as the reporter's
(linxuhao, one CMP 170HX on PCIe Gen2 x4; we have no sm_80 card and measured
none of them), the independent RTX 3090 data of bitaccesscomau is added, and the
pool row describes STRATA_CKPT_POOL_READY=N instead of the PR's change to the
meaning of STRATA_CKPT_POOL=N (STRATA_CKPT_POOL=1 is unchanged).

Original section by linxuhao (PR #3779, commit a04f4399).
2026-10-27 00:52:53 +01:00
linxuhao 85e3dde522 Conversation cache: opt-in STRATA_PARK_VRAM_MIB=N keeps parked conversations' K/V in VRAM in front of the host copy (#3779, #3712)
Problem: restoring a parked conversation copies its K/V from the host over the
link: 27-107 ms over a PCIe Gen2 x4 link.

Change, opt-in and only with STRATA_PARK_ASYNC=1 (CUDA only): a park also keeps
the whole K/V of its image in a VRAM slot of its own (64 MiB granules, up to N MiB
in all, only while 1.5 GB of VRAM stays free beside it); the background copy to
the host reads from the slot, and a restore of that image copies device to device
from the slot (about 1 ms) without waiting for the background copy. Each K/V
layer carries its own join-and-release token, so any order of destruction is safe
(the layers outlive their image when they are retained for the next park). Under
VRAM pressure the elastic K/V's growth releases every slot and the stage, then
retries once (both growth sites in generate.cpp).

With the variable unset the slot code is not reached: the budget is 0, a restore
finds dev_tag 0 and takes the host path, and the growth sites see
conversation_park_vram_release_all() return 0 and behave as before.

Tests: none of its own (it needs a card with the VRAM for the slot and a parked
conversation); see the checklist for the reporters' A/B/A conversation check with
STRATA_PARK_VRAM_MIB set.

Reporter's numbers (linxuhao, CMP 170HX): restore of a 3K conversation 28-47 ->
7-12 ms; outputs identical. The first restore of a run is slower (41.6 / 56.9 ms)
than later ones (6.5-12.6 ms) and they have not found why. Theirs, not measured
here.

Original change by linxuhao (PR #3779, commit bafca410).
2026-10-27 00:52:52 +01:00
linxuhao ca378670b2 Conversation cache: opt-in ready checkpoint pool STRATA_CKPT_POOL_READY=N, gathered small checkpoint copies STRATA_CKPT_GATHER=1, park traces (#3779, #3712)
Problem (reporter, CMP 170HX, PCIe Gen2 x4): a conversation switch's park still
paid 38-52 ms for the live state, because the STRATA_CKPT_POOL pool was empty at
every park (a switch takes two checkpoints), so the 118 MB GDN vector was
allocated fresh and page-faulted; and a checkpoint's 37 small synchronous
device-to-host copies queue behind the 4 MiB DMA pieces of STRATA_CKPT_ASYNC.

Change:
- STRATA_CKPT_POOL_READY=N (opt-in, 1..16, with STRATA_CKPT_POOL on): the pool
  keeps N ready checkpoints, pre-sized to the session's state sizes with their
  pages in, topped up on a background thread after any take (not with a layer
  split).
- STRATA_CKPT_GATHER=1 (CUDA, opt-in): the small running-state vectors (ple, and
  each layer's indexer tail, dead row and block position) are gathered device to
  device into one buffer and fetched with one copy into a pinned buffer.
- The park stage is allocated rounded up to 64 MiB (a growing conversation does not
  cudaFree/cudaMalloc it every time) and a VRAM-resident K/V skips the pointer
  attribute query.
- The background helpers (pool top-up, the checkpoint DMA, the park DMA) leave the
  host's pinned core (aux_cpus.hpp); no output depends on where a thread runs.
- A parked image whose live GDN state is still being copied joins before its
  buffers are freed (SavedConversation::live_pending).
- STRATA_PARK_TRACE prints the joins, the live-state vectors' readiness and the
  K/V sub-timers.

Differs from the PR as submitted: the PR made STRATA_CKPT_POOL=1 allocate two
ready checkpoints at startup (+236 MB of host RSS) and read STRATA_CKPT_POOL=N as
a count. That changes what an existing switch does, so here STRATA_CKPT_POOL=1
keeps its meaning (the pool holds dropped checkpoints only, nothing is allocated
ahead of time) and the ready pool has its own variable, STRATA_CKPT_POOL_READY.
A user of the PR's STRATA_CKPT_POOL=2 sets STRATA_CKPT_POOL=1
STRATA_CKPT_POOL_READY=2. The PR's one-checkpoint refill after a park with
STRATA_PARK_ASYNC is replaced by the top-up, as in the PR, and so is only active
with the new variable. Merged by hand into #2526's park_source.

With none of the new variables set: the stage size is rounded up only when the
park stage is on, the extra code is trace output behind STRATA_PARK_TRACE and
the join calls.

Reporter's numbers (linxuhao, CMP 170HX): park at a 3K context 64-75 -> 21-40 ms
(all park switches together, A/B/A conversation check identical, greedy 11 of 11
identical). Theirs, not measured here.

Original change by linxuhao (PR #3779, commit 56088e71).
2026-10-27 00:52:52 +01:00
linxuhao f73b3c86a4 Conversation cache: opt-in STRATA_PARK_ASYNC=1 parks the K/V in the background and moves the dropped checkpoints (#3779, #3712)
Problem: parking the outgoing conversation copies its K/V device to host and
deep-copies its checkpoints before the next prompt can start: 213 ms at a 3K
context, 334 ms at 13K on a PCIe Gen2 x4 card (the reporter's CMP 170HX).

Change, opt-in and CUDA only (off on HIP, gfx906 and Metal):
- STRATA_PARK_ASYNC=1: conversation_kv_save copies the device K/V device to
  device into a VRAM stage (only while 1.5 GB stay free beside it; otherwise the
  save copies as before), and a background thread moves the stage into the host
  buffers in 4 MiB pieces through two pinned buffers. conversation_park_join
  waits for it: every reader of a parked image's K/V, the cache before it frees an
  entry, and the image itself (SavedConversation::pending) before its buffers go.
  ConversationBuffer::resize_for_overwrite leaves the added bytes unfilled for
  that copy; the buffer's segments use an allocator that value-initializes only
  when a fill value is given, and resize(n, value) is unchanged for every other
  caller.
- With it, the checkpoints this request drops right after the park (all of them
  when another state comes in, else the ones off this prompt's path) move into
  the parked image instead of being copied, and a background allocation refills
  STRATA_CKPT_POOL for the next save. Only the main session's checkpoints move,
  not an idle batch slot's (the park_source of #2526 serves both).
- conversation_snapshot_save takes dropped checkpoints' storage (the existing
  STRATA_CKPT_POOL) for its copies; with the pool off, nothing is taken.
- Unset: park_drops is null, no storage is taken, the K/V copies are the
  synchronous ones; the added work is the K/V size sum for the stage request and
  a few join calls.

Differs from the PR as submitted: the PR's join in conversation_file.cpp
(put_payload) is not ported; the SAVE/RESTORE handler of generate.cpp joins the
park first (see the previous commit, which carries the PR's 33a8146a fix).
Hand-merged into the park_source / ParkSource refactor of #2526.

Not covered: if the background copy fails after resize_for_overwrite, the
parked image keeps unfilled bytes; the failure is logged. A restore of such an
image would read them. No test of the park path with a failing copy.

Tests: the PARK_ASYNC path has no unit test in this commit (it needs a card with 1.5 GB free for the
stage); the reporter's A/B/A conversation check covers it, see the checklist.

Reporter's numbers (linxuhao, CMP 170HX): park 213 -> 64-75 ms (3K), 334 -> 68 ms
(13K); greedy answers identical on their 11 prompts and the A/B/A conversation
check. Theirs, not measured here.

Original change by linxuhao (PR #3779, commit 3e0c141c).
2026-10-27 00:52:52 +01:00
linxuhao 0f7224d168 Conversation checkpoints: opt-in STRATA_CKPT_ASYNC (background device-to-host copy) and STRATA_CKPT_VRAM_SLOTS (#3779, #3712)
Problem: on a slow host link a turn checkpoint copies the whole GDN state to
the host on the request path (118 MB at 1.64 GB/s on a PCIe Gen2 x4 card, 72 ms;
an independent RTX 3090 on PCIe 3.0 x16 showed 73 ms to 62 ms with the switch).
Restoring a parked conversation copies it back.

Change, both opt-in and CUDA only (off on HIP, gfx906 and Metal):
- STRATA_CKPT_ASYNC=1: the GDN state is copied device to device into a VRAM stage
  (one buffer the size of the state, allocated at the first checkpoint save, plus
  a pinned host buffer of the same size; if either allocation fails the save falls
  back to the synchronous copy), and a background thread moves it in 4 MiB pieces
  into the checkpoint's host vector. Every reader joins first and only for the copy
  it needs (restore, snapshot save and restore, the parked copies, the serve loop at
  each request start, the SAVE/RESTORE session handler).
- STRATA_CKPT_VRAM_SLOTS=N (with ASYNC): the last N saved GDN states stay in VRAM,
  tag-checked, only allocated while 1.5 GB stays free; a restore of a checkpoint
  whose tag a slot holds copies device to device.
- STRATA_TRACE lines carry a steady-clock timestamp; STRATA_PARK_TRACE prints the
  timing of the save steps.
- Unset: the code adds a mutex-guarded no-op join per checkpoint save, restore and
  snapshot, a tag field that stays 0 and a few clock reads; the copies are the
  synchronous ones as before.

Differs from the PR as submitted: the PR put a join into conversation_file.cpp
(put_checkpoint), which broke the conversation_file_test and
session_save_reclaim_test builds without CUDA and matched only the copied vector,
not the one the copy in flight writes, so a SAVE within one checkpoint copy after a
turn could write an incomplete GDN state (found by bitaccesscomau; fixed in the
PR's last commit 33a8146a). This commit carries that fix: conversation_file.cpp
stays as it is and the join runs first in the SAVE/RESTORE handler of
generate.cpp. The commit 33a8146a is therefore not a separate commit here.

One more join, found by running the new ctest entries: conversation_snapshot_sources (the SAVE
path of a session file) saves the running state with the background copy and the caller writes
it to the file at once, after the handler's join has already run. It now joins the copy of
that checkpoint before it returns; without it a session file written with
STRATA_CKPT_ASYNC=1 could hold an incomplete GDN state (conversation_snapshot_test failed
at "both files read" before the join).

Tests: conversation_snapshot_test now joins after each save and, with
STRATA_TEST_BIG_GDN=1 (a GDN state over 1 MiB, so the async path is taken), runs as
the new ctest entries conversation_snapshot_test_ckpt_async and
conversation_snapshot_test_ckpt_vram_slots: every checkpoint, capture and restore
must give the same bytes as the plain run.

Not covered: no test destroys a checkpoint while its copy is in flight (readers
join; the cache's eviction paths do not); the reporter saw 3 of about 35 engine
starts stop answering after a park with evictions, on the paths with the
switches off, and could not reproduce it in 15 reruns. The VRAM stage has no
headroom check of its own (an RTX 3090 with 147 MiB minimum free showed no failure
in 42 of 42 saves; the allocation failure falls back silently).

Reporter's numbers (linxuhao, CMP 170HX): turn checkpoint 71.4 -> 0.9 ms,
restore 114-126 -> 28-47 ms. Independent data (bitaccesscomau, RTX 3090 24 GB,
PCIe 3.0 x16, release defaults, 4 pairs): checkpoint save 73.2 -> 62.4 ms, tiny-prompt
time to first token median ratio 1.038, outputs byte-identical. Theirs, not measured
here.

Original change by linxuhao (PR #3779, commits 0d512925 and 33a8146a).
2026-10-27 00:52:52 +01:00
linxuhao 8c2a846947 Prefill GDN: opt-in STRATA_GDN_TC=1 runs the DeltaNet recurrence as a chunked scan on the tensor cores, sm_80 (#3779, #3712)
Problem: the prompt path's DeltaNet recurrence is a large part of the prompt time
of a GA100 card.

Change, opt-in and rounding-level (the output is not the FP32 kernels' bits):
src/prefill/gdn_tc.cuh holds the chunked WY-form recurrence with every product
on the tensor cores (f16x3 splits with power-of-two scaling, one block per value
head) and its prep kernel; kernels.cu dispatches it before the STRATA_GDN_CHUNKED
branch for prompt chunks of 128+ tokens when STRATA_GDN_TC is set.

Where it runs and where it does not:
- gdn_rec_tc returns "not supported" and the existing kernels run unchanged on a
  card below sm_80, with fewer than 48 SMs, or with less than 122 KB of
  shared memory a block (the kernel needs 124,944 B). That excludes sm_86, sm_89
  and sm_120 (99 KB opt-in): the code is compiled into those builds and never
  launched there. It runs on sm_80 and sm_90. A failed scratch allocation
  (60.3 MB a device) also falls back.
- Device code for cards below sm_80 compiles to trapping stubs behind
  STRATA_GDN_TC_SM80; the whole header sits under !__HIPCC__.
- Unset, the dispatch costs one cached flag test per call.

Tests: gdn_rec_parity gains the tc variant (T = 1..4099, 8192, 32768, six stress
inputs against gdn_rec_kh_kernel, gate 1e-4; "not on this card" where it cannot
run), --selftest-frag (each tensor-core helper on one warp against a double
reference) and --bench-tc. The new ctest entry gdn_rec_parity_selftest_frag runs
the helper self-test, which any sm_80+ card can run (77 below sm_80). The full
tc variant can only run on an sm_80 or sm_90 card.

Reporter's numbers (linxuhao, CMP 170HX): recurrence 105 -> 34 ms at 3.3K tokens
and 1,705 -> 547 ms at 52K; prompts +3.9 % (4K), +6.0 % (16K), +6.7 % (52K) over
the same base; within 2.3e-6 of the FP32 kernel in gdn_rec_parity; teacher-forced
KL against the bitwise base mean 0.0157, median 0.0011, max 0.098, top-1 100 %;
1 of 5 conversation checks identical and decode -2.6 % in their pair (they
think draft acceptance moves with the rounding; not checked). Theirs, not
measured here.

Original change by linxuhao (PR #3779, commit f1cabc99).
2026-10-27 00:52:52 +01:00
linxuhao 10c8cc629f Prefill fused IQ experts: opt-in f16 mma path STRATA_PF_FUSED_F16=1 and warp-specialized kernel STRATA_PF_FUSED_WS=1 (#3779, #3712)
Problem: the NT = 256 kernels still decode every 32 values to int8 and scale
the int32 products in FP32.

Change, both opt-in and both rounding-level (the output is not bit-identical to the
int8 kernels; they are the PR's "rounding-level" switches):
- STRATA_PF_FUSED_F16=1 (implies the NT = 256 set): the decoded weights times
  their block scales and the activations times theirs as F16, m16n8k16 F16
  products into FP32 accumulators. The k order inside a 16-value group is
  permuted the same way for both operands.
- STRATA_PF_FUSED_WS=1: native_ws_kernel, producer warps decode into an F16 ring
  in shared memory and consumer warps run the same F16 products (named barriers),
  so it has the F16 path's rounding. One block per multiprocessor; about 101 KB of
  shared memory at its defaults, so it is set up on sm_80 / sm_90 cards and
  says "not available" and keeps the 512-thread kernels on an sm_86 / sm_89 /
  sm_120 card (99 KB). Compile-time knobs STRATA_WS_PROD / _SLOTS / _NB / _KS / _HTAB.
- Both take a WideBatch like the NT = 256 kernels, so STRATA_PF_BATCH above 128
  reaches them. CUDA only (under !__HIPCC__); the Turing pass compiles the F16
  instances against __trap() stubs and never runs them (dev_info stops before the
  NT = 256 setup on Turing).
- With neither variable set the kernels, their launches and their shared-memory
  sizes are as before (the F16 template parameter defaults to false).

Differs from the PR as submitted: when STRATA_PF_FUSED_Q2N=1 is on as well
(#3809, after the PR was written) the WS setup also sets up the Q2_0 gate/up
instance.

Tests: prefill_fused_iq_test_f16 and prefill_fused_iq_test_ws (ctest) run the
fused-experts test (double-precision reference and MMQ) with the switch set. The
test now skips (77) when a requested NT / F16 / WS kernel set is not set up on the
device, so a card that cannot run WS does not report a pass for the 512-thread
kernels.

Limits of the test for these two arms only: with STRATA_PF_FUSED_F16=1 or _WS=1 the
test uses the limits it already has for STRATA_PF_F16 (F16 products): fused against the
int8-rounded model rel RMS 5e-3 (2e-3 otherwise) and against MMQ 5e-3 (1e-3 otherwise).
Measured with F16 on an RTX A4000 (sm_86, z440), over the 11 pairs: against the int8-rounded
model rel RMS 2.85e-3 to 2.90e-3 and worst row 5.9e-3 to 8.1e-3; against MMQ the same;
against the FP32 reference 1.438e-2 to 1.480e-2 with MMQ at 1.435e-2 to 1.478e-2 (the
fused path stays within 1.003x of MMQ's rel RMS and 1.03x of its worst row). The rounding is the F16 rounding of the
weights times their scales and of the activations times theirs (an 11-bit mantissa, about
2^-11 relative per operand), which the int8-rounded model does not have; it is not a
different summation order. The same arm on an RTX PRO 5000 (sm_120): up to 2.90e-3 (rel RMS), worst row up to 8.1e-3.
The default path and the NT=256 arm keep the old limits and their printed errors are
unchanged: with no switch set the test prints identical output (52 lines) from the base
tree and this tree, on an RTX A4000 (sm_86) and on an RTX PRO 5000 (sm_120).

Reporter's numbers (linxuhao, CMP 170HX): one MoE layer of 3,256 tokens 8.27 ms
(NT = 256) to 7.90 ms (F16) and 7.65 ms (WS); prompts +2..+6 % over the
256-thread kernels, 1 of 11 greedy answers changes. Teacher-forced KL against the
bitwise base on their 20-prompt set: F16 / WS mean 0.0062, median 0.00006, max
0.076, top-1 100 %. Theirs, not measured here.

Original change by linxuhao (PR #3779, commit 579cbfbb).
2026-10-27 00:52:52 +01:00
wannagoxuf 17878ac579 batch slots: MTP drafts in the pipelined groups, STRATA_PIPED_MTP=N opt-in with --batch-mtp (#4121, #3794)
Problem: a layer split with --batch pipelines the slots in groups, and the pipelined groups run no MTP drafts.
--batch-mtp with serial windows leaves all but one card waiting. A multi-stream deployment wants both.

What changed: STRATA_PIPED_MTP=N (with --batch-mtp) keeps the pipeline and drafts in it. A group's window carries
each of its slots' rows: the slot's token and up to N MTP proposals (at most --spec - 1; the first always, each
further one while the draft layer was at least --spec-min-p sure of the one before). The last stage judges each
slot's proposals on their own. The number of rows to keep is known only then, and by then the earlier stages run
other groups' windows, which reuse the scratch the commit replays from. So a small save graph copies those rows
right behind each window (Verifier::batch_launch_rows), and every stage queues the commit of the kept rows after
the verdict (Verifier::batch_commit_rows). The slot drafter catches up on its own rows and drafts the next
proposals as an asynchronous chain (MtpDrafter::chain_launch). Unset, nothing changes.

Changes on top of the PR as sent (review):
- The verifier window size is worked out where the verifiers are built, from the slots that fit and the final
  groups, not earlier from --batch as asked. Unset it is the old expression.
- STRATA_PIPED_MTP is decided again once the slots that fit are known. It says it is off, and why, when fewer than
  2 groups are left, when STRATA_BATCH_SELF_COMMIT is on, or when a group's window would hold more than
  kVerifyMaxT rows. The start line names groups and slots correctly.
- batch_groups::resolve keeps its old mtp_dropped rule; only mtp_piped is new, and it also applies with
  --batch-groups given (the PR's fix 1). The "--batch-groups 1" test case now expects "not dropped".
- --remote-expert-opt alone no longer blocks --batch-mtp on a layer split: the object it guards is only made with
  a helper expert cache (the PR's fix 2).
- docs/BATCHING.md rewritten with the numbers measured here.

Tests: batch_groups_test (the PR's cases, four "unset is as before" cases); builds on CUDA sm_86 (Linux), SYCL
(the sycl tree has its own verify.cpp and generate.cpp and does not include this code) and MSVC. Model runs:
see docs/BATCHING.md and the gate scripts 4121-*.sh.

Not covered: AMD (HIP) was not built or run; the draft layer on the first stage; helper expert caches.
2026-10-27 00:21:07 +01:00
Niko1221 1d45da3a8a Merge f-027-rp15: route-prior default 1.5 on discrete NVIDIA cards of 13 GiB or less (owner decision)
2.0 showed copy and code defects on the RTX 3060 (IQ3_XXS); 1.5 showed none in 19 tasks and keeps the speed
(story +15%, code +37% vs off). The 61-task check decides 1.5 vs a 1.0 fallback.
2026-10-26 23:30:18 +01:00
Niko1221 0afaaa399e Route prior: the 0.2.7 default on a discrete NVIDIA card of 13 GiB or less drops from 2.0 to 1.5 (owner decision 2026-10-26). On the RTX 3060 12 GB (IQ3_XXS) the copy and code check found real defects at 2.0 and none in 19 tasks at 1.5 or 1.0; 1.5 keeps story +15% and code +37% against off. The value lives in strata::kRoutePriorSmallCard (sm120_series.hpp); scope (CUDA discrete <= 13 GiB), STRATA_ROUTE_PRIOR=0, an explicit value and STRATA_DEFAULTS=026 are unchanged. arch_defaults_test, verify.cpp comments, DETAILS.md and the 0.2.7 release notes follow. 2026-10-26 23:26:18 +01:00
Niko1221 1e9678cb7c Split serving: check tool uses cold prompts per request and a single-card arm on the shared arena
Idea and manual setup by @MarcoTool in #4000.
2026-10-26 23:15:49 +01:00
Niko1221 b1bea3a17c Merge f-027-shs: STRATA_SH_STREAM off by default on Windows gfx1151 (owner decision)
+6.9% Qwen IQ3_S decode on Strix Halo Windows, prompts unchanged; token ids identical in 4 starts x 5 prompts.
Linux gfx1151 and other cards unchanged; STRATA_DEFAULTS=026 restores it. Docs label the DeepSeek Strix numbers by build.
2026-10-26 23:01:38 +01:00
Niko1221 f932e410e6 docs: Windows Strix Halo leaves STRATA_SH_STREAM off, with the token-id check; DeepSeek numbers say which build they come from
The Windows guide says what 0.2.7 does and how to turn the switch back on, backed by five fixed greedy prompts
(895 ids) run four times with the switch on, off, on, off: identical ids. STRIX_HALO.md and the 0.2.7 release notes
get a line. The DeepSeek 4K prompt figures of 2026-10-25 (78.7, 87.2) stay and are labelled with their build; a table
of the 0.2.7 candidate changes one thing at a time (chunk size, DS4_DEFAULTS=026).
2026-10-26 23:00:50 +01:00
Niko1221 c77292f09d gfx1151 on Windows: the shared-expert fork (STRATA_SH_STREAM) is off by default
Interleaved A/B on a Strix Halo Windows PC (Qwen IQ3_S, engine 0.1.51): chat decode 43.6 tok/s with the fork,
46.6 without (+6.9%), prompts unchanged. Linux gfx1151 and every other architecture keep their table.
STRATA_SH_STREAM=1 turns it back on, STRATA_DEFAULTS=026 keeps v0.2.6's table.
arch_default_env(arch, defaults026, windows) is a pure rule; arch_defaults_test checks both platforms.
2026-10-26 22:28:05 +01:00
Niko1221 e240850324 Docs: Strix Halo Windows guide, plain emphasis and a stray blank line 2026-10-26 22:24:30 +01:00
Niko1221 538acd098c Merge f-027-gh-ctl: GitHub setup, Docker, docs and bench items for 0.2.7
Ported PRs (authors kept) and our fixes, plus the owner's part-2 items: the restored TROUBLESHOOTING entries with a
link check, #3895 (no shared-expert fork calibration on Windows HIP with --kv-resident; a warning on a hand-set
STRATA_SH_STREAM=1), #3930 (docs), #3421 (Docker stops when models are present and MODEL_PATH is unset), #3570
(launch --show-key reads setup's config), #3311 (engine paragraph only) and #3029 (Strix Halo Windows guide,
re-measured on 0.2.7).
2026-10-26 22:24:08 +01:00
Niko1221andHaiYunChuang ee1a6efd5c docs/STRIX_HALO_WINDOWS.md: a Strix Halo Windows guide, measured again on 0.2.7 (#3029)
Structure from the guide in PR #3029 by HaiYunChuang (what the PC reaches, memory and the BIOS, the configuration, what is on by default, traps),
rewritten for the 0.2.7 install and with every number measured again: the Terminal and the gfx1151 engines built from f-027-int2 (3e468f5de), a
local signed test feed, strata launch as a standard test user on the NucBox EVO-X2 (Adrenalin 26.10.1), tools/strata_bench.py over a tunnel.
Qwen IQ3_S: 44 tok/s chat, prompts 755 / 821 / 792 tok/s at 4K / 16K / 32K. DeepSeek 2.00 bpw: 12 to 14 tok/s chat, prompts 214 to 342 tok/s.
An interleaved A/B of STRATA_SH_STREAM on Windows gfx1151 (three pairs): off is 6.9% faster in chat decode, prompts unchanged.
What launch chose and logged is quoted instead of a hand-made config. The first guide's tuning rows (engines 0.1.41 to 0.1.47) are not repeated.

Co-authored-by: HaiYunChuang <[email protected]>
2026-10-26 22:21:04 +01:00
Niko1221 f1dadd1710 fused_gr: the persistent hyper-connection read's barrier only calls __nanosleep on sm_70 and newer (the sm_60 build of f-027-int2 failed on it) 2026-10-26 22:07:55 +01:00
Niko1221 3cabb9e3fe Merge f-027-gh-app: GitHub app and Terminal items for 0.2.7
Part 1 fixes and 12 ported PRs (authors kept), and the owner's part-2 items: the Monitor readings strip with
local currency (#3628), approval notification + opt-in minimise to tray (#3477), opt-in done sound (#3586),
app port setting with a busy warning (#3402) and interface size up to 200% (#3970). Host tests: only the two
known base failures on Linux; connector-setup and the new tests pass on Windows.
2026-10-26 21:56:21 +01:00
Niko1221 3b908cc4f7 Tests: the new catalog is checked in nine languages, French included 2026-10-26 21:55:58 +01:00
Niko1221 0d0ec83b7d Merge f-027-int2 into f-027-gh-app 2026-10-26 21:54:30 +01:00
Niko1221 c17a6f4ce1 App: the Monitor's readings strip (energy since start, cost in the person's own currency, GPU fan and clock, CPU and SSD temperature) with the Electricity price setting (#3628, PR #3628 by hash13 as the starting idea); a sound when work finishes (#3586); desktop notification and tray, pinned port and interface size settings with their host routes (#3477, #3402, #3970); the gh-<lang> catalog in nine languages 2026-10-26 21:31:51 +01:00
Niko1221 902dc814d6 Desktop shell: a pinned app port with a warning when it is busy (#3402); minimise to tray with Tauri's own tray icon and a native desktop notification through the loopback bridge (#3477) 2026-10-26 21:31:47 +01:00
Niko1221 59b4be2a54 App: French for the four new DeepSeek card and 12 GB NVIDIA strings (fx80-fx83) 2026-10-26 21:19:31 +01:00
Niko1221 98e35984c6 Merge f-027-dscard: the DeepSeek card names the free disk the set needs (owner-approved wording)
# Conflicts:
#	apps/host/src/locales/fix-de.json
#	apps/host/src/locales/fix-en.json
#	apps/host/src/locales/fix-es.json
#	apps/host/src/locales/fix-ja.json
#	apps/host/src/locales/fix-ko.json
#	apps/host/src/locales/fix-pt.json
#	apps/host/src/locales/fix-ru.json
#	apps/host/src/locales/fix-zh.json
2026-10-26 21:19:14 +01:00
linxuhao dc390c3864 Prefill: opt-in STRATA_DEQUANT_SIDE=1 dequantizes the dense weights on a side stream into a double-buffered scratch (#3779, #3712)
Problem: Gemm::native dequantizes each dense weight to FP16 in front of its
GEMM on the compute stream, so the dequant never overlaps the kernels queued
ahead of it.

Change, opt-in (CUDA only, always off on HIP): with STRATA_DEQUANT_SIDE=1 the
prompt path takes a second GEMM scratch (64 MiB more of its region, counted in
bytes_needed and carried through relayout) and Gemm::set_dequant_side hands it
to the Gemm. native() then dequantizes the next weight on a non-blocking side
stream into the scratch the compute stream read last but one. Four events order
every write and read of each scratch (free -> dequant -> ready -> GEMM -> free),
so each product sees the same bytes as in the serial form. The row-sliced path
(a weight larger than the scratch) stays serial. Metal answers false and keeps
the serial form.

Differs from the PR as submitted: if the side stream, its events or the
setup fail, the PR failed the prompt-path init. Here one line is printed and the
dense weights are dequantized in front of their GEMMs as before. Unset, nothing
changes (gemm_dequant_side() is one cached flag; the scratch, stream and events
are not created).

Test: new gemm_dequant_side_parity (ctest, GPU, synthetic, 77 without a device):
eight Q4_K / Q5_K / Q6_K weights of the engine's shapes (one larger than the
scratch, so it takes the sliced path) run three rounds through Gemm::native
serially and with the side stream, with and without a spin kernel queued ahead of
each GEMM; every output must be identical bit for bit.

Not covered: dense weights read by the side stream are the resident ones; a weight
that arrived by an upload on another stream would need its own event (the prompt
path has none that do).

Reporter's numbers (linxuhao, CMP 170HX): measured with STRATA_PLE_COPY_STREAM and
STRATA_EMBED_VRAM, the four PLE / embedding / dequant switches together give
+2.2..+2.6 % prompts. Theirs, not measured here.

Original change by linxuhao (PR #3779, commit 741fc6ab).
2026-10-26 21:19:13 +01:00
Niko1221 03d709820d Merge f-027-app2: master switches in the app and Terminal settings
DS4_DEFAULTS=026 behind the DeepSeek speed-ups Off toggle, the 12 GB NVIDIA route-prior row, the Advanced-only
Qwen '0.2.6 behaviour' row (STRATA_DEFAULTS=026), and the app-ui Quit test fix. Terminal suites pass; host suite
failures on Linux also fail on the base.
2026-10-26 21:18:49 +01:00
Niko1221 663ec80abf mmvq_il: the sm_89 (Ada) decode tables are on by default on compute capability 8.9 only, STRATA_MMVQ_IL_89=0 and STRATA_DEFAULTS=026 turn them off (#3453)
Owner decision for 0.2.7: the tables of PR #3453 (the cells where an RTX 4070 and an RTX 4080 community run gave the same rows count, and
the one-column Q3_K row) ship on for sm_89 cards, untested on Ada by us. The output is bitwise the same with them off (a table only
changes how many rows a warp takes); no end-to-end gain was seen on a 4090 or 4080 in the report, so the reporter tests 0.2.7 and reports.

The rule is a pure function in sm120_series.hpp: mmvq_il_89_default(hip, cc, v026) is true for cc 89 only, false in HIP builds and under
STRATA_DEFAULTS=026 (defaults_026.hpp, as 38c9080db did for the other defaults); mmvq_il_89_resolve() adds the variable: "1" and "0" win,
unset or other text is the rule. native_mmvq.cu reads it once. arch_defaults_test checks cc 89 on, every other cc off, HIP off, 026 off,
"0" off, "1" wins over 026, the empty text. mmvq_il_parity expects the on-tables by default and the old ones with =0 or 026.

Docs: MMVQ_IL_TABLE.md, DETAILS.md and the 0.2.7 release notes (a line that says it is not measured on an RTX 40 card, with the off switch).
2026-10-26 21:11:03 +01:00
Niko1221 46a9cd87c7 Merge f-027-int2 into f-027-gh-eng (brings STRATA_DEFAULTS=026 and the other 0.2.7 integration work) 2026-10-26 21:09:26 +01:00
Niko1221 52ba0b7816 Merge f-027-i18n: French as the ninth app language and the Polish README
French catalogs for the app (3,528 strings + fix/tune/tools catalogs) and the served web page (247 keys),
README.pl.md from the current README (#3355, Jakub Luwierski), Polski in every README language row, and the
README.md language row's double-encoded names fixed. Tests: 21 host files 180 pass on p1006, Terminal and web
i18n checks on l620.
2026-10-26 21:03:59 +01:00
Niko1221 551f1cdc30 Merge f-027-gh-eng-k1 (#3867 #3751 #3453 #3943 #3820 #3752 #3455: Pascal and sm_89 opt-in kernel ports)
# Conflicts:
#	src/core/expert_cache.cpp
#	src/core/verify.cpp
2026-10-26 21:03:12 +01:00
Niko1221 2b9869eddf Web chat page: French (#3970)
serve/web/locales/fr.json (247 keys, plurals one/other, same terms as the app catalog) and the French line in serve/web/i18n.js. tools/web_i18n.py and serve/test_web_i18n.py (node and python) pass.
2026-10-26 20:59:54 +01:00
Niko1221 710b6a1bfe Merge branch 'f-027-int2' into f-027-i18n 2026-10-26 20:58:00 +01:00
Niko1221andJakub Luwierski 2ce07f8631 Docs: Polish README (#3355)
Ports the Polish translation from #3355 and the Polski entry in the language row of every README. The pull request translated the older long README, so README.pl.md is rewritten from the current README.md (logo, centred language row, DeepSeek speed tables including Strix Halo, Gyro, coffee button). README.md's own language row had garbled names (double-encoded text); it is written out correctly.

Co-authored-by: Jakub Luwierski <[email protected]>
2026-10-26 20:57:57 +01:00
Niko1221 52a558abbe App: French as the ninth interface language (#3970)
fr.json, fix-fr.json, tune-fr.json and tools-fr.json (3,654 strings, vous, plain short words), the Francais entry in the language picker, every locale test and gap check now covers fr, a french-catalog test (spacing before ? ! : ;, apostrophes, short labels, no leftover English), scripts/fr_sync.py to list and merge strings added later, and scripts/language-shots.mjs for screenshots of the app in one language.
2026-10-26 20:57:57 +01:00
Niko1221 5d9f479229 Merge f-027-web: the served web chat page for 0.2.7
Wide/narrow CSS (#3176), page translations with a language choice, opt-in KaTeX math (#2972, #2974),
sandboxed svg/html preview (#1628), Load/Unload in the header (#3550), and one chat-history design
(#1787, #2615, #3823, #4004). Tests: browser suite on the owner PC, node + pytest serve suites on boston
(457 passed, 2 skipped).
2026-10-26 20:57:41 +01:00
ruibeikaa 9067cc64d3 iq_kernels: GP100 4-bit gate/up expert slots, opt-in STRATA_EXPERT_NIBBLE=1 (#3455)
Problem: on a GP100 die the verify window's gate/up kernels for the IQ2_S,
IQ3_XXS and IQ3_S expert formats are not held by memory bandwidth but by each
block's serial chain (unpack the codebook indices, the emulated dp4a, the
16-lane reduction); the same kernel with every dot product removed still takes
about two thirds of its time.

Change: with STRATA_EXPERT_NIBBLE=1 the engine, after the expert cache is
filled and every expert is resident, transcodes the gate/up blocks of every
fully resident IQ2_S / IQ3_XXS / IQ3_S layer (n_embd 2560, n_ff 640) into slots
of 4-bit codes, 16 bytes per 32 weights, with the integer scales and fp16 d kept
as they are; the down rows stay GGUF. The decode gate/up reads those slots with
kernels that run the production integer chain and float tail in the production
order (R4S / R4P / R4SV / R4PX by format and group size); the prompt path's MMQ
gather rebuilds the GGUF blocks from a slot on the fly. At load every item is
round-tripped and every slot's rebuilt GGUF blocks are compared with the staged
blob before the cache is sealed (ExpertCache:🦭 a later fill_slot stops the
engine). New: include/strata/core/expert_nibble.hpp (slot contract, switches,
per-layer table), nib_gu_proto.cuh (STRATA_NIB_GU_PROTO=1 variant), the
transcoder, hash and rebuild kernels and the 4-bit gate/up kernels in
iq_kernels.cu, generate.cpp (the fill, the VRAM accounting, STRATA_EXPERT_NIBBLE_
RECHECK), iq_nibble_parity and native_expert_bench --nibble.

Review changes against the PR:
- An option the switch cannot work with (--peer-device, --kv-grow, --vram-elastic,
  remote caches, STRATA_MMQ_DIRECT, ..., a card that is not cc 6.0, a build
  without the kernels) now prints a warning and the start goes on with the usual
  GGUF slots, where the PR refused the start. What is known only later (an expert
  out of VRAM, a loan, the prompt chunk) still refuses, since slots are
  converted by then.
- The checks this switch adds to expert_source.cpp and prefill.cpp are compiled
  out of every build that has no 4-bit kernels, so the default builds add nothing
  to the hit split or the prompt loop.
- iq_nibble_parity gets --synthetic-only (every gate/up format x both down
  types on random experts, no model file), skips with 77 on a card that is not a
  GP100, and the synthetic and --header runs are ctests.
- The PR's docs section is rewritten for these changes and its measurements are
  labeled as the author's.

Default path (switch unset): the table is empty, nibble::slot_bytes(l) is
expert_layout().blob_bytes(l) and nibble_max_slot_bytes() is max_blob, every
sizing the PR touched returns what it returned before, and the new branches in
generate.cpp (guard_raw before the adaptive tier's copies, the profile fill's
test), expert_cache.cpp (one bool in the fill calls and admit) and verify.cpp
(a capture-time layout choice, compiled out without the kernels) are not taken.
Proof on the kernels: in the sm_60 build (CUDA 12.9) the SASS of all 446
kernels iq_kernels.cu had before is identical with and without this change,
as is that of the 603 in native_mmvq.cu, 29 in qsa_select.cu and 10 in
moe_mmq.cu; 21 kernels are new.

Tests: iq_nibble_parity --header passes on z440 and boston (the host half of the
contract; also a ctest). The GPU parts (--synthetic-only, a model shard) refuse a
card that is not a GP100, so they were only compiled here (sm_60, CUDA 12.9,
P1004: iq_nibble_parity and native_expert_bench build; the whole tree builds).
sm_86 build without the SM60 option (z440): exit 0, the 4-bit paths compiled out.

Not covered: no GP100 run. The model gate is
phaseA-tests/gh027/gates/3455-expert-nibble.sh: iq_nibble_parity --synthetic-only
and on a model's layers on a P100, then identical replies with the switch on and
off (a layer is converted only when every expert of every layer is resident, so
the rig needs the pack to fit whole), then the decode A/B. The contributor's
numbers (four GP100 dies, Swift 1.5 IQ3_S, 47 of 48 layers converted, +12.7 GiB
VRAM, decode 47.3 -> 51.9 tok/s, +9.7%; prompt reading 2.3-3.0% slower; replies
of 15 requests equal) were not re-measured, and the enabled path was written
against an older base than this branch's.
2026-10-26 20:52:54 +01:00
ruibeikaa 5dbd8774d8 prefill: a resident layer's experts weight-stationary on GP100, opt-in STRATA_MMQ_WS=1 (#3752)
Problem: on a GP100 die (compute capability 6.0: no dp4a, no tensor cores) the
prompt pass's routed experts run as MMQ launches per 16-expert group: a
gate/up, a swiglu, a quantize and a down launch per group, each group's experts
gathered and padded to the group's largest row count, every MMQ tile decoding
its weight blocks again. With every expert resident the per-group overhead, not
the weight bytes, is a large share of a prompt read on this card.

Change: with STRATA_MMQ_WS=1, a layer whose routed experts are all resident on
this GPU runs quantize_scatter, then gate/up as ONE weight-stationary launch
over all its experts (read where they lie in the expert cache), ONE
swiglu_quant over all its rows, then the down as ONE launch. New
src/prefill/moe_ws*.cu(h): a CTA holds 128 (or 256) weight rows of one expert
and runs them against up to 64 of that expert's activation rows, a thread per
weight row, so each weight block is decoded once per pass; gate/up for
IQ2_XXS, IQ2_XS, IQ2_S, IQ3_XXS and IQ3_S, down for Q2_0 and IQ4_NL. Every
product keeps MMQ's int8 values, scales and per-32-value epilogue in its K
order, so GU, H and the down rows are MMQ's bits. ws::faster carries the
author's measured ws / MMQ time ratios per format and picks a layer only where
both products win at its rows per routed expert. prefill.cpp decides per layer
after quantize_scatter (all experts resident, this GPU alone, no peer or CPU
share, not the fused layout, not STRATA_MMQ_DIRECT, no Hadamard, no NVFP4 tail,
no FP4 activations, aligned, not STRATA_DBG_NAN); every other layer keeps its
path. STRATA_WS_FAST=1 (second opt-in, same bits) selects a down kernel for the
IQ4_NL 32-token tier with fewer instructions per pass.

Gating: the kernels and STRATA_PREFILL_WS exist only in the CUDA build with
STRATA_EXPERIMENTAL_SM60 (not HIP, not Metal; prefill.cpp links stubs that
answer mode 0 elsewhere). At run time ws::mode() is 1 only with STRATA_MMQ_WS=1
and every visible GPU at compute capability 6.0. Unset, nothing is allocated
(the offset and chunk buffers, under 1 MiB per die, exist only when the mode is
not 0), the arena layout and the mapped table are the ones before, and the
per-layer test short-circuits on the cached mode.

Not ported: moe_ws_down_bench (needs a model shard) and its CMake target.
Added: ws::set_mode (tests only) and tests/cuda/prefill_ws_parity_test.cpp, a
synthetic layer-level check: random experts of all 5 gate/up x 2 down formats,
random routing, the per-group MMQ (mmq::Context per expert) against ws::gate_up
+ swiglu_quant + ws::down, GU, Hq and Dm compared byte for byte, the down also
alone on the reference H and with STRATA_WS_FAST on. It gives its verdict on a
compute capability 6.0 card only (the kernels follow MMQ's sm_60 SASS); other
cards run it for information with --any.

Tests: prefill_ws_parity_test on boston (2x RTX A6000, sm_86, CUDA 13.1, gpu lock,
SM60 build, --any because it is not a GP100): 40 cases (all 5 gate/up x 2 down
formats, four routings each), 0 declined; the largest GU and down-alone error
against that card's MMQ is 5.2e-07 of the largest value (float rounding: MMQ's
sm_86 epilogue is not the sm_60 one the kernels follow), the full chain's Dm
differs up to 1e-3 where int8 rounding of H flips on those rounding
differences, and STRATA_WS_FAST output equals the default kernel's in all
cases, 0 differing bytes. The byte-for-byte verdict against MMQ is the P100's.
sm_60 compile of the whole tree (CUDA 12.9, P1004): exit 0; sm_86 build without
the SM60 option (z440): exit 0. SASS of the 10 kernels moe_mmq.cu had before
this change is identical in the sm_60 build.

Not covered: no GP100 run here. The contributor's numbers (prompt reads 31-37%
shorter on four GP100 dies with every expert resident: 240 tokens 2,156 -> 1,364
ms, a fresh 26K read 41,231 -> 28,555 ms; one run per arm) were not re-measured.
The model gate is phaseA-tests/gh027/gates/3752-mmq-ws.sh.
2026-10-26 20:52:32 +01:00
Niko1221 60e6a06086 Split serving: docs pointers, check tool puts session files in /dev/shm
Idea and manual setup by @MarcoTool in #4000.
2026-10-26 20:44:45 +01:00
linxuhao 884e37dfdf Native head: opt-in STRATA_EMBED_VRAM=1 keeps the token-embedding table in VRAM (#3779, #3712)
Problem: the token-embedding table of a native pack lives in mapped pinned host
memory, so every prompt's scattered row gather crosses the PCIe link. On a Gen2
x4 link that costs more than the table's size in VRAM is worth.

Change, opt-in: with STRATA_EMBED_VRAM=1 the table is copied into the current
device's VRAM when the table plus 512 MiB stay free (322 MiB for an IQ4_XS
table). The gather kernels read the same bytes through dev_ as before. If the
VRAM is not there or the copy fails, the table stays in mapped host memory as
before and one line says so. The start-up line now says where the table is
("in VRAM" also for the two existing VRAM fallbacks, which it called mapped
host memory).

With the variable unset nothing changes (one getenv at load).

Not covered: a layer split across several devices places the table on the
device that is current when it loads, as the existing VRAM fallback does. No
unit test (it moves the same bytes to a different memory; the gather kernel is
unchanged).

Reporter's numbers (linxuhao, CMP 170HX): measured together with
STRATA_PLE_COPY_STREAM and STRATA_DEQUANT_SIDE on a 51.8K-token prompt, the
embedding step went from 191-201 to 70 ms. Theirs, not measured here.

Original change by linxuhao (PR #3779, commit 952dd784).
2026-10-26 20:41:23 +01:00
linxuhao d6428bf291 Prefill: opt-in STRATA_PLE_COPY_STREAM=1 uploads the compact PLE rows on their own stream (#3779, #3712)
Problem: with STRATA_PLE_COMPACT the raw-row upload sat on the compute stream
before layer 1, which idles for the DMA (4.7 MB per 3K-token chunk, 2.9 ms on a
Gen2 x4 link).

Change, opt-in, only with STRATA_PLE_COMPACT=1: the raw rows go up on a
non-blocking stream of their own once the previous chunk's dequant has read the
device buffer (ple_raw_free event); the compute stream waits for the upload
(ple_copied) only at the dequant. Same bytes, same kernel. The stream is drained
when a run ends so no upload is left in flight from a buffer the next run
refills.

Differs from the PR as submitted: if the stream or the event cannot be created,
the PR failed Prefill::init. Here it prints one line and the rows go up on the
compute stream as with STRATA_PLE_COMPACT alone. With the variable unset
nothing changes (the stream and the events stay null).

Reporter's numbers (linxuhao, CMP 170HX): PLE time of a 51.8K-token prompt 95-103
to 47 ms, measured together with STRATA_EMBED_VRAM and STRATA_DEQUANT_SIDE
(+2.2..+2.6 % prompts for the four PLE/embedding/dequant switches together).
Theirs, not measured here.

Original change by linxuhao (PR #3779, commit 0ba47e61).
2026-10-26 20:40:42 +01:00
linxuhao 17b08c9633 Prefill PLE: opt-in STRATA_PLE_COMPACT=1 uploads the raw IQ4_NL rows and dequantizes them on the device (#3779, #3712)
Problem: the prompt path reads 16 PLE rows a token, dequantizes them on the
host and uploads 10,240 floats a token. On a slow host link (PCIe Gen2 x4) that
upload is on the critical path of the first layer.

Change, opt-in with STRATA_PLE_COMPACT=1: for an IQ4_NL table held in memory
(--ple-io ram / mmap) the prompt path copies the raw 90-byte rows (1,440 bytes
a token) into the same pinned buffer, uploads them and runs
ple_iq4nl_dequant on the device (d times the code's value, one multiply, the
split-half nibble order of iq4nl_dequant_row). Any other table (Direct I/O,
pageable fallback, another format) and a failed allocation take the host path
as before. With the variable unset nothing changes: the new code is behind one
cached flag, and the extra members of the Prefill state stay null.

Two things differ from the PR as submitted:
- PleTable::gather_raw_iq4nl writes a row past the table as scale 0 and code 8
  in every nibble. The PR wrote zero bytes, which the device decodes to
  -0.0f (0 times code 0's value -127), while the host path writes +0.0f for such
  a row.
- New test ple_iq4nl_dequant_test (ctest, synthetic, no model, 77 without a
  device): the device decode against iq4nl_dequant_row, bit for bit, over every
  fp16 scale pattern with random codes, random rows, an odd row count and the
  out-of-table row.

Reporter's numbers (linxuhao, CMP 170HX, PCIe Gen2 x4): prompts +1.0..+1.7 %
(4K, 16K) with the switch on. Theirs, not measured here. On a Gen4 x16 card
the gain will be smaller.

Metal gets a stub (the Metal prompt runs through its window and never calls
it).

Original change by linxuhao (PR #3779, commit da818261).
2026-10-26 20:40:20 +01:00
sergiywith dd97421d3f prefill: mmq::swiglu_quant, swiglu and the down product's q8_1 rows in one kernel (#3752)
Problem: the prompt path stores H (swiglu of the gate/up rows) as floats and
then quantizes it to the q8_1 rows the down product reads: two kernels and an
H round trip per MMQ group.

Change: swiglu_quant_kernel, a block per row (128 threads, 4 values each,
blockIdx.y the row's 512-value half), computes swiglu_kernel's expression for
each value and quantize_mmq_q8_1<D4>'s result for each 32-value block, so hq
holds the same bytes and H is never stored. swiglu_quant_ok(type) says where
it applies (a q8_1 D4 down type, no FP4 activations). Built only in the CUDA
build with STRATA_EXPERIMENTAL_SM60; HIP and the other builds get
swiglu_quant_ok() == false and a no-op. Nothing calls it in this commit; the
weight-stationary path of the next commit does. prefill.cpp gets the matching
stubs for builds without the MMQ sources.

Tests: prefill_swiglu_quant_test (new, registered under STRATA_BUILD_TESTS in
the SM60 build, skips with 77 without a device) compares the fused kernel with
swiglu + quantize byte for byte for Q2_0, IQ4_NL and Q8_0 down types, split and
interleaved gate/up rows, a zero row and a row with one huge value.
Run on z440 (RTX A4000, sm_86, CUDA 13.0, SM60 build, gpu lock): all 18 cases print
"0 of N bytes differ" (1, 37 and 4113 rows) and the test ends "prefill_swiglu_quant_test:
ok". sm_60 compile of the tree (CUDA 12.9, P1004): exit 0.

This is a partial port of another contributor's prompt-path fusion change;
only the kernel is taken, as in PR 3752.
2026-10-26 20:38:06 +01:00
linxuhao bdcc34dfa8 Prefill fused IQ experts: opt-in 256-thread kernels STRATA_PF_FUSED_NT=256 and whole-layer launches STRATA_PF_BATCH=N (#3779, #3712)
Problem: on a GA100 card (70 SMs) the 512-thread fused prompt-expert kernels
leave the card idle at every barrier and at every grid tail.

Change, all opt-in:
- STRATA_PF_FUSED_NT=256 sets up a second set of native_kernel instances with
  256 threads and two blocks a multiprocessor (WW 2 only), with two-pass weight
  loads (the loads of the next k step are issued before the current step's
  decode; load_unit_l gives the same words as load_unit). Every output is the
  same thread's sum in the same order.
- STRATA_PF_BATCH=N launches up to N experts at once (WideBatch holds 512 blob
  pointers, 4 KB of kernel parameters: CUDA 12.1+, Volta and newer; HIP, gfx906
  and the Pascal build keep kMaxBatch). Above 128 it needs the NT = 256 kernels.
  Below 128 it only changes the launch grouping.
- With neither variable set the 512-thread kernels, their launches and their
  load sequence are unchanged: tile_rows(ww, 512) equals the old 256 / ww, the
  two-pass code is behind a constant-false TWO_PASS, and the one added test in
  experts_native is occ_nt > 0, which only STRATA_PF_FUSED_NT=256 sets.
- If the 256-thread instances cannot be set up (shared memory, occupancy) the
  engine prints one line and keeps the 512-thread kernels.

Cost: the NT = 256 instances are new device code in every CUDA build (the
reporter measured 365,632 bytes for sm_80).

Tests: new ctest entry prefill_fused_iq_test_nt256 runs prefill_fused_iq_test
(the fused kernels against the double-precision reference and the MMQ path)
with STRATA_PF_FUSED_NT=256; it exits 77 below sm_80.

Reporter's numbers (linxuhao, CMP 170HX): one MoE layer of 3,256 tokens
8.27 ms with NT = 256; prompts +7.6 % (4K), +4.9 % (16K), +4.3 % (52K) for
NT=256 with BATCH=512 on the PR's own build. Greedy answers identical on their
11 prompts. These are theirs.

Original change by linxuhao (PR #3779, commit 056d36a9). Merged onto the Q2_0
fused path of #3809 (STRATA_PF_FUSED_Q2N=1) without textual conflicts; the PR
predates it, so one line is mine: the NT = 256 setup also sets up the Q2_0
gate/up instance when Q2N is on (without it that instance would have launched
without its shared-memory attribute).
2026-10-26 20:38:00 +01:00
Niko1221 945a63bec5 tests: lm_kernels_parity checks the two kernels of the layer-major prompt path (#3976)
moe_shared_init and moe_scatter_add came with the layer-major walk and had no test.
lm_kernels_parity runs them on synthetic data of the model's width (N = 2560, no
model, a few MiB) against a host reference:
- moe_shared_init: acc = shared * sigmoid(sg), within 1e-5 relative (the device
  sigmoid uses __expf);
- moe_scatter_add: the per-token fma chain in row order, bitwise equal to the host
  chain and bitwise equal between two launches (no atomics), over groups of one
  row, one sub-chunk, several sub-chunks, one row per token and eight per token.
Registered in CMakeLists.txt next to gr_upmix_parity (CUDA builds, not gfx906),
SKIP_RETURN_CODE 77 when no device is visible.

Not covered: the walk itself needs a model (see the 3976 gate scripts).
2026-10-26 20:37:49 +01:00
linxuhao 91aebae636 MMVQ-IL: sm_80 row tables, bitwise (#3779, #3712)
Problem: an sm_80 card took the shared interleaved-MMVQ row tables, which
were measured on other cards (#3252 reports A100 slowdowns).

Change: kIlRows_80 (2-4 columns), kIlRows58_80 (5-8 columns) and kIl1Rows_80
(one column), looked up under the key 80 only. They were read off
mmvq_il_parity --bench --emit-table with the same 3 % rule as the other tables.
STRATA_MMVQ_IL_SM80=0 maps an sm_80 card to the key 81, which has no table, so it
gets the shared table again (and no one-column table, as before).

A table only picks the rows a warp (0, 1, 2 or 4); 0 means native_mmvq's kernels.
mmvq_il_parity already checks every rows choice against native_mmvq bitwise for
every type and shape, T = 1..8, so any entry of the new tables is covered.

Reporter's numbers (linxuhao, one CMP 170HX): decode +2.7..+3.2 % over the shared
table, Qwen3.8-Flash-Next IQ3_S. The measured card is not an A100; the rows may
want re-measuring there.

Original change by linxuhao (PR #3779, commit eaedca84); the commit message here
is rewritten. The sm_89 table of #3453 is a separate change in the same list.
2026-10-26 20:34:14 +01:00
linxuhao 269c6d3e4a Dense dequant: the wide kernels by default on sm_80 too, bitwise (#3779, #3712)
Problem: sm_80 (GA100: A100, A30, CMP 170HX) ran the per-group dequant kernels
that sm_61 and sm_86 had already left for the wide ones, because
dequant_wide_on() had no sm_80 entry.

Change: dequant_wide_on() answers yes for compute capability 8.0 as well. The
test is the raw (major, minor) of the device, so it is false on every other
card (sm_60, 70, 75, 86 keep their answer, sm_89, sm_90 and sm_120 stay off as
before). STRATA_DEQUANT_WIDE=0 still turns the wide kernels off.

Reporter's numbers (linxuhao, one CMP 170HX, GA100 sm_80 with 70 SMs): the GPU
time of a 3,259-token prompt went from 857 to 792 ms, prompts +6.3..+7.2 % at
3.3K tokens and +3.9 % at 52K. We have no sm_80 card; these are theirs.

Bit identity: the wide kernels are the ones sm_61 and sm_86 already run by
default. dequant_wide_parity compares them with the per-group kernels byte for
byte for FP16, BF16, FP32, the padded stride and a row slice. The kernels have
no sm_80-only code, so the result does not depend on the card; what only an sm_80
card can show is that ptxas for sm_80 keeps the same arithmetic (see the
checklist in phaseA-tests/gh027/gates/3779-sm80-checklist.md).

Original change by linxuhao (PR #3779, commit 48dc535e); the commit message
here is rewritten.
2026-10-26 20:34:14 +01:00
Niko1221 40c9dae95b Merge f-027-gh-eng-g (#3935 sm_89 in the DeepSeek CUDA package builds, #3796 collapsed tool-name alias and the rejected-calls marker) 2026-10-26 20:31:38 +01:00
Niko1221 805ebe17be serve: a call returned as text for an undeclared name is named in the reply; the collapsed mcp__a__b spelling is the declared tool (#3796)
Problem: a request declares tools; the model writes a complete call for a name that is not among them (bash for shell, a dropped
prefix, an MCP name with every "__" written as "_"). Since #3075 such a call stays text, which is the rule we keep, but the client
saw an ordinary answer with finish_reason stop and nothing else, so the agent turn ended with raw XML and the model never learned
of its mistake. Reporters on OpenCode, Pithagoras, OpenClaw, LibreChat, Open WebUI and Pi hit it.

What changed (declared-only stays: nothing undeclared is ever returned as a call, finish_reason is unchanged):
- serve/frontend.py OutputParser: a declared name that contains "__" is also read when written with every "__" as "_"
  (mcp_playwright_browser_navigate for mcp__playwright__browser_navigate); the call goes to the client under the declared name.
  Same rules as the other aliases: only when unambiguous, and a tool declared under that spelling wins. A reordered or renamed
  tool (server1_label_values for label_values_mcp_server1, bash for shell) is not guessed.
- serve/server.py: when a block went back as text for an undeclared name, the reply carries
  "strata_rejected_tool_calls": [{"name": ..., "reason": "undeclared"}] (the names the parser already collected, up to five): at the
  top level of a Chat Completions reply and in its last streamed chunk, and in the message_delta event and the message of
  /v1/messages. It is absent when nothing was rejected, so every other reply is unchanged. The text itself is untouched.
- The server window line says the response carries the names; the count goes to totals["tool_calls_unread"] and to
  /metrics/prometheus as strata:tool_calls_unread_total.
- docs/DETAILS.md: the alias, the marker, the counter.

Tests: serve/test_tool_name_alias.py (collapsed spelling at several chunk widths, a declared spelling and a plain tool unchanged,
a tool declared in the collapsed spelling wins, two tools with one collapsed spelling are not guessed, reordered/renamed names stay
text and are named), serve/test_server.py ForcedToolChoice (the field in Chat Completions whole and streamed and in /v1/messages,
absent for an ordinary answer and a declared call, the collapsed name is a call; the counter). The whole serve/ suite: 1055 tests,
two failures that also fail on the base tree (test_fatal_recovery on this PC, RestartWindow.test_not_alive_until_ready under load).

Not covered: returning an undeclared call as a real tool call (the owner keeps declared-only); a block for a DECLARED name that came
back as text because it was malformed (counted, no name to list); the /v1/messages field with a real client; a stderr or log-file copy
of the warning.
2026-10-26 20:30:44 +01:00
ruibeikaa b434a8aa6e qsa_select: multi-CTA decode top-k and a shuffle-free decode scorer, opt-in STRATA_SELECT_LONG=1 (#3820)
Problem: a decode window's QSA select runs one CTA per query for the top-k
(block_topk_wide_kernel past ~135K cells) and the warp-shuffle block scorer
(block_scores_multi_kernel), in each QSA layer. Both grow linearly with the
context: on one GP100 die at 300K cells a top-k call takes 0.39 ms with 1-5 SMs
busy and a scores call 0.22 ms (two queries). At short contexts the same work is
small and an extra kernel is pure overhead.

Change: qsa_select.cu (inside !__HIPCC__ && STRATA_EXPERIMENTAL_SM60) gets
block_topk_multi_* (six kernels over several CTAs per query, one per 2,048
blocks of capacity, at most 32, exchanging through caller scratch: no clusters,
no spin waits, capturable) and block_scores_dx_kernel (the multi scorer's
per-head sums built in registers as the same 32-leaf xor-butterfly tree, no
__shfl_xor_sync, the tail block stays block_scores_tail_kernel's). New public
qsa_topk_multi_scratch_ints, qsa_block_topk_long and qsa_block_scores_long
forward to qsa_block_topk / qsa_block_scores outside the build gate or their
conditions. verify.cpp keeps a second set of window graphs (exec_[sel_long_][T])
for windows that reach STRATA_SELECT_LONG_AT cells (default 65536); the choice
is made before a capture and handed to the next stage of an overlapped layer
split, capture_all captures both, pl_launch picks by the window's position.
The ids and the score bits are the replaced kernels'. STRATA_TOPK_MULTI=0 and
STRATA_SCORES_DX=0 turn either half off inside the long graphs.

Review changes against the PR: in a build without the multi-CTA kernels
STRATA_SELECT_LONG=1 now says so and is ignored (the PR would have kept two
sets of graphs for nothing there); Metal gets stubs for the three new calls
(the SYCL tree has its own verify.cpp and qsa_select copy).

Default path: unset, select_long_on() is a cached false, topk_scratch_ is not
allocated, sel_long_ stays 0, and every window uses exec_[0][T], the graph it
used before; the index change is the only difference in verify.cpp.

Merge note for f-027-gh-eng (#3702 evict_window_graph, not in this branch's
base): its exec_ / exec_nr_ / launched_ accesses need the same [sel_long_]
first index.

Tests: src/kernels/qsa_select_long_parity.cpp (new; registered with
STRATA_SCORES_DX=1 STRATA_TOPK_MULTI=1 in the SM60 test block, and a second
case with STRATA_TOPK_MULTI_N=3; skips with 77 without the kernels) compares
qsa_block_scores_long with qsa_block_scores (nq 1-8, n_bid 8 .. 131072, tail
blocks of 0-3 cells, every block 0..n_bid memcmp) and qsa_block_topk_long with
qsa_block_topk (nq 1-16, contexts 1,000 .. 524,288, random scores, all-equal
scores and 7 distinct values for ties across CTAs, selected ids of every query;
the multi scratch is junk-filled and must have been written). z440 (RTX A4000,
sm_86, CUDA 13.0, gpu lock, build with -DSTRATA_EXPERIMENTAL_SM60=ON): "172
cases, 0 differ -> PASS" with one CTA per 2,048 blocks and with N=3. sm_60
compile of the whole tree (CUDA 12.9, P1004): exit 0.

Not covered: no GP100 run here (the kernels' last word is the P100, and the
speed claim). The contributor's numbers (four GP100 dies, every expert resident,
--pipeline-windows 3: 400 tokens at 115K context 51.0 -> 52.5 tok/s, +2.9%;
prompts below 64K unchanged; replies and state hashes equal on 15 requests) were
not re-measured. The model gate is phaseA-tests/gh027/gates/3820-select-long.sh.
2026-10-26 20:25:56 +01:00
Niko1221 40e78366bc Web page: German and dark screenshots in tools/webtest/shots.mjs 2026-10-26 20:23:06 +01:00
Niko1221 4661596ce3 Web page: the chat view in eight languages, and the page's docs
Everything the Chat view shows (header, tabs, message box, settings, toasts,
tool blocks, the sidebar, the history and Load/Unload texts) now goes through the
translation catalog: serve/web/locales/{de,es,ja,ko,pt,ru,zh}.json beside en.json
(written from the English text in the code by tools/web_i18n.py). Plurals follow
the language (Russian has few and many, Japanese and Korean only one form).
serve/test_web_i18n.py fails when a catalog lacks a key, has a stale one or
changes a {placeholder}. A ninth language is one file plus one line in
serve/web/i18n.js. The Monitor, Images and About views are still English (the
language setting says so); they are the next step.

Browser tests: a language pick translates and is kept, /?lang= is for one visit,
an unknown code stays English, all seven catalogs load without an error.

docs/DETAILS.md: a section "The web page" (every switch, what the preview can and
cannot reach, the languages, the tests), the Sampling drawer's new name, and the
credits (KaTeX and the contributors of the PRs ported into the page).
2026-10-26 20:22:05 +01:00
fcd3cdbcd7 Web chat: one chat-history design - saved chats, edit and rewind, branch, send while answering (#1787, #2615, #3823, #4004)
Five overlapping proposals, one set of controls (Chat settings > Chats, all off
by default; off, the page is the page from before: one chat, New chat clears it
with an Undo, Send is off while an answer is written, no new request field).

- Saved chats list (#1787): the chats kept in this browser (IndexedDB, one record
  per chat, names of attachments only), a list with day groups, search over
  titles and text, rename, delete with Undo, collapse; a private window or full
  store keeps the list in memory and says so once. Nothing goes to the server.
  The list has the New chat button, so the composer's one is hidden while it is on.
- Edit and rewind messages (#2615 + #3823): under each message, Regenerate (a new
  answer to the same question), Edit answer (its text only: thinking and tool
  calls go, no request) and Rewind to here. Rewind asks, as #3823 proposes: "Edit
  and send again here" hides this message and the ones after it, puts the
  question and its attachments in the box and keeps one Undo bar ("2 messages
  hidden"); "Branch from here" starts a new saved chat with the messages before
  it and leaves this one alone. A branch keeps a title that says where it came
  from ("Branch of ...", also at the very first message) and links back. One
  control does what #2615 had as Edit on a question and as Remove from here.
- Send while answering (#4004, for the web page): "Wait" (default, as before),
  "Send after" (the message waits and goes when the answer is done) or
  "Interrupt" (the answer stops where it is, what is written stays and is marked
  interrupted, the message goes at once; a running tool finishes first). One
  message waits; Take back returns it to the box; Stop keeps it for you to send.

The pure logic is serve/web/history.js, the store serve/web/chatstore.js, the
page's part serve/web/chats-ui.js. Tests: serve/test_web_history.js (Node, with
the IndexedDB stub and cases of #1787), serve/test_web_history.py, and 17 browser
tests (tools/webtest/browser_test.mjs). Screenshots: tools/webtest/shots.mjs.
Also: the Sampling drawer is now Chat settings (its button says so), a wide
window lets a message use the column (#3176 widened only the column).

Ported by hand and merged: #1787 by Scotty Hudson, #2615 by Joyer Huang (uppet),
issues #3823 and #4004 by xvcezrqpbxtpzblrt (their proposals, for the desktop app,
carried to the web page).

Co-authored-by: Scotty Hudson <[email protected]>
Co-authored-by: Joyer Huang <[email protected]>
Co-authored-by: xvcezrqpbxtpzblrt <[email protected]>
2026-10-26 20:14:14 +01:00
Niko1221 a732a34c65 layer-major prefill: a prompt that needs a run_impl feature the walk lacks takes the chunk-major path (#3976)
The layer-major walk is a copy of run_impl's attention half with the experts moved
out. It does not carry four things run_impl does, and when one of them is asked for
the walk now declines (the prompt is read chunk-major, and the one-time "layer-major
off for this prompt" line names the reason):
- the DFlash2 drafter's prompt taps (tap_layers_): the walk would leave the taps
  stale and the drafter would read old rows;
- STRATA_KVA (approximate late layers): the walk would silently read exactly;
- a stop_at_chunk callback (the pipelined batch reader that yields at chunk
  boundaries): the walk reads every token, and stopped_at() must stay -1;
- a pending force_chunk (a --prefill-pipe plan): it is left set for run_impl.
The walk also resets stopped_at_ like run_impl does at the start of a run.

With STRATA_PREFILL_LAYER_MAJOR unset none of this is reached.

Not covered: no model run in this commit.
2026-10-26 20:13:56 +01:00
Niko1221 d02478b7a1 layer-major prefill: stays off on AMD builds (#3976)
Only NVIDIA CUDA has run the layer-major walk. On a HIP build (wave32 or the
gfx906 compat layer) lm_prepare now declines with a reason, so a prompt takes the
chunk-major path and the one-time "layer-major off for this prompt" line names it.
The code still compiles everywhere the shared prefill.cpp does; nothing else
changes. With STRATA_PREFILL_LAYER_MAJOR unset nothing is reached.

Not covered: HIP and SYCL were not built or run (SYCL has its own prefill.cpp).
2026-10-26 20:07:56 +01:00
Niko1221 0acba452ec Test: app-ui 'Quit works once' waits for the shutdown callback (polling, up to 5 s) instead of a fixed 20 ms sleep, so a busy PC cannot fail it (test only) 2026-10-26 20:06:26 +01:00
Norihiro Nishimura e7318fa23d layer-major prefill on a split: a stale hand_in_ left by a chunk-major prompt no longer turns layer-major off (#3976)
run_impl never clears a later stage's hand_in_, so after one short prompt (read
chunk-major) every later prompt was declined with "a layer split's chunk hand-off
is in use". lm_prepare_chain now declines only when a hand-off is really in flight
(a stage's next_run_ is valid) and otherwise clears the stale hand-in pointers.
2026-10-26 20:03:03 +01:00
Norihiro Nishimura 18c5357666 layer-major prefill: even super-chunks (STRATA_PREFILL_LM_EVEN, default on with layer-major) (#3976)
A prompt longer than the common super-chunk is split into the same number of
super-chunks of near-equal size (32K at a 28K fit: 2 x 16K, not 28K + 4K). That
keeps a stage's residual rows in VRAM and lets a split's stages overlap on every
super-chunk. STRATA_PREFILL_LM_EVEN=0 gives the old largest-first split. It only
acts when STRATA_PREFILL_LAYER_MAJOR is set.
2026-10-26 20:03:03 +01:00
Norihiro Nishimura c6f6db857b layer-major prefill on a split: the PLE layer is fine in a first stage that holds layers 0 and 1 (#3976)
The split was declined whenever the model has PLE layers. The PLE block (layer 1)
reads the token history, which only the first stage has, and a later stage never
reaches it. So the split is declined only when the first stage does not hold both
layer 0 and layer 1.
2026-10-26 20:03:02 +01:00
Norihiro Nishimura 6eff377a79 layer-major prefill on a CUDA layer split: each stage runs its layers over the super-chunk and hands its rows to the next stage (#3976)
Problem: layer-major prefill declined every layer split, so a split model kept
re-reading each layer's streamed experts once per 8192-token chunk.

Changed:
- lm_prepare_chain sizes every stage with the same super-chunk (the smallest any
  stage fits, up to four rounds until they agree) and gives them back together;
- a stage runs only its layers [LB, LE); a later stage's rows arrive in its pinned
  R_host;
- a stage before the last writes its final rows into the next stage's R_host on
  the copy stream, then runs the next stage on a thread (std::async) while it reads
  its own next super-chunk. There is one hand-off buffer: a stage's final pass waits
  until the next stage has finished the previous super-chunk;
- layer_major_mid() asks the last stage, the one that reports the rows;
- run() drains the pipeline after the walk and clears the mid flag on every stage.

Not covered: only two stages were tried by the reporter (RTX 3060 + RTX 5060 Ti);
the peer-GPU tier and the stage helper have no layer-major form and decline.
2026-10-26 20:03:02 +01:00
Norihiro Nishimura cc125e06a0 layer-major prefill: no MMQ row table without an MMQ layer; QSA shapes from core::qsa_shapes (#3976)
run_layer_major called mmq::iota on ids_identity, which is null when no layer is on
MMQ; it now skips that. The QSA shapes come from core::qsa_shapes(g) as run_impl
does, instead of being filled in by hand.
2026-10-26 20:03:00 +01:00
Norihiro Nishimura 5602253e3f layer-major prefill: the FP16 path needs no MMQ identity table or token upload buffer (#3976)
lm_prepare required ids_identity and tok_dev even when no layer is on MMQ. Only a
model with an MMQ layer needs the identity table, so the check now asks for it only
then. Without this a model whose layers all take the FP16 path was declined.
2026-10-26 20:03:00 +01:00
Norihiro Nishimura 42094da9c1 layer-major prefill: the FP16 expert path for layers MMQ does not take, decline reasons name the layers (#3976)
Problem: lm_prepare declined every model whose experts are not all on MMQ. All 48
layers of UD-Q4_K_XL take run_impl's FP16 path, so layer-major never ran on them.

Changed: a layer outside the MMQ plan dequantizes each routed expert to FP16 once
per super-chunk and runs two FP16 GEMMs per entry on that entry's FP16 input rows
(a new per-super-chunk buffer mixed_h, the bits run_impl's gather reads), then
adds the group's rows per token with moe_scatter_add as the MMQ path does. The
buffers each path needs are checked separately, and a missing one gives a decline
reason that names it. The summary line counts the groups on the FP16 path.

Not covered: no model run in this commit.
2026-10-26 20:02:59 +01:00
Norihiro Nishimura df7068b6be layer-major prefill: STRATA_HC_Q8 dequant, guards for APR/NVFP4/WMMA layers, QSA indexer calls of this tree (#3976)
Problem: the layer-major walk (#1670) was written for an older base. It read the
hyper-connection matrices as bf16 only, accepted expert formats its MMQ pass does
not carry, and called the QSA indexer with the older argument lists.

Changed (Norhiro's port of vizdatom's walk, kept as one step):
- the HC projections are dequantized per half when STRATA_HC_Q8 holds them as Q8_0
  with no bf16 copy, and released after the GEMMs that read them are enqueued;
- lm_prepare declines (the prompt then runs chunk-major) for a layer that needs the
  WMMA tiles, an APR (Hadamard-folded) layer, and an NVFP4 layer;
- STRATA_PREFILL_DIRECT is not used by the walk.

Not covered: no model run in this commit.
2026-10-26 20:02:59 +01:00
vizdatom ffe40c1695 prefill: layer-major prefill (STRATA_PREFILL_LAYER_MAJOR=1) (#3976, #1670)
A super-chunk of S tokens is read layer by layer: each 512-token sub-chunk runs the attention half,
the FFN half hyper-connection read, the router and the shared expert in order (GDN / QSA / PLE state
advance as in the default path); then the layer experts run once for the whole super-chunk - each
routed expert staged once, its rows of every sub-chunk in one MMQ group, the results added per token.
Expert traffic drops by about S / 512 (a 12.9K prompt: ~509 GB -> ~79 GB on ryzen).

- STRATA_PREFILL_LM_TOKENS: the super-chunk target (default 8192), shrunk to fit free VRAM minus
  STRATA_PREFILL_LM_MARGIN_MIB (default 96).
- STRATA_PREFILL_LM_R=vram|host|auto: the residual rows of the super-chunk in VRAM, or in pinned RAM
  with the copies overlapped on the copy stream (auto: VRAM only when the whole target fits there).
- STRATA_LM_PREFETCH=0: no prefetch (default on: every streamed expert of a layer goes to the stager
  when its attention halves start; the ones the routing skips are skipped if not started).
- Not for a layer split, a peer GPU, image prompts, the fused layout or a prompt under two chunks.
- on_chunk still reports every sub-chunk in order; layer_major_mid() tells serve not to checkpoint
  before a super-chunk end (the state is already at that end).

Port to 0.1.43.1: the copied run_impl body follows the new base. Every blob the stager or the copy stream reads later
comes from blob_stable() (blob() may hand out a recycled STRATA_IO_PREFETCH buffer); the token ids go up through tok_up,
the QSA step records and the PLE rows' pageable fallback through up_ring (#1712, #1996); the PLE gather threads pin to
--aux-cpus; bf16x2 takes m.f16_io; the K8V4 append is upstream's, which writes the host copy and the staging pool by
halves. The indexer passes the INT8 pooled keys as the original does, in the QSA port's order (pooled_i8 and
pooled_scale after block_pos, the int8 qsa_block_scores(_tc) overloads); STRATA_IDX_INT8 stays opt-in and unsupported on
the prompt path, as in the original. stage_one calls the stager's pass() before wait(): with batched claims a routed job
after about a ring's depth of skipped prefetch jobs waited forever. lm_prepare reads the fused layout from m.fused_bufs
and declines while STRATA_DBG_FEAT, STRATA_DBG_ROUTE_OVERRIDE or STRATA_PREFILL_DUMP_R_ALL is set. serve's near-tail
checkpoint (--prompt-cache-tail) is not taken mid-super-chunk, so under layer-major it is taken only when the last
super-chunk is a single sub-chunk.

From vizdatom's #1670, original commit .
2026-10-26 20:01:55 +01:00
vizdatom a6df0023a4 prefill: kernels for a layer-major expert pass (moe_shared_init, moe_scatter_add) (#3976, #1670)
moe_shared_init writes the shared expert part of moe_combine before the routed experts are known;
moe_scatter_add adds one expert group rows into a per-token accumulator, per token in a fixed order
(deterministic, no atomics).

From vizdatom's #1670, original commit f0173382d0646907917edf99e59617c1412d898e.
2026-10-26 20:01:36 +01:00
vizdatom 162dec3411 prefill: the per-layer walk warms this layer's and the next layer's file-tier experts (STRATA_PREFILL_WARM=1) (#3976, #1670)
Chunks below stream_all_min() stage each layer's unpinned experts on the stager's few threads, and with a mapped
file tier each of those is a chain of 128 KiB page faults, a shallow queue for the drive.  With STRATA_PREFILL_WARM=1
(and a source that warms: the GGUF in place, or experts.bin with STRATA_BIN_WARM=1) a background thread hands the
source the layer's whole list at once before the stager starts, and the next layer's list from the previous chunk
(less what is on the GPU or pinned) while this layer computes.  It only warms pages: the stager copies the same
bytes.  At most four lists queued; a thread that falls behind drops the oldest.  Off by default: the A/B arm.

Port to 0.1.43.1: the warmer thread pins itself with aux_cpus::pin_current_thread() first, like the other helper threads since --aux-cpus (it would otherwise inherit the host thread's CPU).

From vizdatom's #1670, original commit cabe33c4f585d4c9dd0af1fc9f014d00e2920042.
2026-10-26 20:01:36 +01:00
vizdatom fca1936c2c prefill stager: a job waits for the previous job on its buffer; skip() (#3976, #1670)
issued is a watermark, so a job nobody waits for could be passed while its thread was still writing
the pinned buffer the next job on that buffer was filling - a corrupt expert blob. A job now also
waits until the job kRing before it has marked itself ready. skip(j) lets a job the routing turned
out not to need be marked ready without reading it (the layer-major prefetch). The default walk
consumes every job in order, so neither changes it.

Port to 0.1.43.1: the stager now claims batched runs and sleeps in issued.wait, so the ready[i - kRing] wait
sits in the run's per-job wait loop after the issued wait, and a skipped job is left out of the run's gather
(no memcpy, prefill_copy or copy_blobs entry); its ready flag and `active` stay with the run's single store and
decrement. New pass(j): skipped jobs are never issued, so the layer-major walk moves the watermark to the
routed job it is about to wait for (store plus notify_all, which wakes sleeping threads); without it a routed
job after about a ring's depth of skipped ones waited forever. The default walks never call it.

From vizdatom's #1670, original commit 149b899548fbc98977219e6fd4ee7866384e61f6.
2026-10-26 20:01:36 +01:00
Niko1221 8486336f9b Merge f-027-gh-eng-s (#3291 #3955 #3317 #4036 #2893 #3218 #3200: serve, tools and setup ports) 2026-10-26 20:00:53 +01:00
Niko1221andSakurai Momoka bcdb92ac68 Web page: Load and Unload in the header (#3550)
One button in the header of every tab: Load while the model is unloaded, Unload
otherwise. It calls POST /load and POST /unload (the server already had both,
for its own page only). Unload asks first and never interrupts a turn: the
button is off while an answer is being written, an image is made or a request is
queued. The pill and the Monitor say Unloaded / Loading / Unloading, a message or
an image on an unloaded model shows Loading, and a poll that started before the
POST cannot flip the button back (the POST's state is held until a later poll
agrees). On a phone the pill's text is the first thing the header row gives up
(it measures the row, not a fixed width), so Load/Unload and the theme button
stay on the screen from 320 px.

Browser tests (tools/webtest/browser_test.mjs): unload with its question, load
back, declining, not while answering, a message that loads, the header at 320 to
640 px.

Ported by hand from PR #3550 by Sakurai Momoka (sakuumomo); the texts now go
through the page's translation catalog.

Co-authored-by: Sakurai Momoka <[email protected]>
2026-10-26 19:59:54 +01:00
Niko1221andThomas Ivesdal-Tronstad 0aa64b2de3 Web chat: preview a generated svg or html block in a sandboxed frame (#1628)
A complete ```svg or ```html block in an answer gets a Preview button. The
block's own code is drawn in an iframe (srcdoc) that is cut off from the page:
sandbox="" (an opaque origin: no access to the page's DOM, storage, cookies or
server, no scripts, forms, popups, downloads or navigation of the page), a CSP
in the frame's document (default-src 'none': no network, fonts, frames or media;
styles inline, pictures as data: only; no <base>, no form target), no referrer
and no permissions. A well-formed svg is shown as an <img> with a data: URL: an
image runs no script, loads nothing and animates in every browser. The frame
takes the page's light or dark colours and is built again when the theme
changes. A page (html) can also be run WITH scripts, one block at a time, after
a warning that is shown every time: sandbox="allow-scripts" only (still an
opaque origin), the CSP allows the block's inline script and no address; closing
the preview ends it.

What the sandbox flags alone do not stop is a link or <meta refresh> inside the
model's html loading a web page into the frame. The browser test shows it does
(tools/webtest/browser_test.mjs, with and without the header). So the page is
served with Content-Security-Policy: frame-src 'none' (nothing else: a srcdoc
frame inherits the page's policy, and the script preview needs its inline
script), and a <meta http-equiv=refresh> in a previewed page is dropped.

tools/webtest holds the mock server (the page's own files, scripted answers,
no model) and the browser tests; serve/test_web_preview.js checks the documents,
flags and frames, serve/test_web_preview.py the header and that the page never
listens for a frame's postMessage.

Ported by hand from PR #1628 by thoives.

Co-authored-by: Thomas Ivesdal-Tronstad <[email protected]>
2026-10-26 19:57:56 +01:00
Niko1221 675f7fc238 Merge f-027-gh-eng-k2 (#2526 #1864 #1911 #4005 #3060 #4010 #3809: PR ports for the conversation cache, prefill events, expert source and batch groups) 2026-10-26 19:54:59 +01:00
chaog992 c47c699486 prefill: opt-in fused int8 prompt experts for the native Q2_0 pack on sm_80+, STRATA_PF_FUSED_Q2N=1 (#3809)
A PC whose CPU lacks the AVX-512 set that setup checks for gets the native Q2_0 pack. On a CUDA card of sm_80 or newer its
Q2_0 / Q2_0 prompt experts run through MMQ, although the fused int8 kernel for that format (#2145) is in the binary and set
up only on a Turing card (b6443f3ae).

STRATA_PF_FUSED_Q2N=1 sets that kernel up on any sm_80+ card and takes it for the Q2_0 / Q2_0 layers, under its own variable
so that STRATA_PF_FUSED=1 keeps its bits. STRATA_PF_FUSED=0 and STRATA_PF_FUSED_NATIVE=0 still turn it off. A failed setup
leaves the occupancy as it was and the layers on MMQ.
- moe_fused_iq.cu: DevInfo.q2n; dev_info() sets up Q2_0 gate/up when the switch is set; gu_covered() and native_supported()
  take the pair only when q2n is set (a Q2_0 gate with another down type is refused, as on Turing).
- tests/cuda/prefill_fused_iq_test.cpp: --unset-pf-fused leaves STRATA_PF_FUSED unset (the engine's default) and skips below
  sm_80; CMakeLists.txt: the ctest entry prefill_fused_iq_test_q2n runs the Q2_0 / Q2_0 pair with the switch alone (exit 77 =
  skipped without an sm_80+ card; CUDA branch only).
- docs/DETAILS.md: one section in the author's measured numbers (RTX 5080) and the A4000 caveat below.

Unset (the default), nothing changes: dev_info() reads the variable once and sets nothing up, gu_covered() and
native_supported() return what they returned. Read line by line against base: the Turing path (t75) and the HIP path are
untouched. Set, the prompt arithmetic changes (first-token KL against FP16 0.0110, MMQ 0.0112, the author's figure) and the
fused path's smaller buffers let --prefill auto pick a larger chunk, which is most of the author's 16K gain; the generated
text can change.

Not verified: an RTX A4000 (sm_86). The kernel was taken on every CUDA card in #2145 and then kept to Turing after an A4000
no longer matched the old output and read 4K prompts 4.5% slower; the PR does not repeat that run. Merge note: #3277 and #3779
change dev_info() and native_supported() in the same file.

Tests: prefill_fused_iq_test_q2n and prefill_fused_iq_test pass on an RTX A4000 (sm_86, synthetic data); on the Q2_0 / Q2_0 layer the
fused kernel is 1.76x / 1.30x / 1.24x the MMQ path at 2048 / 4864 / 8192 tokens with the same relative RMS against FP32. Builds on CUDA
sm_86 (Linux) and sm_120 (MSVC). Not covered: no model run; the engine-level A4000 check is the gate.
2026-10-26 19:54:00 +01:00
Niko1221andDavid a965c962cf Web chat: draw math with the bundled KaTeX, opt-in and offline (#2972, #2974)
Chat settings > Render math (off by default). When it is on, \( \), \[ \], $x$
and $$ $$ in an answer are drawn as formulas by KaTeX 0.19.0, which is vendored
in serve/web/vendor/katex (script, style, 20 woff2 fonts, MIT licence and a
PROVENANCE.json with the npm integrity and the sha256 of every file, so the page
needs no network). Nothing of KaTeX loads until an answer holds a formula.

The shared core (math.js, from #2972) keeps the text of every message exactly as
the model wrote it: formulas are swapped for placeholders before the Markdown
runs and drawn over them afterwards, so history, copying, the Markdown export
and the requests never see the drawing. Code, link targets, prices ($5 and $10)
and a formula still being streamed stay text. A formula that fails, is over
16 KiB or times out stays as its source text.

Safety: KaTeX runs with trust off (no \href, \url, \includegraphics, \htmlClass:
they are drawn as their own name), maxExpand 1000, maxSize 20 and a fresh macro
table per formula; it builds DOM nodes itself, so no HTML string from a model
reaches the page. A formula already drawn is placed again at once while an
answer streams, so it does not flicker back to source.

serve/test_web_math.js runs the real vendored KaTeX against hostile formulas;
serve/test_web_math.py checks the vendored files against PROVENANCE.json and
that nothing outside this server is addressed. serve/web/vendor is -text in
.gitattributes so the hashes hold on a Windows checkout.

Ported by hand from PRs #2972 (core) and #2974 (KaTeX) by CC-David-CC.

Co-authored-by: David <[email protected]>
2026-10-26 19:52:56 +01:00
Niko1221 d5cca319c9 Web page: a translation mechanism, and a language choice in About > Settings
The served page had no way to translate its text: every string was English in
the HTML and the script. Now the English text stays in the code next to a key
(tr("key", "text", vars), data-i18n="key" in the HTML), a language is a catalog
serve/web/locales/<lang>.json, and a catalog that is missing or has no entry for
a key leaves the English text. The language is the user's choice (About >
Settings), kept in this browser; nothing is chosen from the browser's language,
because a page translated in part would mix two languages. /?lang=de shows one
for a visit.

The server hands out /web/locales/<lang>.json and /web/vendor/katex/** from
serve/webfiles.py: whitelisted names and types, no backslash, colon, percent or
dot-dot, and the resolved path must stay inside the folder (also against a link
out of it). tools/web_i18n.py reads the English text out of the code, writes
en.json and checks every catalog for the same keys and {placeholders}; the
check runs once the catalogs exist.
2026-10-26 19:52:26 +01:00
Niko1221 3813a459c4 Split serving: exactness and speed check tool, docs draft
Idea and manual setup by @MarcoTool in #4000.
2026-10-26 19:48:29 +01:00
gopinath87607 fa128f91e8 docs: --prompt-cache-every does not set the size of the pieces a prompt is read in (#3311, #2834)
Only the part of PR #3311 by gopinath87607 that is about this engine: a periodic mark is saved at a chunk boundary of the batched prefill and the read carries on
(src/program/generate.cpp, periodic_checkpoint), so the interval is not a piece-size dial; what cuts a read into runs is the re-read, the root, a message
boundary and the start of the last turn; --prompt-cache-every 0 skips the periodic and the near-tail marks. The wall-time numbers and the comparison with the Maya
checkout belong to another engine and are left out. New section in docs/PROMPT_CACHE_TAIL.md, linked from the option's row in docs/DETAILS.md.
2026-10-26 19:47:22 +01:00
Niko1221 47c98bb23a strata launch --show-key shows the key of a model setup.py set up (#3570)
--show-key only knew a running launch or a preset. A model set up by setup.py (START-HERE.bat, ./setup.sh) keeps its generated key in strata-<model>.json, so the command
said there was no key and people read the JSON by hand. With no launch and no --preset, it now looks at the run configs of the installed models: one model with a key
prints that key (stdout is the key alone; a hint line with the model and the file goes to stderr), several models ask for --model NAME (any case, the size or the full
id), a name that matches nothing says so. --preset never falls back to a setup config, and no key is printed while the choice is open. Tests:
tests/terminal/test_launch_show_key_setup.py.
2026-10-26 19:45:45 +01:00
Niko1221 726472a229 Merge f-027-int2 into f-027-app2 2026-10-26 19:43:59 +01:00
Niko1221 10a62b8d2f docker: model files left in the older /data/models stop the start instead of being downloaded again (#3421)
A volume or bind mount from the 0.1.x image keeps its models in /data/models. The entry point moves a folder that is part of the volume, but a folder the person
mounted there is left alone, and a failed move leaves the files too; with no MODEL_PATH the start then did not see them and downloaded the model again
(tens of GB) without a word. Now, when .gguf files are still in /data/models and MODEL_PATH is not set, the container stops with code 2 and names the folder,
says what to do (MODEL_PATH to the folder with the shards, or the files into /data/Strata-data/models) and that nothing was downloaded.
ALLOW_DOWNLOAD=1 goes on anyway with the same text as a warning. docs/DOCKER.md lists the setting. The other points of the issue (the key, the removed
variables, the status loop) are the owner's design questions and are not changed here. Tested by running the guard function with sh (tests/terminal/test_docker_files.py);
the whole container was not run (no docker on this PC).
2026-10-26 19:42:58 +01:00
Niko1221 690172c464 Split serving, first version: GEN 0 prefill=1 in the engine, serve/split.py (SplitEngine, config split_serve / --split-serve), tests
Prefill on one card, decode on another, the session handed over in RAM through the engine's
own session file (SAVE/RESTORE). The engine's new request form GEN 0 prefill=1 reads a prompt
as a request does, up to where the first verify window starts, and stops with the session at
n - 1 tokens, which is the place a continuing request resumes from. Opt-in; without
split_serve nothing changes.

Idea and manual setup by @MarcoTool in #4000.
2026-10-26 19:42:41 +01:00
Niko1221 4a759f82d4 DeepSeek engine packages: add sm_89 (RTX 40) to the CUDA builds (#3935)
Problem: the strata-dsv41 packages were built for sm_86 and sm_120 (+ PTX 120). An RTX 4090 user was stopped at setup on 0.2.4.1; since
b750d757f an sm_86 package is accepted on an sm_89 card (cubin same-major rule), but the engine had no sm_89 image of its own, and the docs said
nothing about RTX 40 or about building the engine for a card the packages do not cover. The reporter built strata-dsv41 for 86-real;89-real
themselves and ran the 2.00 set at 40.2 tok/s on an RTX 4090 48 GB (theirs, --dspark on).

What changed:
- packaging/build_dsv41_cuda.sh and build_dsv41_windows.bat: the default architectures are now 86-real;89-real;120, so the DeepSeek CUDA 13
  packages list sm_86, sm_89, sm_120, compute_120 (Windows and Linux alike). The CUDA 12.9 line in the script header says ARCH="86-real;89".
- CMakeLists.txt: a build of strata-dsv41 with no -DCMAKE_CUDA_ARCHITECTURES defaults to 86-real;89-real;120 (the Qwen engine's default
  stays 120, and the warning now names the architectures it picked).
- strata_control/engine_component.py arch_native(): true when a package holds machine code for the card itself. resolve() and
  feed.select_engine() use it as the last tie-breaker, so with otherwise equal packages the one with the sm_89 image is taken on an RTX 40;
  an sm_86-only package still fits (no refusal).
- setup.py: the "engine not available" stop now points at the build recipe (docs/DEEPSEEK41.md); the developer line names the
  architecture to pass. The card list already said RTX 30, 40 and 50.
- docs/DEEPSEEK41.md, docs/MODELS.md: RTX 40 coverage, the reporter's measurement quoted as theirs, no claim of our own (we have no Ada card),
  and a section "Building the engine for your card".

Tests: tests/terminal/test_engine_manifest.py (arch_native; the build scripts and the CMake default name sm_89 and both OSes agree; the
package list they give is sm_86 sm_89 sm_120 compute_120; an sm_89 card is accepted and prefers the package with sm_89). tools/test_setup_dsv41.py
unchanged and passing.

Not covered: no run on an Ada card (gate script in phaseA-tests/gh027/gates/3935-ada-dsv41.sh).
2026-10-26 19:42:11 +01:00
Norwin ab48216a7b docs/MULTI_GPU.md: say when a layer split pays on a slow CPU (#3930)
One bullet from PR #3930 by NorwinYu: on two identical RTX 2080 Ti over a dual-socket Xeon E5-2696 v4 a plain split moved decode from 35.2 to 51.0 tok/s and an
11.6K prompt from 672 to 908 tok/s, because the CPU pool per window fell from 9.52 to 0.79 ms (STRATA_DECODE_TIMING=1). The page's own 5080 + 3090
figures, where the second card is on par, came from a fast CPU; the bullet says so and that it is one measurement. No code changes: setup already splits
across two cards. Shipped without a hardware run of ours; the reply asks owners of two older cards on slow CPUs to compare and report.
2026-10-26 19:40:24 +01:00
Matthias Nott ad1a310f55 Web page: use wide windows and fit narrow screens (#3176)
The chat, About and Monitor columns grow with the window (the chat and the
composer to 1100 px, About to 1100 px, Monitor to 1920 px), the metric cards
wrap with auto-fit, and under 640 / 560 / 480 px the controls get bigger
touch targets, the composer bar wraps, and long words, code and tables stay
inside the viewport.

Ported by hand from PR #3176 by Matthias Nott (mnott); the CSS is his.
2026-10-26 19:40:00 +01:00
Vop3p c2490b49f5 docs/AMD_HIP.md: why STRATA_SH_STREAM goes both ways, and leave it off on Windows with KV streaming (#3895)
Two paragraphs from PR #3895 by Vop3p, put under the table row they explain: the fork pays only when it has a CPU-pool wait to hide (4,859 slots on one
RX 6900 XT, GPU-reach wait 20.7 of 39.1 ms; with every expert resident it is 43.4 against 62.9 tok/s on an R9700), and the Windows RX 6900 XT fault with
the fork plus --kv-resident, its bisection and its limits (one PC, kernel not isolated). The text now says what the code does about it and asks RX 6000 owners on Windows to test.
2026-10-26 19:39:43 +01:00
Niko1221 5f81454038 AMD on Windows: the shared-expert fork with KV streaming is not measured, never picked, and warned about when set by hand (#3895)
On one Windows RX 6900 XT STRATA_SH_STREAM=1 together with --kv-resident failed the engine on its first request (verify: unspecified launch failure).
setup --calibrate (tools/calibrate.py) no longer measures the fork on Windows when KV streaming is on (the fork is off by default on AMD, so
nothing changes unless the person sets it), strata tune's never-pick list holds the combination (strata_control/tune_knobs.py), and a config that
sets STRATA_SH_STREAM=1 with --kv-resident on Windows HIP gets one warning line when the engine starts (serve/server.py sh_stream_warning). Nothing is refused or
removed: the variable still reaches the engine. Shipped without a hardware run, from one report; the reply to #3895 asks RX 6000 owners on Windows to test
and report. Tests: tests/terminal/test_sh_stream_windows.py.
2026-10-26 19:39:42 +01:00
Niko1221 d98d8f977f docs/TROUBLESHOOTING.md: every entry of v0.2.4.2 is back, updated for 0.2.7
The 0.2.5 rewrite dropped about 25 entries: the Mac quarantine steps, strata doctor, sending logs, the first-start freeze, the graphics memory the app keeps free,
Smart App Control, UPDATE.bat and git pull (#1276), the build tools and the gcc/glibc compile problems, the engine checksum, the engine stopped unexpectedly,
slower than the tables, the empty-reply and Turing switches, --expert-cache and --resident-budget-gib, the page-locked OOM, the NVIDIA 0x141 reset, CUDA 13.2 on
RTX 50, the AMD section (KFD, the build list, 0xC0000005, the driver reset, pinned allocations, verify timeouts) and the body of the verify-timeout entry, which
the rewrite had left empty. Each is back with the 0.2.7 facts: the app's reserve defaults (native-resources.mjs), the Drafting setting, --vision on|off|gpu|cpu
in the Terminal and --vision no in a source checkout, Intel Arc's CPU reader, and the Terminal route next to the source-checkout one.

Every pointer resolves: setup.py's driver-reset pointer, the server's Smart App Control message, DETAILS.md's STRATA_KEEP_EMPTY_TURNS / STRATA_TOPK_STREAM line,
OLDER_GPUS.md and MAC.md. tests/terminal/test_troubleshooting_page.py checks the entries, those pointers and the page's own links and anchors.
2026-10-26 19:36:15 +01:00
ruibeikaa a338990186 qsa_pattn: sm_6x prompt attention without partials, opt-in STRATA_PATTN_SM60=1 (#3943)
Problem: below sm_70 the prompt reads each QSA layer's attention through the
decode kernels (qsa_prompt_attn_batch returns false without tensor cores): 32
queries a call, every chunk's partial results (817 KB a query) written to
scratch and merged by a second kernel.

Change: src/kernels/cuda/qsa_pattn.cu keeps the contributor's default variant
only, ps_score_x_kernel (q in registers, the chunk's K staged with 16-byte
loads, two cells at a time through a select-free reduce-scatter) and
ps_value_d4_kernel (four dims per thread, merged into the output as the merge
kernel does). Every float operation of attn_chunk_kernel_pre75 and
attn_merge_kernel is kept in its order, so the output is the decode kernels'
bit for bit. The PR's other variants (per-chunk, fused persistent, bench split
codes), pattn_bench and the variant argument are not ported; pattn_bench needs
a STRATA_QSA_DUMP file from a model run.

Gating: compiled only in the CUDA build with STRATA_EXPERIMENTAL_SM60 (HIP and
Metal link a stub that answers false; prefill.cpp keeps the call out of the
Metal build). At run time only with STRATA_PATTN_SM60=1, on compute capability
6.x, int8 K/V, head_dim 256, 12 query heads per KV head, at most 33 chunks.
Unset, the only cost on the default path is one cached int test per QSA layer,
and only when qsa_prompt_attn_batch has already declined. Any refusal or a
failed launch returns false and the usual 32-query loop runs (one warning, no
exit; the PR called exit(1) on a launch failure).

Tests: src/kernels/qsa_pattn_parity.cpp (new, registered in CMakeLists.txt in
the experimental sm_60 test block, skips with 77 when the kernels are absent)
compares the kernels with the Daily's chunk kernel plus attn_merge_kernel on
random int8 K/V: a shuffled page table with ~3% unresident pages and a run of
64 unresident pages (whole chunks masked, some queries empty), three selection
shapes, caps 64 / 130 / 2051 / 2112, widths 0..cap, n_q up to 40 with a
scratch that holds only 2 decode-layout queries (several batches), junk in the
scratch and output. On a Pascal card it also requires the checked entry to take
the shapes and qsa_decode_attn_batch to match. The kernel entry that skips the
compute-capability test (qsa_pattn_batch_any_arch) exists for this test only.
Results: z440 (RTX A4000, sm_86, CUDA 13.0, build with
-DSTRATA_EXPERIMENTAL_SM60=ON, gpu lock): "qsa_pattn_parity: 468 cases, 0
differ, 29724792 nonzero output values in all -> PASS". sm_60 compile of the
whole tree (CUDA 12.9, P1004): exit 0.

Not covered: no P100 run here (the Pascal-only checks of the test and the
speed claims wait for the gate, phaseA-tests/gh027/gates/3943-pattn.sh). The
contributor's numbers (43.3 -> 30.8 ms a 1,536-query chunk per QSA layer at
26K context, 2-3% less time on 8K-150K token prompts on four dies) were not
re-measured; compute capability 6.1 is covered by the same switch and was not
measured by anyone.
2026-10-26 19:24:50 +01:00
Niko1221 9db497056b docs: the two master switches in the 0.2.7 release-notes draft 2026-10-26 19:14:00 +01:00
Niko1221 a3ab37fc03 Master switches DS4_DEFAULTS=026 (DeepSeek) and STRATA_DEFAULTS=026 (Qwen): every default changed since v0.2.6 gives v0.2.6's behaviour; explicit per-feature variables and flags still win; every rule is a pure function with a v026 argument and a test 2026-10-26 19:13:25 +01:00
Niko1221 6f1bd21e59 App: "Faster answers on 12 GB NVIDIA cards" row; DeepSeek speed-ups Off also writes DS4_DEFAULTS=026
The Speed group gets a row for Qwen on a discrete CUDA card of 13 GiB or less: "Prefers the parts of the model already on the graphics card, which makes answers
much faster on 12 GB cards. Turn off if answers look worse than before." On (recommended) by default; Off writes STRATA_ROUTE_PRIOR=0, On removes it. The
DeepSeek speed-ups row, when Off, writes DS4_DEFAULTS=026 and the six DS4_*=0; On removes all of them, and a file that holds only the six (an earlier test
build) shows as Off. The Qwen "0.2.6 behaviour" switch is Terminal-only and is not in the app. New strings fx82 and fx83 in the eight fix catalogs.
2026-10-26 19:10:55 +01:00
Niko1221 211fca87df Shared speed-switch table and Terminal: DS4_DEFAULTS=026 with the DeepSeek off switch, the 12 GB NVIDIA row, and a Qwen "0.2.6 behaviour" row (Advanced only)
- DeepSeek speed-ups Off now writes DS4_DEFAULTS=026 (the engine's master switch back to the 0.2.6 defaults) plus the six DS4_*=0 for older engines; On removes
  all seven. A file with only the six (an earlier test build) still reads as Off and On removes them (`legacy` entries).
- New switch "Faster answers on 12 GB NVIDIA cards" (the residency-biased routing default): on by default, Off writes STRATA_ROUTE_PRIOR=0. Applies to Qwen
  Flash-Next (no arch) on a CUDA build with a discrete NVIDIA card of 13 GiB or less (not a chip that shares system memory), the engine's own rule.
- New Terminal-only switch "Qwen: 0.2.6 behaviour" (STRATA_DEFAULTS=026, off by default), a row of the Advanced table for every Qwen model, for
  troubleshooting; the app never shows it. DeepSeek has DS4_DEFAULTS instead.
tests/terminal/test_speed_switches.py covers the new defaults, applicability, the seven DeepSeek entries and the legacy six, the Advanced rows and the
comparison with the app's table (the Terminal-only switch is left out of it).
2026-10-26 19:10:55 +01:00
xiezhipeng-gitandLeonardo Murillo 9ea044962a OpenAI logprobs, top_logprobs and allowed_token_ids on /v1/chat/completions (#3200)
/v1/chat/completions on the CUDA and HIP engine accepted logprobs and
returned an empty choices[0].logprobs.content. Now "logprobs": true with
"top_logprobs" 0 to 20 (and the legacy "logprobs": N) returns one entry
per token with its alternatives, streamed or not, and "allowed_token_ids"
returns a distribution renormalised over exactly the ids the caller named.

Engine (src/program/generate.cpp): the GEN line takes logprobs=K and
allowed=a,b,c. After each "T id" the engine writes one "LP <logprob>
[id:logprob ...]" line from the verify window's head logits before
sampling, penalties and temperature. The SYCL engine already wrote LP
lines for logprobs=K; it now reads allowed= as well, with the same line
format. A request without logprobs reaches none of this: both call sites
are behind "req_logprobs >= 0", a local int of the request, so the default
path prints what it printed before and does no extra work per token.
Changes from the PR: the call sites are guarded instead of calling the
function for every token, the function is static, the token index is
checked against the row, and the Chinese comment is in English.

Server (serve/server.py): LP lines are read before the token is yielded
and belong to the request that asked (tagged with its cancel Event), the
entries spell the reply, json_schema replies return no logprobs rather
than a mismatching set, an allowed id past the vocabulary is a 400, and
logprobs on a --parallel server is answered for max_tokens 1 only (a
slot's BT lines carry no LP line). serve/structured.py: _extract_json is
now extract_json (the logprobs alignment uses it); the two uses added by
the #2893 retry follow the rename.

Left out: the PR's bench and result files (none in the diff). #3332 (/v1/completions) and #4056
(logprobs on it) build on this change; this one needs nothing from them.

Tests: serve.test_logprobs and serve.test_logprobs_parallel (30 tests with
the PR's), serve.test_structured, serve.test_responses: 94 tests OK.
Not covered: no model ran here. The PR author reports 28 of 28 on an RTX
5080 with MTP and on a GTX 1080 Ti; the layer-split pipelined decode path
(the first call site) was not run by anyone, and the LP values were not
compared with a CPU softmax of dumped logits.

Co-authored-by: Leonardo Murillo <[email protected]>
2026-10-26 19:08:35 +01:00
Niko1221androdi4eg 2299c8f70a native_mmvq: sm_89 MMVQ-IL tables opt-in with STRATA_MMVQ_IL_89=1 (#3453)
Review of the first port: it registered kIlRows_89 / kIl1Rows_89 for every
compute capability 8.9 card by default. Bit-identity of an interleaved-kernel
choice to native_mmvq is a property of the compiled code, and the release
engine builds its own sm_89 cubin that no card of ours can run; the issue also
saw no end-to-end decode change on a 4090 or a 4080, and the 4070 and 4080
runs agree in 53 of the 75 cells of the 2-4 column table (33 of them non-zero). So the default
change has no measured benefit and an unproven bitwise claim on the card it
changes.

Change: sm_89 answers the shared table, with no one-column row, exactly as
before, unless STRATA_MMVQ_IL_89=1 is set (read once, like the neighbouring
switches). With it set the cells from the two community runs apply. Nothing
else in the lookup changes; sm_86, sm_120 and the others are untouched.

Why it is bitwise: native_mmvq_il_kernel keeps a separate accumulator set per
row (acc[v][i][c]) and sums each row over the same chunk order and the same
warp_sum, so the rows-a-warp count R only changes how many rows a warp walks,
not the order of any row's float additions; the plain-versus-interleaved
equality is what mmvq_il_parity measures (memcmp against native_mmvq for rows
1/2/4, T 1-8, odd row counts). That test passes on sm_86; it has not run on an
Ada card.

Tests: mmvq_il_parity now checks the unset behaviour (sm_89 equals the shared
table, Q3_K and the one-column row answer 0) or, with STRATA_MMVQ_IL_89=1, the
4070/4080 agreement. A second ctest, mmvq_il_parity_89table, runs the table
check with the switch set and no kernel case. On z440 (2x RTX A4000, sm_86,
CUDA 13.0, gpu lock): mmvq_il_parity and mmvq_il_parity_89table both pass, and
the binary prints "mmvq_il_parity: OK (every case, T 1-8, rows 1/2/4 and the
table, bitwise)" with the switch unset and set.

Model gate owed on an sm_89 card: phaseA-tests/gh027/gates/3453-sm89-il.sh
(mmvq_il_parity on the card, then identity of the answers with the switch on,
then a speed A/B that decides whether the default flips).

Co-authored-by: rodi4eg <[email protected]>
2026-10-26 19:07:23 +01:00
SuperDistance a3bb38ec45 batch-groups: opt-in adaptive-tier rounds in the pipelined loop, STRATA_PIPE_ADAPT_EVERY (#4010)
With --batch-groups above 1 on a layer split the pipelined batch loop never starts a round of the adaptive VRAM tier (the
serial batch windows start one every --adapt-every windows). While the slots are busy the tier only moves when they go back
to the solo path, so a fresh engine keeps its start-up profile for minutes.

STRATA_PIPE_ADAPT_EVERY=N (opt-in; unset, 0 or not a number is off): after N group windows pump starts no new group on the
first stage, lets the groups in flight finish, then runs one round as batch_step does between serial windows, adapt() and
apply_pending(true), so nothing in flight reads a cache slot while experts swap and the new experts have landed before the
next launch. Not while a prompt is read beside the slots (in_prompt_read, its loan of cache slots), not with --adapt-async,
not while the last round's copies are in flight, not with the tier off. The rule is two pure functions in
include/strata/program/batch_groups.hpp (pipe_adapt_every, pipe_adapt_due) with 12 new cases in batch_groups_test. With the
switch set the engine says so once at start and the "strata batch (pipelined ...)" line adds the rounds, the experts
swapped in and the time spent. docs/BATCHING.md documents the switch with the author's table (2x RTX 5090, six streams,
309 to 410 rows/s over eight bursts after a fresh start, two engines per setting).

Reviewed: unset costs one counter increment per group window and one false check where a group starts, and prints no new
line. A due round cannot wait forever: no new group starts, the groups in flight finish, then the round runs. A prompt read
sets in_prompt_read before the reader thread starts (including STRATA_PIPED_CONCURRENT) and clears it after the reader
joined; the drain after the read runs the round if it is due.

Tests: batch_groups_test (the new cases). Not covered and still the author's to show (the PR is a draft): a soak with
prompts read beside the slots, a Linux run, another GPU pair, more than two stages, and exactness settings with the switch
set. The model gate is written down.
2026-10-26 19:03:48 +01:00
Niko1221 557aad0265 hip-<package> is accepted wherever the backend is named (#4013)
STRATA_ENGINE_BACKEND=hip-rdna2 worked for strata update (updater.detect_hardware), but strata update --backend hip-rdna2 offered no package and
strata launch stopped with "STRATA_ENGINE_BACKEND='hip-rdna2' is not one of cpu, cuda, hip, metal, sycl". engine_component.split_backend reads
hip-<variant> as the hip family and its package; detect_backend (the start path), detect_hardware and update --backend all use it. Any other name is
checked as before. Tests in tests/terminal/test_gfx_selection.py.
2026-10-26 19:03:30 +01:00
SanghunYun 2b13a44933 expert source: STRATA_RAM_ADAPT copies the incoming expert out of its stage buffer when it is still there, and moves up to 4 pairs a layer (#3060)
STRATA_RAM_ADAPT (opt-in, --serve, one GPU, needs --adapt-every) lets the resident RAM copy follow the conversation:
experts the windows keep routing to take the RAM places of unused experts of their layer. Each move re-read the incoming
expert from the file and a layer moved one pair per window, although the incoming expert was usually read for the window
before and its blob still sits in that read's stage buffer.

What changed:
- FileExpertSource::replace_resident_staged(layer, in, out): the same move as replace_resident, with the bytes copied out
  of the finished stage buffer of `in` (no file read). The copy runs under stage_mu_, so claim_stage cannot give the buffer
  to another blob meanwhile; it needs a non-busy buffer whose key is `in`, and returns false (nothing changed) otherwise,
  and the pair then goes the old way. The Foresight hold/unhold around the copy is the one install_resident uses.
- generate.cpp: a layer pairs up to 4 of its most routed outside experts with up to 4 of its least routed ones in the copy
  (outgoing count below 0.5, incoming 2 or more), largest difference first. A pair whose incoming expert is staged moves by
  copy; `n` now caps only the pairs that need a file read. The log line counts the moves and how many came out of a stage
  buffer (ram_staged()).
- docs/DETAILS.md: the RAM_ADAPT bullet in the low-RAM list in plain words and one row of the measured table (the author's
  numbers: RTX 3060, IQ2_XS and IQ3_XXS, 120 ShareGPT requests, 3 interleaved runs each).

Unset (the default) nothing runs: the block is guarded by ram_adapt_n > 0 as before. The weights moved are byte copies of the
same blob; which experts sit in RAM changes, as it already does with RAM_ADAPT, so greedy token ids may differ from run to
run once it is on. Changed from the PR: the DETAILS.md hunk was rewritten against the current, shorter bullet.

Tests: file_expert_source_test (new staged cases; the positive case needs unbuffered load and is skipped without it).
Not covered: no model run. Model gate written down: RTX 3060 ShareGPT replay =8 against =1.
2026-10-26 19:02:48 +01:00
Niko1221 a3b7d9de3f docs/AMD_HIP.md: a hand-built engine has no hipBLASLt tuning table unless it is pointed at one (#4034)
The Build section said nothing about it. gemm.cu (create_hipblaslt_state, own_tuning_dir) takes STRATA_HIPBLASLT_TUNING, else the file
<engine folder>/tuning/<arch>-hipblaslt-<version>.txt, else plain hipBLAS. The report measured 826 against 1,043 tok/s at 8K prompts on a
gfx1151 (one run per arm, stated as such) and the log line that tells whether a table was taken. The extra log line the issue suggests for the
no-table case is a small engine change and is not part of this commit.
2026-10-26 19:01:50 +01:00
Niko1221 c966d7ad89 setup: Maxwell gets a warning with the build recipe, not an env switch (#3218)
The first #3218 commit made setup.py admit a Maxwell (sm_52) card only
when a tester set STRATA_EXPERIMENTAL_SM52=1, and then routed it to the
CUDA 12 engine. That engine holds no sm_52 code, and users never set
environment variables to get a fix.

sm60_card() is back to Pascal and Volta (6.0 to 7.0). A PC whose only
NVIDIA cards are Maxwell now gets a warning (maxwell_note) that says no
ready-made engine holds code for it, gives the source-build recipe
(-DSTRATA_EXPERIMENTAL_SM60=ON -DCMAKE_CUDA_ARCHITECTURES=52, CUDA 12.x)
and the one reporter's result, and points to docs/OLDER_GPUS.md. The card
table line says Maxwell runs only from a source build. A Maxwell card
beside a newer card prints nothing new. Naming the card (--gpu, --cuda
12) or any env var does not admit it. The CMake floor from the first
commit is unchanged (opt-in build only).

Tests: tools.test_setup_older_gpus 23 tests OK (a Maxwell-only PC through
setup.main() with and without the old switches installs nothing and
prints the recipe; a Maxwell card beside an RTX prints no note).
Not covered: no engine was built for sm_52 by this commit; the P1004
compile is recorded in the notes.
2026-10-26 19:01:27 +01:00
JoShMiQueL c0845b7fc5 bench: tools/strata_bench.py reads the CUDA UMD version, the storage, the engine's build toolkit and GPU power and PCIe detail (#3250, #2111)
The report written by BENCH.bat and bench.sh had fields missing or wrong: nvidia-smi's 'CUDA UMD Version:' (current drivers) was not read, the value
was the newest CUDA the driver supports but was labelled like the engine's toolkit, storage always said 'not measured', and Windows 11 showed as
Windows 10. Now the report says 'CUDA (driver supports up to): X; engine built for CUDA Y' (BUILD.json beside the engine), names the storage device
(Get-PhysicalDisk, system_profiler, lsblk -e 7 to skip snap loop devices), adds a 'GPU detail (after the run, near idle)' line with power limit and draw,
PCIe generation and width, pstate and temperature, and recognises Windows 11. The standard report (strata bench run, strata_cli/bench_facts.py) already
read the UMD version, storage and power limit; this brings the legacy tool to the same. Ported from PR #3250 by JoShMiQueL with its tests.
2026-10-26 19:01:17 +01:00
Norm 7783b1c18e docs: a crashed engine start names itself, and a setup run again keeps the installed size (#3243)
Two 0.1.44.1 changes no page told. TROUBLESHOOTING.md gets the start error 'the engine process ended: ...' (serve/server.py start_exit_note: the signal or
Windows exit code, and for an illegal instruction the AVX, AVX2, FMA and F16C flags the CPU lacks). SOURCE_INSTALL.md says a setup run again
pre-selects the installed size, marked '(installed)', and --yes takes it (setup.py size_tag, installed_size). Written by hand from PR #3243 by
Buddychek, whose hunks no longer fit the rewritten pages; each sentence checked against the two functions.
2026-10-26 19:00:14 +01:00
Niko1221 f217b23bd3 prefill: the copy-wait dedupe skips a wait only when both slots were released in the current walk and the ring is longer than one group (#4005)
Review fix on the #4005 port. STRATA_PREFILL_COPY_WAIT_DEDUPE=1 skipped the copy stream's wait on a slot's release
event when the previous entry had waited on the same event. Two cases break that:
- At idx == ring the previous entry (ring - 1) is still in the first round, so its wait was on the event of an earlier
  walk. The same slot id can be this walk's first flush, which re-records the event after that wait; skipping the wait for
  slot 0 then lets the next copy overwrite a slot that the flush's gather has not finished reading.
- With a ring of one MMQ group (16, the smallest ring) a flush of the whole ring re-records the event right after the
  previous slot's wait.
The rule is now copy_wait_dedupe_ok() in include/strata/prefill/copy_events.hpp: idx > ring, ring > group, same event.

copy_events_test models two to three walks with random group sizes and random issuer/consumer interleaving and checks that
a refill always waits on a record at or after the flush that released its slot. The same model with the PR's original rule
(idx >= ring) finds the stale-event case, so the test can fail. The default path is unchanged (dedupe off).

Tests: copy_events_test passes (z440, g++ -std=c++20). Not covered: a GPU run.
2026-10-26 18:59:50 +01:00
Niko1221 cea3ed40db serve: a slot's shorter checkpoints come along at a give-back only when they are prefixes of the tokens copied back (#2526)
Review fix on the #2526 port. At an admission from a slot the port copies every checkpoint of the slot that is not
longer than the copied-back length into the main session's chain. Such a checkpoint is valid there only when its tokens
are a prefix of what was copied back (its K/V cells are then the ones the main session holds) and it has no pictures
(the main session's pictures are cleared at a give-back). The PR did not check either; a checkpoint left over from a
branch the slot no longer holds would sit in the main chain with cells that are not its own.

Both trees (CUDA/HIP and SYCL) now test the prefix and the empty picture list per checkpoint. The checkpoint the
request continues from is copied as before. Nothing changes when every checkpoint is a prefix, which is the normal case.

Tests: compiles on CUDA and SYCL (see the build lines of the branch). Not covered: no model run; the gate is written down.
2026-10-26 18:59:50 +01:00
Yuu 05c1dabf1a docs: the xe expert tier has no SSD fallback under the wrapper default, and a native pack needs --spec 2 or more (#2153, #1796)
docs/INTEL.md and docs/INTEL_ARC.md described an SSD expert tier that strata-sycl.sh's default (STRATA_VERIFY_NO_HOST=1) does not have: every
expert must be in VRAM or in the pinned RAM mirror, and the engine refuses to start otherwise (sycl/src/program/generate.cpp, 'REFUSED: ... are
neither in VRAM nor mirrored'). A native (IQ) pack refuses --spec 0 and 1 (sycl/src/program/generate.cpp and src/program/generate.cpp). Ported by hand
from PR #2153 by nekomario28 onto the rewritten pages, each sentence checked against those two messages. The hunk for docs/TROUBLESHOOTING.md is left
out: the entry it changed is no longer on that page.
2026-10-26 18:59:27 +01:00
oscarlius e21a1872f7 docs/BATCHING.md: the cache settings that let a long solo request move into a batch slot without reading its prompt again (#3334)
Adds a minimal handoff configuration (--prompt-cache 1, --prompt-cache-every 0, --prompt-cache-root 0, --conversation-cache-mib 0), what each
option does, that it costs RAM, that it does not make cold prompts prefill in parallel, and how to look for the handoff in the engine log, /metrics
and /v1/status. Ported from PR #3334 by oscarlius (continuing #3035). The four options exist in src/program/generate.cpp and the o.prompt_cache > 0
gate matches the text; the log evidence was not reproduced here (no model run).
2026-10-26 18:57:53 +01:00
stuchapin909 6c09a1929c CI: build the Metal engine on macos-26 (#3870, #3824)
metal/src/mtl_rt.mm names MTLGPUFamilyMetal4 (macOS 26 SDK) and MTLCompileOptions.mathMode (macOS 15 SDK) behind @available, a run-time check;
the names must still exist in the SDK the engine is compiled against, so the metal-link job failed on the macOS 14 SDK on every pull request that
touched src/, include/ or metal/. The job now runs on macos-26 (the runner-images repository lists a macos-26 image). Ported from PR #3870
by stuchapin909. A GitHub Actions run cannot be made here: the author ran this workflow on his fork; the first run after the merge proves it.
#3824 changes the same line with macos-15 and an Xcode 26.0.1 path pin, and is not taken.
2026-10-26 18:57:30 +01:00
Heinz N. Gies 5bc1e24696 kq_cpu_bench: i-quant pairs no longer crash, and Q2_0 is timed on the CPU pool's path (#3871)
make_blob quantized its random test rows with ggml-cpu's type traits from_float, which the i-quants do not have: an IQ3_S pair called a null
pointer (an access violation on Windows, SIGSEGV on Linux) before the first timing. It now uses ggml_quantize_chunk, with a flat importance
matrix where the type needs one. A Q2_0 gate/up or down is timed as pool.cpp runs it (activations as an ActQ, Strata's own Q2_0 rows, SwiGLU
in between) instead of with ggml-cpu's Q2_0 dot, and the pair's header line says so. Other formats are timed as before. Ported from PR #3871 by
Licenser.

Built on this PC (MSVC, CPU only) and run: iq3_s/q2_0 0.531, iq2_xs/q2_0 0.336 and q2_0/q2_0 0.214 ms per expert per thread (--mb 512, 1 thread).
2026-10-26 18:57:13 +01:00
Highlander 0fbbf068eb nvfp4 local pack: read the checkpoint with cached sequential reads, not a process-lifetime np.memmap (#3377, #3378)
Checkpoint.raw mapped every shard for the life of the process. On Windows each faulted page of the ~64 GB of expert bytes is charged to the
process working set, so the pack step looked like a 64 GB RAM demand and was killed by free-RAM watchers on a PC with a model loaded (nothing
leaked: it all came back at once). raw() now reads the tensor's byte range into a fresh buffer through one cached file handle per shard; the
pages go to the system file cache, which counts as available. The author measured the same bytes, 1.2x faster, flat working set, and a
byte-identical pack. raw() is no longer thread-safe (this file uses no threads). Ported from PR #3378 by jerry78424 (the fix for his issue #3377)
with its two tests; I ran the tests (13 pass); the full 102 GB pack was not run.
2026-10-26 18:56:54 +01:00
Keyan Hu 409f2466ad tools/gguf_reader.py: bound the header reads and report malformed metadata as ValueError (#2288, #1919)
A GGUF header with an unknown metadata type or an impossible length raised KeyError or OverflowError inside the reader, and setup, which expects
a ValueError, printed a traceback instead of its diagnostic naming the broken shard. Strings, scalars, arrays and tensor dimensions now share
one bounded read; counts are checked against the bytes left in the file before anything is constructed; type ids are checked before their values
are decoded; nested arrays are reported as unsupported; general.alignment must be a positive multiple of 8. The file name is added once to the
message. Ported from PR #2288 by KeyanHu-git with its tests (tools/test_gguf_reader_malformed.py).

Checked against the old reader on the 18 GGUF v3 vocabulary files of the pinned llama.cpp: same metadata, tensors and data start.
2026-10-26 18:55:57 +01:00
Jakub Luwierski 11237f831f serve: Chat Completions tool results keep the order of the calls they answer (#3395)
When one assistant message made several tool calls and the client returned the results in another order, the call ids were dropped before the
prompt was rendered, so the model saw anonymous results in the wrong order. The ids are now kept while the request is normalized; a run of
results is reordered to the call order only when the call ids and result ids are non-empty unique strings with matching sets. Anything else
(missing, duplicate, unknown or incomplete ids) keeps the received order, and the ids never reach the template. Ported from PR #3395 by
j-luwierski with its seven tests (serve/test_chat_tool_results.py); the hunk was merged by hand beside require_messages.
2026-10-26 18:54:34 +01:00
rodi4egandkzhv bdecbffc6b native_mmvq: sm_89 (Ada) MMVQ-IL table from the cells two cards agree on (#3453)
Problem: an Ada card took the shared MMVQ-IL table, which has no Q3_K row, so
Q3_K shapes ran the plain kernel and the other types used a table measured on
sm_86 and sm_120.

Change: kIlRows_89 (2-4 columns) and kIl1Rows_89 (one column) are registered
for compute capability 8.9. The 2-4 column table keeps only the cells where the
RTX 4070 run (rodi4eg) and the RTX 4080 run (kzhv) gave the same rows count and
sets the rest to 0, the plain kernel; the 4070 and 4080 disagree in 13 of 15
cells of the unmerged tables. The one-column row is Q3_K only, which both cards
gave. The issue's 5-8 column table is not taken: the 4080's changed in 10-14 of
20 entries between two runs of the same card, so there is no kIlRows58_89. The
bench output files of the PR are not ported.

Every choice in a table is bitwise native_mmvq's output (kernel row split only),
so answers do not change; this is speed only. No end-to-end decode change was
seen on a 4090 or a 4080 in the issue, so the gain is the microbenchmark's.
This does change which kernel an sm_89 card runs by default, so the model gate
(identity on sm_89) is written down in phaseA-tests/gh027/gates/3453-sm89-il.sh.

Tests: mmvq_il_parity (registered in CMakeLists.txt) now checks sm_89's table
against the two source runs (check_tables) instead of expecting the shared
table, and still checks bitwise parity of every case and rows choice on the GPU
it runs on (sm_86 on z440). docs/MMVQ_IL_TABLE.md updated.

Not covered: no sm_89 card here, so the table speed is the contributors'
numbers, not ours.

Co-authored-by: kzhv <[email protected]>
2026-10-26 18:53:58 +01:00
i-Vladimir-i 092c7d1ef4 serve: return 503 when the elastic image encoder cannot start (#2435)
When the elastic image encoder failed to restart, its exception left request preparation and the client got a closed connection. It now goes
through the EngineDied path: HTTP 503 with the reason, a later picture tries again, text requests and a later good start are unaffected. An idle
encoder stop is logged even when the cache shrink was refused, and a layer split gets a line saying its encoder VRAM has to be kept free with
--vram-reserve-mib. Ported from PR #2435 by i-Vladimir-i with its tests (chat, messages and responses, streamed and not, plus a real encoder
that exits with code 71 on Linux) and the docs/VRAM_ELASTIC.md text.
2026-10-26 18:51:44 +01:00
Niko1221 a8c1d4b7f8 verify: a window graph that does not fit frees the graphs of other window sizes instead of ending the engine (#3702)
On a layer split the later card is left with a few dozen MiB after loading, and each single-request window size is
captured the first time it is used (68, 64, 58, 52 and then 10 MiB free in the report); when one capture's
cudaGraphInstantiate ran out of memory, instantiate_evicting could only free graphs of batch slot layouts, a
single-request run has none, and "verify: instantiate: out of memory" ended the engine.

Now, after the batch layouts, a single-request window's capture frees the graph of the largest other window size (never the
one being captured or a prelaunched one; pick_window_victim) and instantiates again, and says so once: the freed size is
captured again when it is next used. capture_all keeps every size at once and is excluded. Only a failed instantiate does
anything new; with enough VRAM nothing changes.

window_evict_test (CPU, header-only) covers the choice. The real gate needs a model on a 2-GPU split with a thin later card
(listed in the triage file); not changed here: the later-stage reserve of the split search (kWindowMib), which is a default
placement change.
2026-10-26 18:51:29 +01:00
Vop3p a11f8b0aa5 prefill: opt-in sparse copied events and deduplicated slot waits in the streamed prompt walk (#4005)
Under WDDM every cudaEventRecord and cross-stream wait is a submission of its own (the author measured ~80 / ~45 us each
on an RX 6900 XT under Windows 11), and the streamed walk paid one record and one wait per expert although a group
gathers after one wait on its last copy. Two tester switches, threaded issuer only:
- STRATA_PREFILL_COPY_EVENT_EVERY=N (2..64): the copied event is recorded every N-th entry, before the issuer waits and on
  the last; the compute stream waits on the first recorded entry at or after the one it needs (the copy stream is in order).
- STRATA_PREFILL_COPY_WAIT_DEDUPE=1: the copy stream skips its wait on a slot's release event when the previous slot was
  released by the same event.
The same copies into the same slots: the same bytes.

Default path (both unset): read line by line. The issuer records copied[slot] and waits on used[used_of[slot]] exactly as
before (the counters are bookkeeping, cover() is a no-op). Added to the PR so this holds by construction: wait_copied takes
the slot the old code used and, without the sparse switch, waits on copied[slot] at all three consumer sites (the PR derived
the slot from the entry number). The record/wait selection is now three pure functions in
include/strata/prefill/copy_events.hpp, with a host test (copy_events_test, registered in CMakeLists.txt) that models the
issuer and consumer with a full ring and checks that every wait names a live recorded entry at or after the one needed.

Tests: copy_events_test passes (z440, g++ -std=c++20). Not yet: a CUDA build of prefill.cpp in this tree (queued), no GPU
run. Not covered: the Windows HIP / WDDM speed claim and NVIDIA (the gate is written down). Needs the author's numbers
repeated before any default talk.
2026-10-26 18:50:44 +01:00
PurrSec eba3106c0e serve: support IPv6 bind addresses (#3212)
--host :: failed before the model loaded because Server always opened an IPv4 socket. Server now selects AF_INET6 when the bind address contains a
colon; the ready line, the 'from other devices' lines and the browser address bracket an IPv6 address. The bind is still the person's choice and
no default changes. Ported from PR #3212 by purrsec with its tests (serve/test_security.py, including a real :: bind). setup.py's --host help and
docs/DETAILS.md say :: is all IPv6 interfaces; the README hunks of the PR (they name strata launch --host, not checked) are left out.
2026-10-26 18:50:11 +01:00
Niko1221 61ede87e1b Maxwell sm_52 floor for the opt-in old-card build (#3218)
A reporter built Strata for two Quadro M6000 (sm_52) by lowering two
floors and saw about 22 tokens a second with Qwen3.8-Flash-Next IQ1.
CMakeLists.txt: the experimental build (-DSTRATA_EXPERIMENTAL_SM60=ON)
now accepts sm_52 and prints a warning for it (Maxwell, one reporter, no
native half precision, CUDA 12.x). Without the flag the build still
stops at sm_75 as before, and the architectures a build uses are not
changed.

setup.py: sm60_card() admits a 5.2 card only when the tester sets
STRATA_EXPERIMENTAL_SM52=1, because the ready-made CUDA 12 engine holds
no sm_52 code and a Maxwell card must not be routed to it by default.
With the variable set, setup warns about the Maxwell card instead of
refusing. Default: a Maxwell card is "not supported" exactly as before.

docs/OLDER_GPUS.md: a Maxwell row and a short section, with the build
command and a request for an answer-text check.

Tests: tools/test_setup_older_gpus.py (20 tests, new Maxwell cases for
default, opt-in and a newer card present). Not covered: no model run, and
the answer text on a Maxwell card is unchecked.
2026-10-26 18:48:59 +01:00
Keyan Hu a1d2fd7ed1 setup: use the published size when the download length probes fail (#2287)
When HEAD and the one-byte range probe both gave no length, download() took an early EOF as a finished file even though the repository pin (or
ModelScope's published size) names the size. The published size now stands in for the length, so the existing resume, retry and parallel code
publishes the file only after it has all its bytes; a size that disagrees with the pin is still refused before the transfer and the SHA-256
check is unchanged. Ported from PR #2287 by KeyanHu-git with its tests (tools/test_setup_download_length.py).
2026-10-26 18:48:49 +01:00
Günter 560cc9fbbc setup: --gguf-dir files for Qwen3.6 no longer need a .done mark made by setup (#3606)
install_own_path treated a file as present only when <file>.done existed, and that mark is written by setup's own download or by verify_sha256,
which a file from another tool (LM Studio) never reached: setup said it was missing and asked for the full download size in free disk. With
--gguf-dir an existing file and its mmproj go to verify_sha256(own=True), which checks size and SHA-256, leaves a mismatching file alone and
writes the mark. A file that is not in the folder is still reported missing. Ported from PR #3606 by ai-bits with its two tests.
2026-10-26 18:48:14 +01:00
Niko1221 96b6068c32 Fix the follow-up of #4014 (a variable name already used in Chat.tsx) and two of my tests (#3254 iterator, #3656 raw paths) 2026-10-26 18:48:09 +01:00
Xiaomi2008 3100ca05ed expert cache: STRATA_VRAM_FREE=device reads the whole card's free VRAM through NVML, Windows + NVIDIA, opt-in (#1911)
Under WDDM cudaMemGetInfo answers for the calling process: memory another process holds on the card (the image encoder,
a browser, another engine) is not in its free figure. The reporter measured, on an RTX 5090, 30,994 MiB free from CUDA
where NVML read 24,348 with 6 GiB held elsewhere, so --expert-cache auto sized the cache too large and shrank it by
guesses.

device_free_bytes() now takes min(CUDA's figure, NVML's figure for the whole card) when STRATA_VRAM_FREE=device is set.
nvml.dll is loaded from the system directory at first use (LoadLibraryExW with LOAD_LIBRARY_SEARCH_SYSTEM32), the card
is matched by its PCI bus id, and a missing nvml.dll, a missing export or a failed nvmlInit leaves CUDA's figure with
one line on stderr. The serve check of "VRAM free with everything loaded" uses the same function. The value can only get
smaller, so the switch can only shrink the expert cache.

Unset (the default), nothing changes: the NVML code is compiled only for _WIN32 and not for HIP, Metal or the no-VMM
build, and the env variable is read once. The serve check's min() is the same value elsewhere (on a unified-memory
system device_free_bytes() is never below cudaMemGetInfo's figure). Added to the PR: the NVML block is also left out
of a SYCL build (SYCL_LANGUAGE_VERSION), whose CUDA names are a compatibility layer. docs/DETAILS.md: one row in the
measured-and-left-off table instead of the PR's long paragraph.

Tests: builds on Linux CUDA (the Windows-only block is skipped), SYCL (P1006) and on Windows MSVC (owner PC build of
the final tree). Not covered: no run with a model; the Windows gate is written down.
2026-10-26 18:47:52 +01:00
Jakub Luwierski a332cc95db setup: an explicit --rope-scale must be finite and at least 1 (#3393)
resolve_rope accepted nan, inf, zero and values below 1 and handed them to the engine. It now stops with a message that names --rope-scale and the
valid range, before any configuration is written; valid factors and the automatic scaling are unchanged. Ported from PR #3393 by j-luwierski
(follow-up to #2950) with its tests; the --help text gets the range.
2026-10-26 18:47:46 +01:00
Avicennasis 3a3f35c64a tests: cuda_spill_guard fails the CUDA test build when a Strata kernel spills or keeps a per-thread array in local memory (#2683, #2682)
tools/check_cuda_spills.py reads the resource usage the compiler wrote into the kernel libraries (cuobjdump
--dump-resource-usage: STACK and LOCAL per entry function and per sm_) and fails on any Strata kernel outside its ALLOWED
table, which carries the measured allowance of the kernels that hold such an array on purpose; ggml's vendored kernels
(no namespace) are skipped and counted. ctest cuda_spill_guard runs it on eight libraries, skips (77) without cuobjdump or
a demangler, CUDA builds only (HIP has no cuobjdump); check_cuda_spills_unit runs tools/test_check_cuda_spills.py
(stand-in demanglers are sh scripts, so not on Windows).

Changes against the PR: (1) a demangler that exits 0 but prints a blank name used to make the kernel's key empty, which
reads as vendored, so a spilling Strata kernel was skipped; that is now "no verdict" (77) with a test. (2) The unit test is
registered. (3) The ALLOWED table was re-measured on f-027-int: sm_120 with CUDA 13.0 on Windows (MSVC) and sm_86 with the
CUDA 13.0 objects built on Linux (cuobjdump 12.9); allowances are only raised (new DFlash2 gemm8_kernel and
rope_table_kernel, native_gu_swiglu_kernel on sm_86, dequant_kernel / expdec_gu_kernel / prompt_attn_i8v3_kernel /
native_gu_swiglu_kernel on sm_120), none lowered or dropped. Result: 0 violations on both architectures; a test shows the
same object fails once one allowance is lowered.

Ported from PR #2683 (the tracking issue is #2682).
2026-10-26 18:47:35 +01:00
Niko1221 2cc6ea169b serve: opt-in LaTeX-safe JSON recovery for structured output (#2893)
A json_object or json_schema answer that holds LaTeX such as \alpha is
rejected (502) because \a, \g and the like are not JSON escapes. New
opt-in: "repair_latex_escapes": true in response_format (and in
text.format on /v1/responses). Only after the strict parse failed, the
server doubles each backslash that does not start a valid JSON escape and
parses once more with the same duplicate-key and NaN rules, then checks
the schema as before, and prints one log line. Without the option, and
for any answer that parses on the first try, nothing changes.

Escapes that are valid JSON (\b \f \n \r \t \/ \" \ and a well-formed
\uXXXX) are left alone, because they cannot be told from LaTeX: a \beta
next to a \alpha comes back with a backspace. Truncated output, arrays,
NaN and duplicate keys still fail.

Tests: three new tests in serve/test_structured.py (default still 502,
opt-in 200 and stream, failures stay failures, helper cases, Responses
format passthrough). serve/test_structured.py 13 tests OK.
2026-10-26 18:47:25 +01:00
SanghunYun 2251053b42 serve: the serial decode timing line shows the adaptive waits in its sum (#1864)
With STRATA_DECODE_TIMING=1 the serial line listed `verify + commit/emit + draft` after `ms/window =`, while the adaptive
wait before verify and the join after the draft were timed but printed after the sum, so the named parts did not add up
to ms/window. The serial line now reads `adapt wait + verify + commit/emit + draft + adapt join + other`, where `other` is
the decode time minus the named parts (not a timed step). The pipelined decode keeps its old line, byte for byte, and
nothing is printed with the timing off or with no windows. Diagnostic only: no decode path changes.

docs/DETAILS.md: a short paragraph on the new line (the hunk was rebased by hand onto the current file).

Tests: compiles on CUDA (the format string was counted against its 22 arguments); no parser in the repo reads
`ms/window = verify`. Not covered: no run of the line itself (it needs a decode).
2026-10-26 18:46:55 +01:00
l33tm4st3r c471588f3e bench: tools/strata_bench.py (BENCH.bat, bench.sh) names an AMD card on ROCm 7+ too (#3630)
The quick speed check has its own GPU probe, separate from strata_cli/bench_facts.py, and tried nvidia-smi then rocm-smi only. On a Radeon with
amd-smi alone it wrote the server's vendor stub and 'GPU driver: not read'. The same amd-smi probe as #3519 is added in the shape this file
uses, and a bare vendor name from the server no longer replaces a card the probe named. Ported from PR #3630 by l33tm4st3r.

The three new tests are POSIX-only (symlinks, /sys); run on z440 (Linux, Python 3.12): OK. tools/test_strata_bench.py on Windows: OK.
2026-10-26 18:46:33 +01:00
hotschmoe 01e404c140 serve: an idle batch slot's decoded conversation is parked before an admission overwrites it (#2526)
A batch slot holds the reply it decoded. The main session was parked with the conversation's prompt only (the
admission's copy), so when the next admission or a yielding read overwrote the slot, the reply lived nowhere: the
conversation's next turn found its prompt-read state and read the whole reply again (the author measured 255 of a
256-token reply re-read on four 200K chats over two slots).

What changed:
- park_current is split into park_source (one budget / RAM-admission / snapshot path) and two callers: park_current
  (the main session, output line and byte accounting as before) and the new park_slot (an idle slot's ids, checkpoints,
  sessions and draft K/V). The ParkSource passes what a park captures; the main session's retained K/V (#1503
  take_reuse_for) is taken only for the main session, never for a slot.
- park_slot runs before copy_to_slot at a slot admission and before a BYIELD read overwrites a slot. It does nothing
  when the live main session or a parked conversation already holds that state (ConversationCache::holds /
  conversation_covers, new, with unit tests), for a slot with pictures, or without --batch-mtp when the drafter is on.
- When a slot is admitted into the main session its checkpoints come along (the shorter ones too), so the chain keeps its
  branch points and the parked copies a turn back are superseded (#342) instead of taking an entry each.
- Same refactor in sycl/src/program/generate.cpp.

Changed from the PR: parking is an optimization like a checkpoint (#3350). The PR ended the request (ERR) or the batch
(batch_fatal) when park_slot's snapshot failed; here a failed snapshot, a failed device sync and a host allocation
that runs out are logged, the slot is overwritten as before and the request goes on. The slot's extra checkpoints that
come along at an admission are copied under a bad_alloc guard (the one the request continues from is copied as before).

Tests: conversation_cache_test (new holds() cases and the four-conversation replay at policy level) and
conversation_split_failure_test. Not covered: no model run (the batch path itself needs one); the gate is written down.
2026-10-26 18:46:05 +01:00
Niko1221 9c2d35d978 serve: /v1/responses accepts max_output_tokens of 0 and -1 (#4036)
The Responses validator still refused anything below 1 after the chat
path made max_tokens of -1 and 0 mean no cap (documented in
docs/DETAILS.md). It now accepts -1 and 0 as no cap; values below -1,
floats, booleans and strings stay a 400. Nothing else changes: the
server already passes max_output_tokens or 0 to prepare(), which gives
the answer the room left in the context.

Tests: OverHttp.test_max_output_tokens_zero_and_minus_one_mean_no_cap
(stream and non-stream, status completed) and
test_max_output_tokens_bad_values_are_400. The first fails without the
change. serve/test_responses.py 43 tests OK.
2026-10-26 18:45:04 +01:00
Niko1221 407f659763 docs: GR_V3 and PROMPT_ATTN_IMMA defaults in DETAILS.md and the 0.2.7 release-notes draft 2026-10-26 18:45:02 +01:00
ruibeikaa f213f78bae mtp: the drafter's Q8_0 draft head and projections on the Pascal GEMV, opt-in STRATA_MTP_Q8_SM60=1 (#3751)
Problem: with STRATA_Q8_SM60=1 the main model's Q8_0 matrices run on the GP100
Q8_0 kernel, but the MTP drafter's own Q8_0 draft head and projections kept the
generic kernels.

Change: with STRATA_MTP_Q8_SM60=1 (on top of STRATA_Q8_SM60=1) the drafter packs
its Q8_0 projections as packed copies, as the main model does (the prompt path's
draft_kv still reads the GGUF layout, so a copy is kept), and packs the draft
head's subset in place when every column count fits the kernel's shared-memory
planes and the data is 16-byte aligned (otherwise a packed copy). The in-place
pack writes the GGUF blocks back if the device write fails. Release unregisters
the entry without a cudaFree when the data was packed in place. New
native_q8_0_sm60_pack_quiet and native_q8_0_sm60_pack_in_place; HIP returns
false; metal/src/engine/stubs_0145.cpp gets matching stubs that answer
"nothing packed".

Replies do not depend on the drafts (they are verified); only the draft rounding
changes, because the Pascal kernel sums in another order.

Default path: mtp_q8_sm60() is false unless the env is set, so the packing calls
are skipped. The destructor now walks the tensor list once at teardown, not in a
hot loop. The SYCL tree has its own sycl/src/core/mtp.cpp, so it is not touched.

Tests: compile-only on sm_60 (CUDA 12.9) on P1004. Model gate (identical replies
with the flag on and off, decode speed on a P100) is written down in
phaseA-tests/gh027/gates/3751-mtp-q8-sm60.sh.

Not covered: no GPU run here; the contributor measured +4.4% on four GP100 dies
with all experts resident, one run per arm, not on a single streamed P100.
2026-10-26 18:43:53 +01:00
Dzmitry Senchankau 41188ec734 calibrate: an engine that dies mid-measurement drops only its candidate (#3317)
#1337 made a failed engine start in a later calibration step a candidate
that loses. An engine that started and then exited during its own rate
measurement still failed the whole run. measure_candidate() now treats
both the same: the candidate is dropped and recorded in
report["failed_starts"], the earlier results stay. It is used for the CPU
worker step, the adaptive expert tier step and the AMD shared-expert
fork. An operator cancel (CalibrationCancelled) is still raised. The first
start (steps 1 to 3) still fails the run.

src/core/verify.cpp: when adapt_gate_wait times out after 20 s, the
message now says which layer was reached (atomic and copy signal), that
the copy stream is not drained by the release, and dumps the
STRATA_VERIFY_TRACE breadcrumbs when that is on. Diagnostics only, #2188
stays open. The watchdog label is set only once a wait actually starts,
so a gate that is already open costs nothing extra (changed from the
PR, which set it on every call).

Tests: tools/test_calibrate.py 33 tests OK (3 new death tests).
Not covered: engine compile of verify.cpp is checked in the build step.
2026-10-26 18:43:46 +01:00
ruibeikaa 6a6d58ad03 iq_kernels: GP100 decode down projection staged in shared memory, opt-in STRATA_DOWN_WS=1 (#3867)
Problem: on a GP100 die (cc 6.0, no dp4a, no tensor cores) the verify window's
routed down projection for the native packs' IQ4_NL and Q2_0 rows (n_ff 640)
is latency bound. native_down_multi_kernel reads each weight as 2-byte loads,
one item at a time, straight from global memory.

Change: native_down_ws_kernel copies a block's rows (IQ4_NL 32, Q2_0 64, 11,520
bytes) into shared memory with 16-byte loads, then runs the same row dots on the
copy: same sub-warp, lane, items, float expressions and order. Only the integer
sum inside an item changes form (vmad byte selectors, prmt table lookups), the
same int8 x int8 products, so every output is native_down_multi_kernel's (expert
mode 0) bit for bit.

Gating: compiled only in the CUDA build with STRATA_EXPERIMENTAL_SM60 (not HIP,
not Metal). At run time only with STRATA_DOWN_WS=1 on a device of compute
capability exactly 6.0, IQ4_NL / Q2_0 down rows at n_ff 640, and not with
STRATA_OLD_IQ_MMVQ or STRATA_NO_SUB16_GU. Unset, the only change on the default
path is one cached boolean test per native_expert_grouped call.

docs/OLDER_GPUS.md lists the switch. The contributor's microbench numbers (one
PH402 die, Swift 1.5 IQ3_S, one-token window: IQ4_NL 72.6 us mode 8 and 54.5 us
mode 0 become 42.3 us; Q2_0 53.5 / 65.4 us become 38.6 us) were measured on a
four-GP100 rig, not here.

Tests: compile-only on sm_60 (CUDA 12.9) on P1004. The contributor's bitwise
check (native_expert_bench hash against modes 0 and 8) is not run here: it needs
a GP100 GPU run and the model's layers. The gate script is
phaseA-tests/gh027/gates/3867-down-ws.sh.

Not covered: no end-to-end measurement on a single P100 (16 GB cannot hold the
47 GB pack resident).
2026-10-26 18:43:32 +01:00
l33tm4st3r 2ac3482144 bench: the standard report names an AMD card and its ROCm runtime when only amd-smi exists (#3519)
ROCm 7 and newer ship amd-smi and no rocm-smi, and the amdgpu product_name is empty on gfx1201, so a Radeon AI PRO R9700 run was filed
under 'unknown' with 'runtime: not measured'. bench_facts reads the market name from amd-smi static --asic, the VRAM from the amdgpu sysfs
file of the PCI address amd-smi list reports, the ROCm version from the install (or amd-smi -h), and keeps the probe's card name when the
server only says 'AMD Radeon'. Ported from PR #3519 by l33tm4st3r with its tests and the BENCHMARK_PROTOCOL.md text.

Changed while porting: the amd-smi branch also took macOS (os.name is posix there), which would have hidden the Apple chip; it now skips
darwin, and a test keeps the chip.
2026-10-26 18:41:53 +01:00
Niko1221 9e3fd0f73a App: the DeepSeek card names the free disk the set needs, and the version line says "{N} GB RAM + VRAM, plus about {disk} GB of disk"
The "This computer" card said only "Fits fully in memory." for DeepSeek, with no word of the ~265 GB of disk. For a DeepSeek version it now says "Needs about {N} GB
of free disk space. What does not fit in memory is read from your SSD." (the spill line stays beside it when part of the model is read from the SSD), and the
selected version's line reads "{N} GB RAM + VRAM, plus about {disk} GB of disk". {disk} is not hard-coded: the catalog (tools/strata_mcp.py) gives each set
its `disk_gb`, the set's whole download (model files, n-gram table, drafter, metadata) plus the pack and room to work (dsv41_fit.DISK_HEADROOM_GB), so 2.00 and
2.25 differ. Same card and row markup, no design change; every other model keeps its card. Both sentences are fx80 and fx81 in the eight fix catalogs.
The Terminal's DeepSeek description makes no "fits fully in memory" claim (its lines already state the download and the free disk), so it is unchanged.
2026-10-26 18:41:17 +01:00
Niko1221 54d9363c97 CUDA Plan opt-ins become defaults where measured: STRATA_GR_V3 on a discrete 12.0 card above 13 GiB, STRATA_PROMPT_ATTN_IMMA on CUDA sm_80+ (env =0 turns each off); DeepSeek Faster mode (5 experts) by default on 2+ RDNA4 cards that hold half the experts; the route-prior/GR_V3/IMMA rules are pure functions with tests; gr_parity and fused_gr_bench pin STRATA_GR_V3=0 for their bitwise contract 2026-10-26 18:41:09 +01:00
Tsury fb966cff37 pack_gguf: accept older DeepSeek GGUF metadata names (#3955)
Older converters write the Engram scalars as engram.key_length and
engram.pad_id, and omit the sparse-attention geometry. The packer now
reads the old names only when the canonical engram.head_dim and
engram.pad_token_id are absent, and a new --config-json option fills the
omitted geometry from the source model's config.json. A value that
disagrees with metadata already in the GGUF stops the pack with a clear
message. The weights are never rewritten and a set that already has the
canonical names packs exactly as before.

Tests: LegacyModelMetadata (2 new, 5 in the file) in
tests/terminal/test_pack_gguf_engram.py.

Not covered: the author reports the older Q2_K set still needs dense
Q2/Q3 and Engram Q2_K tensor support in the engine; this does not add it.
2026-10-26 18:40:30 +01:00
Patrick Schaub a0fc88abd6 serve: timings on /v1/responses (#3291)
The final response.completed / response.incomplete event and a
non-streaming /v1/responses reply now carry the same top-level timings
object that /v1/chat/completions has. The server already put timings in
the done dict; the Responses assembler dropped them.

Tests: UsageAndStatus.test_responses (stream and non-stream).
Unset timings leave the event unchanged; response.failed has none.
2026-10-26 18:40:04 +01:00
Norm 5b14fe23f7 tests: three suite gaps a live PC showed (#3236)
test_iq_pack imports without llama.cpp's gguf-py and skips the classes that build GGUF fixtures with a named reason; the MCP discovery tests use
a free port, not whatever Strata serves on 8080; the six conversation-cache tests add the repo root to sys.path so they run from the repo root
as the suite does. Ported from PR #3236 by Buddychek (tests only, no shipped code).
2026-10-26 18:38:42 +01:00
Niko1221 f06bfc30e7 docs/TROUBLESHOOTING.md: the Windows AMD driver-reset entry is back
The 0.2.5 rewrite of the page dropped the 'Windows AMD: the driver resets' entry (v0.2.4.2 had it), although setup.py still tells a Windows gfx12
card owner to read it and tools/test_setup_amd.py (TdrPointer) checks for it, so that test failed on main. Restored from v0.2.4.2 with the
log and config locations worded for a source checkout.
2026-10-26 18:37:19 +01:00
Norm e6f598c0a2 docs: STRATA_VCVARS, /metrics/prometheus behind the API key, and the Monitor's prompt speed (#3244)
Three 0.1.42 changes no page told. Ported by hand from PR #3244 by Buddychek (the PR conflicted with the rewritten pages), each sentence
checked against the code: /metrics/prometheus needs the API key like /metrics (serve/server.py _authorized), the prompt-speed row is
(prompt_tokens - reused) / prompt_ms (apps/host/server/monitor-dashboard.mjs), STRATA_VCVARS is read by setup.find_vcvars. The compiler
text now says Visual Studio 2022 (2019 is refused since 0.2.5, #3566) and went to docs/SOURCE_INSTALL.md.
2026-10-26 18:36:43 +01:00
Niko1221 3478382eee conversation copies: the private copy streams and events are handed back to a pool when a thread ends (#3350, #3470)
conversation_detail::private_stream() and order_after_queued() kept one non-blocking stream and one event per device in
thread_local storage and never destroyed them. The layer split's later stages run each prompt chunk on a fresh
std::async thread (STRATA_HANDOFF_PIPE off, the default), and a checkpoint part is saved on that thread, so every
periodic checkpoint of every long cold prompt left a stream and an event behind on each later card: a few MiB of VRAM
per long prompt that never came back (#3470: the later card's free VRAM drops 2-6 MiB per long cold prompt, no plateau),
until cudaStreamCreate failed with "out of memory" (#3350: "conversation snapshot running-state copy: out of memory",
engine exit) on a card that was already nearly full.

A thread now hands its handles to a process-wide pool when it ends and the next thread takes them, so the number of
streams and events is the number of threads copying at once. Same stream, same flags, same contract; only the lifetime
changed. copy_streams_created() / copy_events_created() count what the process made.

conversation_copy_streams_test (CUDA, no model, skips without a device): 200 short-lived threads in a row create at most
one stream and one event; ten rounds of six concurrent threads at most six more; the same on a second device.
2026-10-26 18:36:32 +01:00
Niko1221 f62589fdbe serve: a checkpoint whose copy runs out of memory is skipped, it no longer ends the engine (#3350)
A conversation checkpoint is an optimization, like parking, but the writer treated a failed copy as fatal: "conversation
snapshot running-state copy: out of memory" (cudaErrorMemoryAllocation from the snapshot copy: the private copy stream's
first creation or the driver's staging on a nearly full card, not host RAM) made the prefill fail and the engine exit,
21 times in 4 h for one reporter and 16 in 34 h for another, each exit a reload and the in-flight requests lost.

Now, after a failed save, checkpoint_failure_is_survivable() clears the non-sticky allocation error and synchronizes the
device: when that is clean the checkpoint is skipped with a line on stderr and the request goes on (a layer split's stage
part is left empty, which already means "no checkpoint at this length"; the message-boundary / turn / root checkpoints
after a prompt chunk do the same). A device that is unwell (a sticky error, a GPU that stopped answering) still ends the
request exactly as before. Same edits in sycl/src/program/generate.cpp.

STRATA_TEST_COPY_FAIL_AT=N (tests only, like STRATA_TEST_SLOW_COPY_US): the N-th snapshot copy of the process fails with
cudaErrorMemoryAllocation, so the case can be made without a full card. Unset, nothing changes.
Not changed: "verify: instantiate: out of memory" (graph capture), which the reporter saw once.
2026-10-26 18:36:32 +01:00
Niko1221 164619c5a8 DeepSeek: the 4 x R9700 share-based defaults and the drafter cut apply when every device is RDNA4 (gfx1200/gfx1201), not to every HIP build (Impl::rdna4) 2026-10-26 18:36:12 +01:00
pschaub 3cd33d504c cmake: a CUDA build with no architecture option is for sm_120, and native builds the W4A4 unit for sm_120a (#4029, #4023)
CMakeLists.txt tested "NOT DEFINED CMAKE_CUDA_ARCHITECTURES" after enable_language(CUDA). Since CMake 3.18 that call
fills an unset variable with nvcc's default (sm_75 for CUDA 13), so the sm_120 default never applied: a hand build with
no option was an sm_75 build. The default is now set before enable_language, unless the user gave
-DCMAKE_CUDA_ARCHITECTURES or the CUDAARCHS environment variable, and the configure step prints a warning that says what
to pass for another card (a recommendation, not a refusal).

With -DCMAKE_CUDA_ARCHITECTURES=native, the one NVFP4 W4A4 unit (mmq_nvfp4_w4a4.cu) stayed on plain `native`, where
ptxas refuses the FP4 MMA on an RTX 50. `native` is now replaced by CMAKE_CUDA_ARCHITECTURES_NATIVE (the detected
architectures) before the 12x -> 12xa rewrite, as a list, so a mixed PC works too. docs/SOURCE_INSTALL.md says what
`native` and the other values build.

Release and setup builds pass their architectures explicitly and are unchanged. Configure matrix, CUDA 12.9 on Linux:
none -> 120 (+ the warning), CUDAARCHS=86 -> 86, 89, "75;86;120" (w4a4 120a only for the 12x entry), native -> detected.

Ported from PR #4029.
2026-10-26 18:35:58 +01:00
sergqwer 4a7e373da3 engine: --adapt-decay outside 0..1 falls back to 0.7 with a line on stderr (#1284)
--adapt-decay took any number. Above 1 the adaptive tier's usage counts grow without end (inf after a few hundred
adaptations) and the swap gains turn NaN, which breaks the sort that orders the swaps; a negative value flips the counts.
The value is now checked where it is parsed (include/strata/program/adapt_decay.hpp, also in sycl/): a finite value in 0..1
is used exactly as before (0 and 1 included, the range the calibration profile already allows); anything else, NaN and inf
too, runs with the default 0.7 and says so on stderr. A warning instead of the PR's exit 2: a risky pick is reported,
the engine still starts.

adapt_decay_test (25 checks) covers the range, the fallback and finite counts after 100000 adaptations.

Ported from PR #1284 (the check; the refusal became a fallback).
2026-10-26 18:35:58 +01:00
Niko1221 bb62a048b8 updater: a failed test start keeps the runtime records, not only the engine log tail (#3654)
The sandbox of a test start was deleted at once, so the PID records that show why a start failed were lost; only the last 64 KiB of the engine
log stayed. A failed start now also keeps run.json (without keys), the host, child and ready JSON records of the runtime (tokens cut to their
first 8 characters) and the host and server logs in .updates/logs/test-start-<version>-failed. The last three failed starts are kept.
Best effort, never hides the failure. Tests: tests/terminal/test_updater.py (FailedStartRecords).
2026-10-26 18:35:15 +01:00
Niko1221 484c689337 DeepSeek: the 4 x R9700 share-based defaults (small chunk for multi-card tp runs, whole-chunk slab, small staging ring, pinned copy at share >= 0.5) apply to HIP builds only; the staging ring takes a hip flag; tests for both branches 2026-10-26 18:33:37 +01:00
Niko1221 3b64abf5e0 DeepSeek drafter cumulative cut 0.5 by default on gfx1200/gfx1201 only (Model::gpus_are_gfx12); Strix Halo, other AMD and CUDA keep 0 until measured 2026-10-26 18:29:34 +01:00
Niko1221 c8596ec391 Merge f-027-marlin (DeepSeek on 4x R9700: staging ring from the room, copy pin auto, chunk 1024 where the cards hold most experts, whole-chunk attention slab, F16 gate/up activations, drafter cumulative cut on HIP, diagnostics). Conflicts resolved: attention slab = one rule (attn_slab_rows: Marlin's share >= 0.5 branch plus the automatic growth, unified memory and DS4_ATTN_SLAB=0/N untouched); staging ring = one function (staging_ring_mb: 16 MB where the cards hold the experts, Marlin's small per-device ring at 0.5..0.82, main's streamed 400..1200 MB on unified memory and under half); DS4_COPY_PIN auto = huge-page arena OR cards hold >= half; DS4_FAST_ATTN stays gfx12-only; chunk 1024 for multi-card tp runs too, but not for --threads on or one card on the default tp 2026-10-26 18:28:51 +01:00
Niko1221 bf994ca414 setup: a build folder configured with another generator is configured again instead of stopping cmake (#4038)
The manual build in docs/INSTALL.md uses CMake's default generator (Unix Makefiles on Linux) in build/, and setup --build always asks for
Ninja, so cmake stopped with 'Does not match the generator used previously'. cmake_build now reads CMAKE_GENERATOR from the folder's
CMakeCache.txt; when it is not Ninja it says so in one line and removes the cache and CMakeFiles (no other file). A Ninja folder or a new
folder is untouched. Tests: tools/test_setup_build_dir.py.
2026-10-26 18:28:23 +01:00
Niko1221 590c69a578 cmake/ggml_tq_overlay_test.cmake sets CMP0057 NEW so IN_LIST works under CMake 3.x (#3412)
A cmake -P script has no cmake_minimum_required, so CMake 3.x used the OLD behaviour and stopped at 'if given arguments ... IN_LIST: Unknown
arguments' (ctest ggml_tq_overlay with CMake 3.31). Checked with pip cmake 3.31.6 and 4.4.3 against the pinned llama.cpp: both print
'ggml_tq_overlay_test: ok'. A static test keeps the policy before the first IN_LIST in every cmake/*_test.cmake.
2026-10-26 18:27:27 +01:00
Niko1221 7507b47525 sycl/tools/build.sh exits with the build's own status (#2990)
The script ended with a grep pipeline, so a failed compile exited 0 (and, before the ocloc check was added at the end, a good build exited 1).
A build.sh && next-step chain then ran after a failure. The status of cmake --build is kept and the script exits with it. The test runs the real
script with a stub cmake on PATH (good build: 0, failed compile: 1; the old script exits 0 on the failed compile).
2026-10-26 18:26:40 +01:00
Niko1221 1be7eb77c1 tests: the setup golden compares the platform's vision binary names, and test_nvfp4_local_pack skips without pytest (#3325)
tools/test_setup_golden normalized strata.exe but not strata-vision.exe, so all 25 baseline entries failed on Linux (48 subtests). VEXE
and HVEXE are mapped to <VEXE> and <HVEXE> like EXE, and the baseline records <VEXE>. test_nvfp4_local_pack imports pytest, which is not in
requirements.txt; under python -m unittest discover the module is now skipped with a reason instead of failing to import.
2026-10-26 18:25:25 +01:00
ctr-137 800730103c tools/strata_bench.py: the redactor replaces a folder before the short user name inside it (#3619, #3326)
Substitutions were ordered by the length of their regular expression, and a user name wrapped in word bounds outranked the Strata or home
folder that contains it, so a report kept /home/<user>/Strata instead of <strata>. They are now ordered by the text they replace. Ported from
PR #3619 by ctr-137 with its test (Linux and Windows layouts).
2026-10-26 18:24:35 +01:00
Niko1221 9a0462a011 Chat: sending a message still scrolls to it when the reader had scrolled up (#4014) 2026-10-26 18:24:01 +01:00
Patrick Schaub bc939b6303 docs/SAFETY.md: strata launch updates an installed DeepSeek engine package by itself (#4028, #4022)
The page said that starting a model never downloads and that nothing checks for updates in the background. Since 0.2.4.2 strata launch with a
DeepSeek model checks the signed feed (10 s at most, silent without a connection) and replaces an older installed DeepSeek engine package.
Ported from PR #4028 by pschaub (wording from issue #4022), checked against strata_cli/launch/runner.py upgrade_dsv41_engine.
2026-10-26 18:23:38 +01:00
xxxliqu1dxxx 3cda075b51 setup (AMD, Linux): the engine probes get the engine's lib_dirs on LD_LIBRARY_PATH (#3305)
engine_env put the ROCm library folders first on PATH, which only Windows' loader reads. On AMD Linux strata --families exited 127 on
libhipblas.so.3, so setup falsely said the engine does not run Qwen3.6. On Linux and elsewhere the folders now go first on LD_LIBRARY_PATH
(an existing value is kept); Windows keeps PATH. Ported from PR #3305 by xxxliqu1dxxx (resubmission of #2914), with its tests.
2026-10-26 18:23:22 +01:00
cuijinkuo 7c9b6f3a28 MCP chat: opt-in mcp.image_results caches PNG/JPEG/WebP tool results beside the config, shows them in the web chat with an authenticated download and passes them to the vision reader (PR #2473 by cuijinkuo) 2026-10-26 18:22:31 +01:00
Demetree Kallergis cc2c628cbe setup (Mac): say when an update was asked for a model that is not installed (#3848)
metal/setup_mac.py returned right after the engine step on --update while the model questions had been asked, so choosing a model the Mac did
not have ended with no download and no word. The update now names the missing model (or image encoder) and points at ./setup.sh --setup; an
update for a model that is installed prints what it printed before. Ported from PR #3848 by demetree, with its two tests (mocked; not run on a Mac).
2026-10-26 18:22:30 +01:00
MegaRushX 3e81173d63 chat.py: fenced code blocks in streamed answers are coloured on a terminal; prose is untouched (PR #1895 by MegaRushX) 2026-10-26 18:21:36 +01:00
medking82 df6928400f Agent: the staged Harness keeps output room in small contexts (8K): a guarded, hash-checked correction of pi-ai 1.1.0's safety margin below 64K (PR #3684 by medking82) 2026-10-26 18:21:20 +01:00
medking82 1e4ae3ddbf Models page: a failed preparation is shown only on the model it belongs to, not on the next model you select (PR #3361 by medking82) 2026-10-26 18:21:08 +01:00
kossumandMIDHAT NASHAR e2e99ae54a Windows: the token default DACL grants generic rights, so a child process can still be opened after adopt_user_defaults (#3428, #3399)
adopt_user_defaults() set the token's default DACL with FA (FILE_ALL_ACCESS). That default also applies to new process objects, and the file
mask has no process query or set-information right, so a child could not be reopened by PID: the HighQoS helper timed out and
procs.start_token could not record the child. GA lets Windows map the mask to the object being created; the principals are unchanged.

Ported from PR #3428 by kossum (QoS reopen test). Test from PR #3399 by midhatn (a live child is identified and recorded) added; both fail
without the change and pass with it.

Co-authored-by: MIDHAT NASHAR <[email protected]>
2026-10-26 18:21:07 +01:00
medking82 f499922379 App: the composer's effort (Off, Low, Medium, High, Max) reaches saved API models that support reasoning, with the OpenRouter mapping; unknown support keeps the provider default (PR #3363 by medking82) 2026-10-26 18:20:52 +01:00
jubeater 14bef6bb46 setup: hand component engines to the server for validation (#4027, #4026)
_start() checked cfg['exe'] with Path.exists(), so a run config whose engine is component:strata-engine was refused as a missing file
before the server could resolve and verify it. Ported from PR #4027 by jubeater. A component reference is no longer tested as a file; model
files and literal engine paths are checked as before. ensure_engine_for, the Windows HIP runtime repair and the HIP architecture lookup
skip the source-build engine folder for a component. Eight tests (tools/test_setup_component_start.py).
2026-10-26 18:20:42 +01:00
zapabob c7833932a1 App: a model load validates and parses the same captured text of its settings file, so a file changed between two reads cannot start with unchecked settings (PR #3954 by zapabob) 2026-10-26 18:20:17 +01:00
SuperDistance d2c69e6d4b tools/strata_bench.py: read CMakeLists.txt as UTF-8, not with the locale's code page (#4011, #2792)
Ported from PR #4011 by SuperDistance. project_version() read CMakeLists.txt with Path.read_text() and no encoding, which raised
UnicodeDecodeError on Windows code pages such as cp936. The test makes read_text default to each of 14 Windows code pages. The same crash was
reported in #3235 and #3414.
2026-10-26 18:20:17 +01:00
BColsey 19bfcb33f0 Tests: the Terminal fixtures write settings where the platform keeps them and no longer swap os.name or assume a Windows ACL on Linux (PR #3782 by BColsey) 2026-10-26 18:19:55 +01:00
Niko1221 9e2c0b6eb0 docker: test that both Dockerfiles take the source commit and the source-build commands pass it (#3644) 2026-10-26 18:19:55 +01:00
jackbapa 24c3f24ae1 Tests: the ACL checks compare trustees through the SDDL alias, so an account that Windows writes as LA passes (PR #3406 by jackbapa) 2026-10-26 18:19:43 +01:00
Niko1221 7c4c20799c Residency-biased routing default is CUDA-discrete-only: read from the device properties once in Verifier::init (never first inside a graph capture), off in HIP builds and on integrated GPUs, a line in the log when it is on; the wide HC up read never defaults on in a HIP build (gfx12 reports compute capability 12.0 too) 2026-10-26 18:19:39 +01:00
Niko1221 68ba321acc launch: a model folder is found whatever its letter case, so a downloaded model is not offered again on a case-sensitive disk (#3996); a log path the person set in the config is kept (#3656); --dspark takes on|off|auto|none and the word reaches the engine (#3959) 2026-10-26 18:19:38 +01:00
Niko1221 a8755042bd Delete a model: every part of a multi-part GGUF goes with it (the run config names only the first), and a part another model names stays (#3716) 2026-10-26 18:19:38 +01:00
Roberto Jose Etcheverry Romero 255861c442 docker: the root Dockerfile passes the source commit to the Terminal stage (#3644)
.git is not in the build context, so package_terminal.py had no commit to put in the archive and the documented source build stopped.
Ported from PR #3644 by robertoetcheverryr: ARG STRATA_COMMIT / ENV STRATA_BUILD_COMMIT as in docker/Dockerfile, and the two 'From source'
commands in docs/DOCKER.md pass --build-arg STRATA_COMMIT=$(git rev-parse HEAD).

Added: .dockerignore leaves out the folders docker/entrypoint.sh creates in a mounted data/ (install, state, home, Strata-data). A dangling
data/install/third_party/llama.cpp link in a clone that had been mounted made the Terminal stage fail with a copytree error (seen in the
review thread of #3644). Not built with docker here (no docker on this PC).
2026-10-26 18:19:35 +01:00
i-Vladimir-i e0469a585f MCP chat: at the tool-round limit the model gets one final reply with no tools from the results already found, instead of the turn ending without an answer (PR #3741 by i-Vladimir-i) 2026-10-26 18:19:27 +01:00
天空Blond d398f35325 docker: the build context carries the nested tools/ scripts the Terminal bundle reads (#3974)
docker/build.sh staged only top-level tools/*.py, so the Terminal stage stopped at 'installer tool missing: tools/dsv41/tune.py'.
Ported from PR #3974 by hurui200320: the pathspec is tools/**/*.py, the no-.git fallback finds nested files, and the context test reads
INSTALLER_TOOLS and INSTALLER_SCRIPTS from packaging/package_terminal.py. Fixes the failure that images built from v0.2.4.1 and v0.2.4.2 had.
2026-10-26 18:19:09 +01:00
toel1234 dc1fd367d3 procs: an engine adopted by systemd --user is found as an orphan, and ending it waits on the start token until the process is really gone (PR #3254 by toel1234) 2026-10-26 18:19:05 +01:00
MaKla89 eb1c40038c App sharing: the shared and tunnel address answers a browser's CORS preflight and sends Access-Control headers when an API key is set; without a key nothing changes (PR #3853 by MaKla89, #3208) 2026-10-26 18:18:15 +01:00
thisisspartan cd72accac3 Web page: a request that timed out in the queue shows a Timeout badge in Recent requests (PR #3991 by thisisspartan) 2026-10-26 18:17:49 +01:00
Niko1221 4abccbb360 Merge f-027-cuda (CUDA speed work: wide HC up read on sm_120, residency-biased routing on small cards, STRATA_TRACE, release-notes draft) 2026-10-26 18:16:40 +01:00
Niko1221 f92cfa4374 Merge f-027-win (main 2d07d1c0b with the DeepSeek slab scoping 32 MB unified / 8 MB discrete plus automatic growth, the dq kernel arch choice, and the HIP-only guard for the gfx11 WMMA gemm call) 2026-10-26 18:16:34 +01:00
justindressler b3db5a1c83 serve: the lifeline identity is the child's start time, the sampler survives a bad source (#3590, #3407)
The portable lifeline watcher built a child's identity from `ps -o lstart=,stat=` and kept the whole line, so the token
changed with the process state (R while it starts, S asleep). The watcher compared its own reading with the token taken
at start, decided it was not the child it was told about and exited without ending it (test_lifeline "the child outlived
a killed server (watcher)", #3407). The identity is now the start time alone: field 22 of /proc/<pid>/stat on Linux
(a zombie has none), `ps -o lstart=` elsewhere; strata_control/procs.start_token drops the state column the same way.
A launch that is not a real Popen is not watched.

Telemetry._loop no longer ends on the first failing sample (a machine whose GPU or psutil source is unreadable): it
prints one line and keeps sampling, and NoLeakedSampler checks the thread itself instead of counting threads.

New test: the token is the same while the child runs (R) and sleeps (S); on the old code it fails on Linux
('...R' != '...S').

Ported from PR #3590.
2026-10-26 18:15:01 +01:00
Niko1221 9a1e768388 App: the source-checkout server is started without an empty STRATA_API_KEY (#3851); a card taken out of the PC no longer decides the engine make, and a PC with two makes accepts a build for either (#3892) 2026-10-26 18:14:26 +01:00
Niko1221 085dcded73 Monitor: the host reads the graphics card every 3 s only while a model runs or the Monitor is open, every 30 s otherwise, and the PCIe sample only while a model runs (#4066) 2026-10-26 18:12:22 +01:00
Righteous-AI fd8d37e93d serve: accept Codex spellings of a namespace tool (#3972)
A declared namespace.member tool (Codex MCP: mcp__filesystem.write_file) was accepted only as namespace__member (#3556).
Qwen3.8-Flash-Next also writes namespace=member (the <function= tag's equals sign ends up in the name), namespace_member
(one underscore dropped) and the bare member. Each is now an alias of the declared tool when exactly one declared tool
produces it and no declared name is the same; a name that matches nothing stays text (#3075), two namespaces that share
a member are not guessed, and a declared flat tool always wins. Requests without namespaced tools are unchanged.

serve/test_tool_name_alias.py (10 tests, 5 of the first 9 fail without the change; the tenth pins that a flat tool named
like the underscore alias wins), docs/DETAILS.md names the spellings.

Ported from PR #3972.
2026-10-26 18:12:20 +01:00
Niko1221 89a68d8829 Chat: the transcript follows the answer only while the reader has not left; a wheel notch, finger or key moving up lets go at once, in the app chat and the served web page (#4014) 2026-10-26 18:11:11 +01:00
Stefanos Anastasiou f8ff3420d9 conversation cache: never park one conversation with another's retained K/V (#1503)
With --batch and conversation parking, a restore keeps the restored K/V (ConversationCache::retain) so the next park of
the main session copies only what changed. The reuse tracked a token count (unchanged_tokens), not whose tokens, and
copy_from_slot (a batch slot giving its conversation back to the main session) replaced the main session's K/V without
dropping the retained image. The next park reused the earlier conversation's K/V for the new one's prefix: a restore of
the new conversation then answered from the other one's history.

retain() now records the ids of the conversation the K/V belongs to; a park calls take_reuse_for(live), which reuses only
the prefix live shares with those ids (nothing when they share none or the ids are unknown), and one stderr line is
printed whenever a reuse is cut. copy_from_slot drops the retained image before it replaces the main session's K/V.
Same edits in sycl/src/program/generate.cpp. The guard only cuts reuse: at most one full copy of a parked conversation's
K/V, the saved bytes are otherwise identical; no flag, no default change.

Ported from PR #1503 onto the current tree (the first-stage ids sit after the stages field so the existing aggregate
initialisers keep their meaning). conversation_cache_test gains the cross-conversation cases of the PR and the logged
sequence (restore P, a slot hands Q to the main session, park Q): another conversation never gets the K/V, a diverging
one only the shared prefix on every stage, a slot hand-back drops it, unknown identity is never reused. Removing the
ids limit makes the first case fail.
2026-10-26 18:09:44 +01:00
Niko1221 f681b9ad62 Manual mode: the look-only shell allowlist takes only options known to look (#3975)
A command on the allowlist used to run without a card with any option except -delete/-exec/-execdir/-fprint/-fls/-ok. Write modes (sort -o,
tree -o, uniq IN OUT), program-execution modes (sort --compress-program, rg --pre, git grep -O), date -s and tail -f got through, and so did
git config -e, git branch --set-upstream-to and git remote ... forms.

Each command now has a spec of the options and operands that only look (short and long, spelled in full); every other option, abbreviation
or attached form asks. git subcommands have the same specs, a branch/tag name is taken only with a list option, git config only reads with
--get. date takes only +FORMAT operands, uniq at most one operand, hostname none. less and more leave the list. Quoted texts are checked as
operands too. `du -sh` no longer trips the 'sh' word check.

Tests: apps/host/tests/gate-readonly.test.mjs (deny and allow cases for options, operands, spellings, quoting; every listed command asks for
an unknown option).
2026-10-26 18:08:17 +01:00
Niko1221andyehya 6c01f85add qwen35: a penalty for a history token outside the logit row no longer indexes past the row (#3911)
sample() applied the repeat / frequency / presence penalties to lg[id] for every token id of the last penalty_last_n
tokens of the history. A row shorter than the vocabulary (the MTP draft head picks among the first N token ids,
--draft-vocab N) made a history token with id >= N an out-of-bounds read and write on the host. A token that is not in
the row takes no penalty; the loop skips ids < 0 or >= lg.size(). A full-vocabulary row is bit-identical.

The loop and Sampling move to include/strata/qwen35/sampling.hpp (header-only) so a CPU unit test can run them:
src/qwen35/sampling_test.cpp (qwen35_sampling_test) checks nothing is written past a short row, that in-row tokens
take the penalty, and that a full row matches the unguarded loop bit for bit over 108 penalty/window mixes. Without the
guard the test dies with an access violation.

Co-authored-by: yehya <[email protected]>
2026-10-26 18:07:29 +01:00
Niko1221 3a604e0855 Merge f-027-kva (docs/KVA.md: the shipped opt-in's numbers on the R9700 and the P100, and the decode trade on fast GPUs) 2026-10-26 17:52:48 +01:00
Niko1221 6f45b24864 docs/KVA.md: the R9700 and P100 numbers for the shipped opt-in (split 36), and the trade stated plainly 2026-10-26 17:52:39 +01:00
Niko1221 063b3c5dc8 DeepSeek engine: the RDNA3 WMMA dense GEMM call in gemm() is HIP-only (wmma_gemm.cu is part of the AMD builds; main's 0eea4a3d5 called strata_pf_gemm_f16_ld unconditionally, so a CUDA build failed to link: unresolved external) 2026-10-26 17:48:36 +01:00
Niko1221 20c2f42bf1 ds4_iq.cu: the NUL character literal in dq_use_old_kernel had been written as a raw NUL byte by the merge script (nvcc: quoted string should contain at least one character); it is a backslash-zero escape again 2026-10-26 17:36:16 +01:00
Niko1221 2e1fad783d host test offline-start: point STRATA_COMPONENTS_DIR at the test's own folder so the host cannot find an installed Strata Control. On a PC with the app installed the update check went through the installed Control (no host fetch, 45 s), so the test saw 0 outside calls and failed; it failed the same on v0.2.6. With the fix both tests pass in 2-12 s. 2026-10-26 17:35:48 +01:00
Niko1221 afc45665fc Merge main 2d07d1c0b (Strix Halo DeepSeek work: fd33cb6ee, 0eea4a3d5, 3133160ed, 2d07d1c0b) into f-027-win. The attention slab: the base is STRATA_DS4_A_MB megabytes of q, 32 MB on a unified-memory GPU (where main measured it) and 8 MB on a discrete card (the 0.2.6 slab); the automatic growth (attn_slab_rows) stays on top for discrete cards with spare VRAM, DS4_ATTN_SLAB still overrides, both env names work. The dense dequantizer: dq_rows_kernel2 stays the default; a gfx11 device (gcnArchName) defaults to main's improved row-per-lane kernel, DS4_DQ_OLD=1/0 forces either. 2026-10-26 17:29:43 +01:00
Niko1221 63b6e0e834 Merge branch 'main' into f-027-cuda 2026-10-26 17:19:22 +01:00
Niko1221 259532bf2e prompt: the RAM store's post-prompt preload is off by default - it costs a 4K prompt 4.7 s and the decode that follows gains nothing
cache_settle()'s store preload (DS4_SETTLE_STORE) read 2,243 hot experts into free store slots after every chunked prompt: 4,713 ms of a 4K prompt.  Measured on the box (code fixture, prompt + 256 tokens, interleaved, 3 runs a side): with it on the decode is 13.9/14.4 s, with it off 14.2 s - no benefit - while the 4K prompt goes 31.3 -> 26.6 s (130.89 -> 153.78 tok/s).  The swap loop above it is what the decode uses (it holds the CPU lane at ~5-7 k experts a run against ~14 k without), so that keeps its budget; DS4_SETTLE_STORE=1 restores the preload.
2026-10-26 17:07:53 +01:00
Niko1221 9c059bade1 Merge f-027-kva (KVA ships off in 0.2.7: STRATA_KVA unset is the exact 0.2.6 engine; STRATA_KVA=1 approximates layers 36-47 with a 2048-token exact tail, the configuration that kept agent loops and long extraction at the exact level; geometry gate; draft fixes; opt-in STRATA_KVA_SKIP=moe) 2026-10-26 17:02:35 +01:00
Niko1221 9c8c90391d Merge f-027-hip (gfx12: decode miss split starts at pcie_frac 0.15, STRATA_PCIE_FRAC_DEFAULT=old restores 0.55; DeepSeek matrix-core prompt attention on by default on gfx1200/gfx1201, DS4_FAST_ATTN=0 off; huge-page copy arena and its pinned DMA on only when a reserved pool holds the copy; KVA draft fixes) 2026-10-26 17:02:13 +01:00
Niko1221 bda3005082 KVA, when switched on, approximates layers 36-47 with a 2048-token exact tail (the measured configuration); STRATA_KVA_SPLIT / STRATA_KVA_SUFFIX override
On the RTX 5070 (--agent-long, 22 tasks) split 36 kept agents at 6/10 (exact 4 and 7) and extraction at 585/606 facts (exact
585) for +11% prompt speed at 14K and +18% at 30K; the split-24 default passed 0/10 agent runs and found 556 facts, a tail of
8192 with split 24 found 574. KVA stays off unless STRATA_KVA=1. docs/KVA.md gives the numbers and the reasons; kva_test
checks the opt-in's split 36 / tail 2048 and the overrides. The geometry gate is unchanged.
2026-10-26 17:02:12 +01:00
Niko1221 aeb70de349 Merge f-027-kvaoff (KVA is off by default: the app's and the Terminal's 'Faster long prompts' switch turns it on, shown for Qwen3.8-Flash-Next models only) 2026-10-26 17:01:54 +01:00
Niko1221 76cd7d7970 KVA: the one-time note says how many positions were approximated, not where the tail starts (chunks round the frontier down) 2026-10-26 16:55:44 +01:00
Niko1221 25a62e0fb3 KVA: keep the geometry gate - a model whose shape KVA was not measured on reads exactly; STRATA_KVA_ANY_MODEL=1 is the research override
The app toggle will write STRATA_KVA=1 for Qwen models in general; the split layer, the tail and the measured quality apply to
Qwen3.8-Flash-Next's shape only (48 layers, 512 experts). Any other shape (Qwen3.6-35B-A3B, ...) now logs "KVA configured but
NOT applied: this model's shape (L layers, E experts) is not one KVA was measured on" and reads exactly; the routine reasons
(short prompt, continuation) still come first. STRATA_KVA_ANY_MODEL=1 allows it, with one warning. kva_test and docs/KVA.md
cover both.
2026-10-26 16:40:59 +01:00
Niko1221 72b7696810 Faster long prompts is shown for Qwen3.8-Flash-Next models only (app and Terminal)
KVA was measured on the Flash-Next shape (48 layers, 512 experts) and the engine declines any other shape with a log reason, so the row would do nothing
for Qwen3.6-35B-A3B. Setup writes "arch" in a run config only for the families with an engine path of their own (Qwen3.6-35B-A3B: qwen35moe; DeepSeek:
deepseek41); every Qwen3.8-Flash-Next file (the original, Swift, Coder, Unsloth, Gyro, NVFP4) has none. The row is therefore shown for a model without an
"arch" on CUDA or HIP, in the app's Native engine settings and in the Terminal's Advanced table. The Radeon RX 9000 and DeepSeek rows are unchanged.
Tests: a Qwen3.6 config gets no KVA row (CUDA and HIP), the six Flash-Next family names do, and through the host fixture a model with an arch has no switch row.
2026-10-26 16:38:15 +01:00
Niko1221 e3450e4552 KVA ships OFF in 0.2.7: STRATA_KVA unset is the exact 0.2.6 engine, STRATA_KVA=1 turns it on with an 8192-token exact tail
The quality runs found lost records and invented rows in long extraction tasks and defects in agent loops with a ~10K
system prompt when most of the prompt was approximated. Opt-in now: unset (or 0) nothing runs, the draft layer included.
Switched on, the tail is 8192 tokens (STRATA_KVA_SUFFIX overrides), the safest configuration measured. A model other than
Qwen3.8-Flash-Next's geometry gets a one-line warning instead of a refusal. Kept: the untouched draft layer (cell floor
behind STRATA_KVA_DRAFT_FLOOR=1), the decline reasons, STRATA_KVA_SKIP=moe, the tests. docs/KVA.md says why it is off and
how to turn it on; kva_test checks unset = off and STRATA_KVA=1 = on with the 8192 tail.
2026-10-26 16:36:31 +01:00
Niko1221 9760cbca4b App: "Faster long prompts" (KVA) is Off by default in the model's Expert settings; On writes STRATA_KVA=1, Off removes it
The Speed row now follows the shared table's default: Faster long prompts reads "Off (recommended)" / "On" and its new sentence is "Reads very long prompts
faster by approximating part of the work. Off by default: answers about long documents can miss or mix up details." (replaces fx73; "Off (recommended)"
is fx79; translated in zh, ja, de, es, ru, pt, ko). The other two rows are unchanged ("On (recommended)" / "Off"). A STRATA_KVA=0 stored by an earlier
0.2.7 test build shows as Off. Still shown only where KVA applies (Qwen on CUDA or HIP). tests/speed-switches.test.mjs updated: the default launch has
no KVA entry, On writes STRATA_KVA=1, Off removes it, the old 0 shows as Off, and the other two switches are tested as before.
2026-10-26 16:34:01 +01:00
Niko1221 ae03ef6323 Terminal and the shared speed-switch table: Faster long prompts (Qwen KVA) is OFF by default, STRATA_KVA=1 turns it on
Owner decision (26 October, conditional on the last KVA variant results): KVA ships off. strata_control/speed_switches.py now gives every switch a default
and the environment it writes outside that default: KVA is default off and writes STRATA_KVA=1; the Radeon RX 9000 and DeepSeek switches stay on by
default and write their off entries as before. A launch in the default carries no entry. A file that holds STRATA_KVA=0 (the off switch of an earlier
0.2.7 test build) reads as off, which is what it is now; choosing Off cleans it up.
The Advanced table row says "off by default; set 1 to turn on" with the new sentence (`--set STRATA_KVA=1`; `off` or Del puts the default back).
tests/terminal/test_speed_switches.py covers the defaults, KVA on/off/old-zero, the rows and the comparison with the app's table.
2026-10-26 16:34:01 +01:00
Niko1221 3171291420 KVA: STRATA_KVA_SKIP=moe (opt-in) - the late layers of an approximated chunk run their whole attention half and skip only the MoE half
The default stays the state-only form (byte-identical). With STRATA_KVA_SKIP=moe the GDN recurrence (gate projection, output
normalization, output projection) and the QSA attention (query side, selection, attention, output projection) run in full
for layers >= split, their outputs go into the residual, and the loop leaves before the MoE half; the expert transfers of
those layers are still skipped. The KVA line says "mode moe-skip". Motivation: the extraction losses of the state-only form
come from late-layer K/V and state built from a stale residual; this keeps the attention halves' contribution in it.
kva_test covers the switch.
2026-10-26 16:18:49 +01:00
Niko1221 fd81df6972 DeepSeek engine (AMD): the matrix-core attention on by default for the chunked prompt
fast_attn_enabled() was opt-in (DS4_FAST_ATTN=1); with the prompt chunk at 1024 it is worth far more than when
it was gated: measured on 4x R9700, 2.25 bpw, 8K code prompts, three warm prompts a load, the same ids
(5705e1164a) --

  without it  1159 / 1180 / 1195 ppts   (median 1180)
  with it     1327 / 1354 / 1360 ppts   (median 1354, +14.7%)

and after this commit the un-enved default reads 1314 / 1357 / 1369 (median 1357), i.e. identical to setting the
flag, so a deployment no longer needs any environment at all.  The QK/PV sums are the matrix cores' order, which
the owner already approved for the prefill; the decode path keeps the scalar kernels.  DS4_FAST_ATTN=0 turns it
off (it is a no-op on CUDA, whose dispatch is not compiled).
2026-10-26 16:16:28 +01:00
Niko1221 879b958a3e docs: setup is expected to choose --ple-io ram on a RAM-rich Linux PC - say to check setup --plan rather than asserting it 2026-10-26 16:08:11 +01:00
Niko1221 be4450fdc3 Merge branch 'main' into f-027-cuda 2026-10-26 15:51:50 +01:00
Niko1221 67cfaf2ac6 prompt streaming: the staging ring sizes from the VRAM room (400 -> 1200 MB on Strix Halo) and the attention slab defaults to 32 MB - 4K prompt 27.3 -> 21.0 s
ring_mb_auto() returned a flat 400 MB on any box whose experts do not fit in VRAM, but on such a box the ring is exactly what lets a prompt queue its likely experts early.  400 -> 1200 MB measured on the box (settle off): the queue 3,200 -> 8,087 experts, the disk's effective rate 1.49 -> 2.44 GB/s, the compute's exposed read wait 11.4 -> 3.5 s, the 4K prompt 148.98 -> 190.98 tok/s; 16K 290.97 -> 315.01, 32K 316.92 -> 325.89.  Decode unmoved in an interleaved A/B (17.54/16.39 against 17.61 tok/s).  It is now a sixteenth of the VRAM room clamped to 400-1200 MB, so a smaller pool still loads; DS4_RING_MB still overrides.

c.A: the attention slab decides how many times each dense matrix is re-read a prompt.  8 -> 32 MB (STRATA_DS4_A_MB): 32K 325.89 -> 357.78 tok/s, 16K 315.01 -> 328.04, 8K +1.6%, 4K flat, decode unmoved (16.81 against 17.60 tok/s interleaved, noise-limited) for ~0.4 GB of VRAM.

Best measured prompt set with the two new defaults (DS4_SETTLE_MS=0;DS4_SETTLE_STORE=0, which is a prompt-only trade): 4K 195.18, 8K 258.47, 16K 328.04, 32K 357.78 tok/s.  With the settle work at its default a 4K prompt is 31.3 s = 130.89 tok/s (was 88.30).
2026-10-26 15:43:31 +01:00
Niko1221 bffb41ef47 Merge f-027-win (ctest registers ple_residency_test with the GPU-build tests) 2026-10-26 15:13:40 +01:00
Niko1221 3e2f45e2e1 ple_residency_test is registered with the GPU-build tests (it sat in the conversation-tests block, which is off by default, so ctest never ran it) 2026-10-26 15:12:23 +01:00
Niko1221 de6229fb17 Merge branch 'main' into f-027-cuda 2026-10-26 15:08:46 +01:00
Niko1221 1ee6060afb docs: the 12 GB card's prompt figures are withdrawn - a five-pair repeat read +254% and -20.5% where the first read +35% and +17%, and that box's prompt swings 218-466 tok/s between arms 2026-10-26 15:08:43 +01:00
Niko1221 cca3dd2e33 dense GEMM: the chunked prompt's gemm() prefers the engine's own RDNA3 WMMA kernel (src/prefill/wmma_gemm.cu, now linked) over hipBLAS - 4.97 vs 20-40 TFLOPS at our shapes
The dense GEMM was 11.1 s of a 4K prompt at hipBLAS's measured 4.97 TFLOPS (M 4096 N 2304 K 5120, the engine's call convention). A blocked WMMA kernel reaches 23.96 TFLOPS at that shape (128x128 tile, BK 32, 2-stage LDS, rocwmma; scripts/wmma2.cu) and the engine's own tuned kernel (BM 128 x BN 256, grouped tile order, raw wmma intrinsics) 20.8-20.9 at the wq_b shape and 38-41 at M 16384. gemm() now calls strata_pf_gemm_f16_ld with hipBLAS as the fallback (rows < 64, N < 512, K % 32, unaligned X: unchanged); STRATA_DENSE_PFGEMM=0 restores hipBLAS for an A/B. Same operands, the kernel's own k order (rounding-level).

Measured on the box (eng-028, 4E224E7F -> 467D7320, 4K prompt fixture p4k, chunk 16384): 46.4 -> 38.1 s, 88.30 -> 107.63 tok/s; with DS4_SETTLE_MS=0;DS4_SETTLE_STORE=0 27.5 s = 148.98 tok/s. 8K 134.66 -> 218.31, 16K 158.6 -> 290.97, 32K 178.9 -> 316.92 (settle off). Decode unchanged in an interleaved A/B (old 15.17/16.73, new 17.38/18.48 tok/s) with acceptance 151/164 = 92% (93/98/100/100/90 per position).

c.A (the attention slab, the reason each dense matrix is re-read 10-16x a prompt) takes STRATA_DS4_A_MB: 32 gives 107.63 -> 110.67, small because the expert reads now dominate.
2026-10-26 15:07:37 +01:00
Niko1221 4329c0d380 Merge f-027-ctx (DeepSeek-V4.1-Flash: 512K and 1M contexts in setup, the Terminal and the app up to the pack's own limit, with a 'not tested yet' note past 256K; the app now honours a chosen DeepSeek context; idea from Project Maya v1.1.28 #442 by @needmorevram) 2026-10-26 14:47:11 +01:00
Niko1221 f9fa567e1d Merge branch 'main' into f-027-cuda 2026-10-26 13:58:25 +01:00
Niko1221 6bc27ebef7 docs: the agent scenario across all three boxes 2026-10-26 13:34:58 +01:00
Niko1221 b8946f4c7b Tests: nobody gets a bigger DeepSeek context than before unless they choose it (8K stays the default in setup, the Terminal and the app; an installed model shows and loads the context it was prepared with) 2026-10-26 13:28:18 +01:00
Niko1221 c4c320a755 App: DeepSeek's context goes up to the pack's limit (512K, 1M), with a plain "not tested yet" note past 256K, in eight languages (Project Maya #442, @needmorevram)
The Models screen listed the shared sizes for DeepSeek but installed and loaded it at 8K whatever was chosen. Now the choice counts (8K stays the
default for a version that is not installed) and the list is DeepSeek's own: 8K to the limit the installed pack states in ds4.txt (max_position_embeddings,
read by the host as `contextMax`); a pack without it behaves as before (up to 256K). Past 256K the existing note slot under the model shows: "Not tested yet.
A context this long takes graphics memory from the model and reads long prompts slowly." (fx78 in the eight fix catalogs). Qwen's choices are unchanged.
src/lib/deepseek-context.ts is the app's copy of strata_control/dsv41_context.py; tests/terminal/test_dsv41_context.py compares the two.
2026-10-26 13:25:32 +01:00
Niko1221 33780a4955 DeepSeek-V4.1-Flash: 512K and 1M contexts in setup and the Terminal, as Project Maya v1.1.28 did (#442 by @needmorevram, MIT)
The model reads 1,048,576 positions natively. Setup and `strata launch` offered DeepSeek the shared list that stops at 512K and dressed anything past
256K in Qwen's "rope scaling" words. Now:
- strata_control/dsv41_context.py: the choices are 8K, 32K, 64K, 128K, 256K, 512K and 1M (labelled "512K" and "1M"), limited to the pack's own limit
  (max_position_embeddings in its ds4.txt) when it has one; a size past it (2M, or 1M on a pack that says 256K) is the one refusal; past 256K a plain
  note says it is not tested yet and what it costs.
- setup.py: --context is checked before any download and again against the pack once it is built; the install tool refuses the same sizes.
- Terminal: the context step lists DeepSeek's own sizes (and the note beside them), a typed size is checked, `--context 512k` / `1m` and 524288 / 1048576
  are accepted, plan and launch carry the note as a warning; Qwen's list and wording are unchanged.
- Engine: read, no change needed. strata-dsv41 takes --max-context as given (ds4_main.cpp) and sizes its buffers from it (ds4_model.cu); nothing clamps it
  below the pack's limit, and the rope settings come from the pack. A test keeps it that way.
- docs/DEEPSEEK41.md: the cost (graphics memory per position taken from the expert cache, slow reading of very long prompts) and that 512K / 1M are untested.
2026-10-26 13:25:32 +01:00
Niko1221 11b5a77fe9 docs: the agent scenario - two conversations nearly double throughput 2026-10-26 13:23:56 +01:00
Niko1221 f9907b6d9c docs: the routing default is +67% with two conversations in flight 2026-10-26 13:14:27 +01:00
Niko1221 d56dec89b5 Merge commit '2e6f88b7b' into f-027-hip 2026-10-26 13:04:40 +01:00
Niko1221 6c00851ac1 KVA: the draft layer is untouched by default (window 32768, cells built for approximated chunks, no floor); the cell floor is STRATA_KVA_DRAFT_FLOOR=1
Measured on a 5070 (IQ3_S) the cell floor cut decode after a 14K / 30K approximated prompt by 22 / 34 percent (drafts
accepted 100 -> 83 / 73 percent): a draft layer that reads only the exact tail loses its context. The approximated
chunks' draft cells are usable, so by default nothing about the draft layer changes: not the window, not the cells
built, not the attention. The floor machinery (MtpDrafter::set_cell_floor, window_ids_floor, KvaFloorRegistry) stays
behind STRATA_KVA_DRAFT_FLOOR=1 for research. kva_test now really covers window_start with floor 0, the registry and
wants_cell_floor.
2026-10-26 13:03:41 +01:00
Niko1221 2043b682af Merge f-027-win (DeepSeek: the chunked prompt's attention slab is automatic, up to 2048 rows only where the VRAM would not hold an expert; DS4_ATTN_SLAB=0 = the 0.2.6 rule) 2026-10-26 13:00:52 +01:00
Niko1221 e66d2758ed docs: the release notes carry the two-conversation batching gain 2026-10-26 12:46:27 +01:00
Niko1221 03056168d9 dense dequant: the chunked prompt's dq_rows_kernel packs each sub-block's 32 halves into four 16-byte stores and stages the warp's input through contiguous uint4 loads decoded at a relative index - the same values (bit-identical), 4K prefill 79.5 -> 88.52 tok/s, acceptance 97.4% 2026-10-26 12:21:18 +01:00
Niko1221 2d1db1ec6f Merge commit '9c282fbc5' into f-027-hip 2026-10-26 12:15:19 +01:00
Niko1221 7a34c60d61 KVA: leave the draft window alone; a per-session cell floor keeps the draft layer off approximated cells
The first default-on build set --mtp-window 2048 once per process whenever KVA was on, so short prompts (where KVA never
applies) lost draft context and decode speed. Now the draft layer keeps its window (32768 by default) and
MtpDrafter::set_cell_floor() names the first draft cell built from a real row: a KVA read raises it to the end of its
approximated chunks (they are neither built nor read), the draft attention starts at max(floor, pos - window)
(window_ids_floor, window_start.hpp), and KvaFloorRegistry gives every state a request continues from (live, checkpoint,
parked) the floor of its own tokens. A session KVA did not approximate has floor 0 and attends exactly as with
STRATA_KVA=0; with STRATA_KVA=0 or a process KVA is ruled out for the draft layer's code is 0.2.6's. The tail is 2048
whatever the window. Unit tests: window_start with floor 0 equals the plain window, the registry, wants_cell_floor.
2026-10-26 12:14:30 +01:00
Niko1221 50df0b95a0 Merge f-027-app (off switches for the three 0.2.7 speed features: app Native engine settings > Speed in 8 languages, Terminal Advanced rows, shared table strata_control/speed_switches.py; host tests isolated from the real components folder) 2026-10-26 12:08:29 +01:00
Niko1221 af7f7371c3 DeepSeek attention slab: the two new log lines lost their newline escape in the previous commit (a literal line break inside the string literal); no other change 2026-10-26 11:56:33 +01:00
Niko1221 bdde697225 DeepSeek engine: the chunked prompt's attention slab is automatic (DS4_ATTN_SLAB unset): up to 2048 rows where the bigger buffers take VRAM no expert would have used (the expert tier off, or every expert fits with room to spare), else the 0.2.6 rule; DS4_ATTN_SLAB=0 restores the 0.2.6 rule exactly, N rows overrides. If the automatic slab does not fit the load retries with the 256-row slab before it halves the chunk. Measured on the RTX 5070 (12 GB, 2.25 bpw): 1024 / 2048 rows gave +2% prompt but cost 29 / 83 VRAM experts, -3% decode and different greedy ids (placement), so there the rule keeps 256. attn_slab_rows() in ds4_experts_rule.hpp, 14 new checks in experts_rule_test. 2026-10-26 11:34:15 +01:00
Niko1221 eb448299c0 Merge branch 'f-027-kva' into f-027-hip 2026-10-26 11:04:09 +01:00
Niko1221 621e660604 Host tests green on this PC: the GuidedSetup very-large-model sentence gets its catalog key in all 8 languages; the Control lookup test no longer reads the real components folder
- locale-gaps: the sentence added in 72c37ea2d (RTX 30/40/50, R9700, Strix Halo; "the line below says what to expect") had no key. fx77 in the eight fix catalogs.
- packaged-control: findControl was given an env without STRATA_COMPONENTS_DIR, so it fell back to the real per-user components folder and found the
  updated Control installed on the PC (read only). The test now passes a scratch components folder.
2026-10-26 11:03:37 +01:00
Niko1221 cb2e1dbb4e Merge branch 'main' into f-027-cuda 2026-10-26 11:03:10 +01:00
Niko1221 bc5f84fd62 PLE residency policy: the load log also says the expert arena size the rule counted (the proof that the layout is initialised and the sum is not 0) 2026-10-26 10:55:20 +01:00
Niko1221 8a46ca9061 Merge f-027-int (the PLE residency fix) into f-027-kva 2026-10-26 10:55:03 +01:00
Niko1221 0ee72a5041 Merge branch 'f-027-int' into f-027-hip 2026-10-26 10:54:58 +01:00
Niko1221 ad1660aadf Merge f-027-win (PLE residency: admit the table only when RAM holds table + expert arena + 8 GiB; fixes the IQ3_S load failure, error 1453, on a 64 GB Windows PC) 2026-10-26 10:54:45 +01:00
Niko1221 472de9cdac PLE residency policy: admit the table only when RAM holds table + the expert arena + 8 GiB. The 0.2.7 rule (table + 8 GiB) admitted the 26.8 GiB table on a 64 GB Windows PC (RTX 5070, IQ3_S), where the engine then fills a 51 GiB pinned expert arena: the locked table left too little lockable memory and the expert load died with ERROR_WORKING_SET_QUOTA (1453). ple_resident_admit() (include/strata/core/ple_residency.hpp) sizes the arena from the native expert layout; unknown sizes or telemetry admit nothing. Unit test ple_residency_test. 2026-10-26 10:53:11 +01:00
Niko1221 ddd4d5d42b App: off switches for the three 0.2.7 speed features in the model's Expert settings (Native engine settings > Speed), on by default, in eight languages
Three rows under "Speed" in the native settings, in the existing row markup with the existing Select (On (recommended) / Off): Faster long prompts
(Qwen on CUDA or HIP; off writes STRATA_KVA=0), Faster answers on Radeon RX 9000 / AI PRO R9700 (Qwen on a gfx1200/gfx1201 card; off writes
STRATA_PCIE_FRAC_DEFAULT=old) and DeepSeek speed-ups (DeepSeek; off writes the six DS4_* = 0 of the contract). Each has a plain sentence saying what
turning it off does. A row is shown only where it applies, like the DeepSeek Speed row; the host decides from the model's file and the hardware report.

The switch is the model's file "env": off adds exactly its entries, on removes them, so a default launch is byte for byte what 0.2.6 started and the
Terminal, Control and the app reach the engine the same way (serve/server.py child_env). The host now shows and edits STRATA_* and DS4_* names (the
Strix Halo async entry is visible and kept); other names stay untouched. New strings are fx70..fx76 in the fix catalogs, translated in zh, ja, de, es,
ru, pt, ko. tests/speed-switches.test.mjs covers the default, each switch off and on again (same file), where each applies, and the catalogs.
2026-10-26 10:37:53 +01:00
Niko1221 3b3ea883c3 Terminal: the three 0.2.7 speed features as rows of the Advanced table, with their off values and plain words
strata_control/speed_switches.py holds the engine switch contract (Faster long prompts: STRATA_KVA=0; Radeon RX 9000 / R9700: STRATA_PCIE_FRAC_DEFAULT=old;
DeepSeek speed-ups: six DS4_* = 0), where each applies (Qwen on CUDA/HIP; Qwen on a gfx1200/gfx1201 card; DeepSeek) and the one-sentence effect text.
`strata launch` shows the environment names of the switches that apply as rows of the Advanced table (default "on", the sentence beside them); typing the
off value (`--set STRATA_KVA=0`) writes it into the run config's env, `off` takes it out again. A default launch carries none of them.
tests/terminal/test_speed_switches.py checks the names, the applicability, the config a launch writes, the rows, and that the app's table
(apps/host/server/native-settings.mjs) says the same.
2026-10-26 10:37:47 +01:00
Niko1221 750ca8c3b1 KVA on by default for a cold prompt of 4096+ tokens (Qwen3.8 on one CUDA/HIP device); STRATA_KVA=0 gives the exact 0.2.6 prompt read
The tail follows the draft layer: with KVA on and no --mtp-window given, the draft window becomes 2048 cells and the
fully computed tail 2112 (a given --mtp-window N sets the tail N + 64; a window of every cell, or STRATA_KVA_SUFFIX, is
handled and named). Layer splits, batch slots, --peer-device, pictures, the DFlash2 drafter, other model geometries and
the Metal build read exactly, with the existing "NOT applied: reason" line (quiet for continuation and short reads unless
STRATA_KVA=1). The reasons live in KvaConfig::decline and are unit tested (kva_test). Later chat turns continue from the
live session or a checkpoint and read their new tokens exactly over the cells the first read left; docs/KVA.md says so.
2026-10-26 10:14:06 +01:00
Niko1221 3c8a452900 DeepSeek copy-all: the huge-page arena (DS4_COPY_HUGETLB) is on when the reserved pool already holds the whole copy, and the pin of the copies (DS4_COPY_PIN) is on only when the whole arena is on explicit huge pages; =0 turns either off, an explicit =1 keeps the old opt-in behaviour, no pool means the ordinary arena exactly as before 2026-10-26 10:00:12 +01:00
Niko1221 1f079f9d13 DeepSeek HIP: the matrix-core prompt attention (DS4_FAST_ATTN) is on by default on gfx1200/gfx1201 only; DS4_FAST_ATTN=0 is the off switch, gfx11 and gfx1151 stay scalar unless =1 2026-10-26 10:00:11 +01:00
Niko1221 e5cfa4bbc8 docs: the miss split is the gfx12 start value again (the owner's decision 26 Oct; revert of the opt-in wording) 2026-10-26 09:58:12 +01:00
Niko1221 8d56d41768 gfx12: native packs start the decode miss split at pcie_frac 0.15 (R9700: +9.9% decode at 8K, +25.6% at 32K, +10.4% under serve, first-token KL 0.0013 vs 0.55 with top-1 12/12); explicit --pcie-frac wins, STRATA_PCIE_FRAC_DEFAULT=old restores 0.55 2026-10-26 09:58:11 +01:00
Niko1221 45c9e623e2 STRATA_LOGITS_DUMP: a post-window logits tap for KL quality measurement (off by default); the sampler itself runs inside a CUDA graph capture, so the copy lives in the verify window's host-side epilogue where STRATA_DBG_NAN already copies from 2026-10-26 09:44:39 +01:00
Niko1221 053a41570c KVA: say why a configured run fell back to the full path (the observability criterion: an operator must be able to tell a configured-and-declined run from one that never asked) 2026-10-26 09:44:39 +01:00
Niko1221 5b469ee9a3 KVA: keep gdn_recurrence_variant's public signature - the SYCL tree defines that symbol, so a new parameter would break its link; the state-only form goes through a private impl 2026-10-26 09:44:39 +01:00
Niko1221 4abef6a74e KVA: what the opt-in approximate late-layer prefill does, how to enable and capture, and the measured frontier 2026-10-26 09:44:39 +01:00
Niko1221 c5cf218e54 KVA: state-only GDN recurrence (no gate projection, no output norm) behind STRATA_KVA_GDN_NORM for the A/B 2026-10-26 09:44:39 +01:00
Niko1221 011e5beced KVA: layer-type filter, per-layer transfer skip, stream-entry release on a skipped MoE half 2026-10-26 09:44:38 +01:00
Niko1221 f4012d6369 KVA: optional approximate late-layer prefill (split 24, chunk-aligned suffix, capture/oracle modes) 2026-10-26 09:44:38 +01:00
Niko1221 7dacaf8ea1 Merge f-027-r9700 (Qwen on one R9700: the PLE table kept in RAM when the PC can afford it, four bit-identical gfx12 decode switches back on) into f-027-int 2026-10-26 09:44:24 +01:00
Niko1221 ca7b3d2ca5 Merge f-026-miss (DeepSeek 0.2.7: the RAM store's spare + put, the 12 GB-cell prompt fixes, opt-in matrix-core attention on HIP) into f-027-int 2026-10-26 09:44:16 +01:00
Niko1221 7e1a7cb93c docs: the release notes carry the 12 GB routing default with its held-out evidence 2026-10-26 09:38:45 +01:00
Niko1221 a266bdfe1d markov bias in Q8: the drafter's head is quantised to Q8_0 blocks as it loads (66 MB -> 33 MB, the pack untouched) and read by a Q8 variant of markov_kernel - the markov segment 1.80 -> 1.34 ms, the drafter's round 13.46 -> 12.95, the drafts' acceptance 90-95% (the same band) 2026-10-26 09:35:56 +01:00
Niko1221 e43284572b head: 8 columns a pass for a GGUF head (every window rows<=kWin walks the 129280x5120 head once instead of twice, the same logits per column) - the drafter's fixed T=5 head 4.55 -> 2.83 ms, its round 14.49 -> 13.46; the verify window's head 3.48 -> 2.68 ms 2026-10-26 09:02:28 +01:00
Niko1221 0f66c316ee DeepSeek engine (AMD): the whole chunk's attention slab where the cards hold most of the experts
The slab rule (8 MB of q/o per device, 256 rows here) was set to keep the dense dequant's VRAM small; with a
1024-token chunk it dequantizes q_b/wo once per 256 rows - four times a chunk, 32 times an 8K prompt.
Measured on 4x R9700, 2.25 bpw, share 0.77, chunk 1024, 8K code prompt, ids 5705e1164a:

  256-row rule     1204 / 1236 / 1250 ppts, decode 77.7 tok/s
  chunk's 1024     1324 / 1353 / 1363        decode 78.1        (+9% prefill, decode unchanged)

The 1024 rows cost 268 MB a device (1% of the card).  So high-share runs take min(chunk, 1024) rows and the
old rule stays elsewhere; DS4_ATTN_SLAB still overrides.  The un-enved default verified at 1335 ppts /
77.8 tok/s with the ring at 253 MB.
2026-10-26 08:52:39 +01:00
Niko1221 4aa645c007 DeepSeek engine (AMD): a small staging ring where the cards hold most of the experts
With the 1024-token prompt chunk (the default where the cards hold most of the experts, abfe7d813) a big
staging ring is no longer the trade it was: every MB it takes is a tenth of an expert the decode keeps.
Measured on the same box, 8K code prompt + six 128-token decodes a load, ids 5705e1164a / 81d895366c:

  2000 MB  1243 ppts / 73.2 tok/s
  1000 MB  1287 / 77.5
   500 MB  1314 / 78.5
   256 MB  1324 / 78.4
   128 MB  1338 / 78.3

So high-share runs take clamp(room/device * 0.008, 128, 512) MB (248 MB here) and the old deeper ring stays
for the cases it was measured for (share < 0.5, where the cards stream most of the experts).  The un-enved
default verified at 1339 ppts / 78.1 tok/s, 42 s of summed GPU time in a 36 s pass.
2026-10-26 08:10:37 +01:00
Niko1221 88c2931963 DeepSeek engine (AMD): the small prompt chunk whenever the cards hold most of the experts, 4 cards included
prefill_chunk_default() returned 1024 only for a single-card run (own_default); a multi-card run whose cards
hold most of the experts fell through to 2048 (HIP skips the two 4096 branches) - and the rule's own datapoint
for 1024 was a 4 x R9700 run ('1024 370 tok/s, 8192 214').  Measured again here, 2.25 bpw, 4 cards, 8K code
prompt, --dspark auto, ids 5705e1164a, 4 prompts a load:

  512   1110 / 1137 / 1142 ppts (median 1123)
  1024  1229 / 1257 / 1270        (median 1243)   <- this commit's default
  2048  689 / 695 / 696           (median 692)    <- what the engine picked before
  8192  690 / 690 / 695           (median 690)    <- what the 0.2.7 configs passed

Decode unchanged (73.2 against 73.2 tok/s medians, --dspark auto).  So the owner's default 8K prompt goes from
692 to 1243 ppts (+80%) with no flag; --prefill-chunk still overrides.
2026-10-26 07:24:22 +01:00
Niko1221 85ddf96ffc Merge branch 'main' into f-027-cuda 2026-10-26 06:48:53 +01:00
Niko1221 e83b82afe5 DeepSeek engine (AMD): the drafter's cumulative confidence cut on by default (0.5) on HIP
DeepInfer's rule: verify a draft while the product of the confidences so far stays above the cut, instead of
only each draft's own floor.  The 0.2.7 notes left it off ('not measured faster on code: H100 / owner PC'),
Project Maya runs 0.5 on a Radeon 8065S (+6.5% decode, the same tokens).

Measured on 4x R9700, DeepSeek 2.25 bpw, six 128-token decodes an arm in one load, same ids
(81d895366c): 71.7 -> 76.9 tok/s (medians) with STRATA_DS4_DSPARK_CUM=0.5, and a second pair 74.1/73.6;
the default with the rule on (no env) verified at 74.5 tok/s (6 decodes).  CUDA keeps 0 (the rule was flat
there).  STRATA_DS4_DSPARK_CUM still overrides either way.
2026-10-26 06:09:02 +01:00
Niko1221 2247e5a98d DeepSeek engine (AMD): pin the CPU lane's RAM copy when the cards hold >= half the experts (DS4_COPY_PIN auto)
The prompt path copies every streamed expert from the RAM copy into the pinned staging before its DMA (the
file's own note: 6.8 GB/s through the staging against 24-28 GB/s probes).  Registering the copy lets it DMA
in place.  Three interleaved pairs on 4x R9700, 2.25 bpw (share 0.72): the second 8K prompt of a process
12.0 / 11.9 / 11.9 s against 28.8 / 13.8 / 12.3 s unpinned; decode 35.42 / 35.50 / 34.9-36.0 against
35.43 / 35.45 / 34.9-36.0 tok/s, i.e. no cost; ids unchanged.  Costs 23 s of load (registering a 50 GB
copy) - once a process, which for a server is the right trade.  Auto only when the expert share is >= 0.5
(the CPU lane is then a minority of the decode, so the pinned pages' TLB cost cannot bite); DS4_COPY_PIN=0/1
forces it, and it stays off where the 12 GB cards' decode is CPU-lane-bound (the -15% measured there).
2026-10-26 04:46:46 +01:00
Niko1221 5b969c1f7a DeepSeek engine (AMD, diagnostic): the io threads' busy time in the prompt stream line
The RAM->staging memcpy runs on the IoQueue's threads; on a warm 8K prompt (stream stage 14.3 s, 12.8 GB a
device) they are busy 4.1 s, i.e. ~3 GB/s a device - a real share, and the thing to check in the 50-95 s
first-prompt runs (where the same counters are what to read first).  DS4_PROF only.
2026-10-26 04:30:12 +01:00
Niko1221 3cc132ec51 DeepSeek engine (AMD, diagnostic): where copy_ahead's time goes in the prompt stream line (DS4_PROF only)
The first prompt after a load is 2-9x slower than the second on the 4x R9700 2.25 bpw cell and it is not
the disk (0 MB read during it), the RAM store (removing it is worse), the dense GEMM shapes (q_b+rope
1.46 s) or the compute waiting (0 ms).  The lead is the RAM->pinned-staging->DMA path (the file's own note:
6.8 GB/s through the staging against 24-28 GB/s probes); DS4_COPY_PIN=1 measured 47.9 -> 31.4 s on the
first prompt and 28.8 -> 12.0 s on the second, decode unchanged.  This commit only adds the counters
(staging / DMA issue / slot wait) to the existing prompt stream line under DS4_PROF; no behaviour change.
2026-10-26 04:07:19 +01:00
Niko1221 3b23994568 DeepSeek engine (AMD): the staging ring from each device's room (not the summed room)
No behaviour change on the 4x R9700 cell (126.8 GB of room / 4 = 31.7 GB a device, 8% -> 2000 MB, the value
measured in 76177ec34); on other card counts it is the per-device share that was meant.
2026-10-26 03:36:03 +01:00
Niko1221 1e72cdc4b9 DeepSeek engine (AMD): a staging ring sized from the VRAM room, and expert-tile knobs for the MarLin work
The prompt path streams the experts the cards cannot hold.  ring_mb_auto() gave 16 MB whenever the cards
held >= 1/1.3 of the experts, which is exactly this 2.25 bpw cell (126.8 GB of room, 156.3 GB of experts,
share 0.769): the stream then had no depth and the expert tiles waited 1.7 s inside a 5.2 s stage.  A share
of the VRAM room (5%, 400-2000 MB) when any expert spills, 16 MB when they all fit (the 0.2.6 rule, kept
for the decode it was chosen for).

Measured, 4x R9700, 2.25 plain pack, 6 experts, --dspark off, 8192-token code prompt, ids 5705e1164a:
8K warm 632 -> 675-678 ppts (+6.8%, 5 prompts a run, 3 runs), agent decode 35.9 -> 35.3 tok/s (-1.7%,
4 runs).  The profile of the warm pass: expert stage 5185 -> 4034 ms (kernels 3509 -> 2645 ms, the same
kernels now that the weights arrive in VRAM ahead of them, waits 1676 -> 1389 ms), everything else equal.

Also in this commit (opt-in, defaults byte-identical except the F16 activation path which is bit-identical):
- SetArgs::x16: gate/up take the layer's F16 copy of the activations (rows_f16 rounds exactly as the tile's
  own smem store did) - half the activation bytes, 26% faster gate/up in the standalone bench
  (box/expert_bench.cpp), +0.3% at the pass level (the stage is stream-bound).
- HIPK_CFG 6..11: tile variants for the expert GEMMs (64x128, 32x128, 128x256, 64x128/4 waves,
  128/128+down 256, 64x256).  64x128 (cfg 6) is 21% faster than 128x128 in the bench and +0.9% in the pass.
- HIPK_DBG 1/2/3: switch the MMA / the decode off in the tiles (the breakdown in 09-marlin-kernel-diary.md).
- ds4_iq_hip.inc: a UTF-8 BOM on a source file removed.
2026-10-26 03:01:27 +01:00
Niko1221 81ac4dd83b setup: a 16,384-token prompt chunk on a unified-memory PC whose pool holds ~128 GB (16K prompt 152.2 -> 158.6 tok/s, 32K 174.0 -> 179.7, 8K flat, decode unchanged); smaller unified PCs keep 8,192 2026-10-26 01:52:05 +01:00
Niko1221 44b88bc91e docs: the release notes carry the residency-biased routing default
It now ships on at 2.0 for a card of 13 GiB or less: +37.6% story / +35.2% code
decode over five interleaved pairs on an RTX 3060 12 GB, and it stays off on
bigger cards where it measured neutral to negative (l620 -1.9%/-0.5%, z440
-4.2%/+11.0%).  It changes which experts run - the owner approved that trade
for 12 GB cards - and STRATA_ROUTE_PRIOR=0 turns it off.
2026-10-26 01:01:09 +01:00
Niko1221 2395c8a53e hc: the one-kernel persistent hyper-connection read builds and runs on CUDA
The design was HIP-only: 80 resident blocks, a column slice of D each, two
grid barriers on an atomic counter, a barrier timeout so a card that cannot
hold all 80 blocks resident turns a hang into a refused start-up check.  Ported
by giving grid_sync a CUDA branch (volatile generation counter + clock64() for
the timeout, scaled to SM clocks), guarding the gfx-arch refusal to HIP and
adding a masked-shuffle helper.

On the RTX PRO 5000 Blackwell 72 GB it passes its own check - '80 blocks on 110
multiprocessors, 0 token counts timed out, max relative difference 1.8e-07
against the default read' - and measurably does NOT win: story 201.4 -> 190.6
tok/s (-5.4%), code 245.6 -> 249.6 (+1.6%) over 3 interleaved pairs.  That
matches the AMD note in the same file (an R9700 measured it neutral to 1%
slower), so it stays opt-in (STRATA_HC_PERSIST=1) and is not a default.  The
port is kept as the starting point for a CUDA-tuned block mapping.
2026-10-26 00:47:21 +01:00
Niko1221 c30656aaf9 route: residency-biased routing is on by default at 2.0 on a card of 13 GiB or less
The router's top-10 is picked again with a logit bonus for the experts already
in VRAM (route_prior.cu); the weights stay the router's.  Measured on an RTX
3060 12 GB, IQ3_XXS, 5 interleaved pairs of fresh engines: story 44.2 -> 60.9
tok/s (+37.6%), code 47.3 -> 64.0 (+35.2%); =1.0 gives +22.6% / +24.6%.  Three
more pairs against the switch itself read +23.6% / +36.9%.

It changes which experts run, so it is a default only where the expert cache
cannot hold the model, and the owner approved that trade for 12 GB cards.  It
is NOT a default on a bigger card: on an RTX PRO 5000 72 GB it measures
-1.9% / -0.5% and on 2x RTX A4000 -4.2% / +11.0% (3 pairs each); both were
re-run after this change and are unchanged (201.7 / 246.9 and 87.1 / 107.1
tok/s).  STRATA_ROUTE_PRIOR=0 turns it off and any explicit value wins.
2026-10-26 00:27:14 +01:00
Niko1221 219beb5736 DeepSeek engine: porting guards for the 12 GB work. The matrix-core attention and its dispatch are now inside #if defined(STRATA_USE_HIP) (a CUDA/sm120 build keeps the scalar path and compiles), and a failed DS4_COPY_HUGETLB allocation falls back to the ordinary arena with a warning instead of leaving that layer's experts in place (the flag is safe on a machine with no huge-page pool). HIP build verified: 8K warm 768.7 / 767.4 / 765.6 ppts with DS4_FAST_ATTN=1, ids 5705e1164a, the agent decode's 81d895366c. 2026-10-26 00:21:01 +01:00
Niko1221 32c686fa67 DeepSeek engine: the matrix-core attention's fragments load two halves a word (the scores' Q/K tiles and the values' P tile), and the online softmax runs a warp per two heads with a lane a key (16-lane shuffle max/sum) instead of a thread a head walking 16 keys. R9700 12 GB cell, 2.25 pack, 8K warm with DS4_FAST_ATTN=1: 767.7 / 765.8 ppts (10.67/10.70 s), the sparse attention stage 2516 -> 2311 ms, first prompt 465.6, with the same ids (5705e1164a, the agent decode's 81d895366c) and the same decode speed (16.49 tok/s). Quality gate on this binary (4995-position score, 2.25 plain, ref_base1): KL 0.10776 / ppl 3.389 / top1 87.89% against the scalar kernel's 0.10746 / 3.395 / 87.81% (delta 0.0003; the gate is 0.006). The softmax's sum order differs from the previous matrix-core build (a shuffle tree, not a key walk). 2026-10-25 23:43:12 +01:00
Niko1221 f05ccd3b54 DeepSeek engine: DS4_FAST_ATTN=1 runs the chunked prompt's packed-KV attention on the gfx11/gfx12 matrix cores (new sparse_attn_wmma_kernel, opt-in; the decode and verify paths stay on the scalar kernels). The matrix's rows are 16 heads of ONE query, so a block stages a 16-key tile once for all of them: Q (16 heads x 512) is the A operand of the scores, the key tile is B, and the same tile is B of the values' product; the scores' 64-dim slices are computed by the 8 warps and summed in shared, the values by a warp a 64-dim slice, the online softmax a head a thread. R9700 12 GB cell, 2.25 pack, 8K warm: 752.2 / 748.3 ppts (10.89/10.95 s) against 694.2 / 693.0 / 691.2 for the scalar kernel; the sparse attention stage 3750 -> 2516 ms; first prompt 484.8. Quality gate (4995-position score, 2.25 plain, ref_base1): KL 0.10750 / ppl 3.394 / top1 87.83% against the scalar kernel's 0.10746 / 3.395 / 87.81% (delta 0.00004; the gate is 0.006). The ids are the same (5705e1164a) and the 128-token agent decode's ids too (81d895366c); its decode speed unchanged (16.49 vs 16.87 tok/s). The QK/PV sums are the matrix cores' order, hence opt-in (the owner approved the trade for the prefill). 2026-10-25 23:29:29 +01:00
Niko1221 2dde837d9f DeepSeek engine: DS4_COPY_HUGETLB=1 puts the CPU lane's RAM copy on explicit 2 MB pages (MAP_HUGETLB from the kernel pool; the copy's RAM budget adds the pool, which MemAvailable does not count). With DS4_COPY_PIN=1 the pin then keeps 2 MB pages: on a transparent-huge-page arena the registration pins 4 KB pages and the prompt's DMA from it swings between 11.9 and 30.4 s for the same 140.2 GB stream. R9700 12 GB cell, 2.25 pack, 8K, 3 warm prompts: huge-page arena + pin + DS4_RING_MB=3500 + DS4_ATTN_SLAB=2048 -> 694.2 / 693.0 / 691.2 ppts (11.80-11.82 s), first prompt 435.4 (18.8 s), ids identical (5705e1164a); the THP arena with the pin ran 691/573/270 in one load and 665-691 in another. Copy at load 33.3 s for 156.7 GB; the pool is reserved once per boot (sysctl vm.nr_hugepages=78000). Opt-in: default path unchanged; no-pin staged stays 651. 2026-10-25 22:27:30 +01:00
Niko1221 bc6a39217a docs: 0.2.7 release notes draft from the CUDA speed work
A draft for the owner: the one engine default that changed (the wide
hyper-connection up read on a 12.0 card, +1.9% story / +0.7% code decode on
the RTX PRO 5000, 5 pairs, bitwise), the four settings that are faster where
the PC supports them (PLE in RAM, the English draft vocabulary, --prefill-pipe
on a two-card split, q4_0 KV on 12 GB with its precision cost), and the list
of what was measured and rejected so it is not retried.  No version number is
claimed: the owner decides what else 0.2.7 carries.
2026-10-25 21:26:06 +01:00
Niko1221 d39e2a7539 sm_120: the wide hyper-connection up read is on by default
STRATA_GR_UP_WIDE (one warp a row, N/2 = 1,280 blocks instead of 160)
measured on the RTX PRO 5000 Blackwell 72 GB, IQ3_S, every expert in VRAM,
5 interleaved pairs of fresh engines: story 197.6 -> 201.3 tok/s (+1.9%),
code 243.5 -> 245.1 (+0.7%).  A third run against the switch (default on vs
STRATA_GR_UP_WIDE=0) reads +1.7% / +0.8%.  The produced token ids were
identical in all 6 pairs, and fused_gr_bench's per-T hashes are the plain
read's.  The switch stays: =0 off, =1 on any card, STRATA_SM120_PERF=0 the
whole 12.0 set.  Other cards are untouched.
2026-10-25 21:21:28 +01:00
Niko1221 2465b93708 docs: the miss split is opt-in, not a default: the text can differ (4 of 6 fixed prompts within 64 greedy tokens) even though the first-token KL is 0.0013 2026-10-25 21:03:52 +01:00
Niko1221 1d00c018e1 Revert "gfx12: native packs start the decode miss split at pcie_frac 0.15 (R9700: +9.9% decode at 8K, +25.6% at 32K, +10.4% under serve, first-token KL 0.0013 vs 0.55 with top-1 12/12); explicit --pcie-frac wins, STRATA_PCIE_FRAC_DEFAULT=old restores 0.55"
This reverts commit 550c6ddbf90d57a0d018878115f6d0f9fa86cb74.
2026-10-25 21:03:43 +01:00
Niko1221 1f00c1825e Merge f-027-async: synchronous verify window on Strix Halo (+31% code decode)
setup writes DS4_ASYNC_T=4 into the config env for an integrated Radeon: the engine's mailbox path is sized for cards that hold
most of the experts and costs this box 31% on code (15.9-16.3 -> 20.1-21.3 tok/s), 16% on agent with identical accepted tokens,
9% on chat; the 2.25 set (no drafter) is unchanged.  The same tokens come out.  Discrete cards keep the engine default.
2026-10-25 20:53:56 +01:00
Niko1221 63b8a8d3cc Strix Halo: run the verify window synchronously (DS4_ASYNC_T=4 in the config env), +31% code decode
The engine's windowed decode uses the mailbox ("async") path for windows up to DS4_ASYNC_T (8 by default). That default was sized
for cards that hold most of the experts; on the 128 GB gfx1151 box the 6-row verify window is faster on the synchronous path.
Measured 2026-10-25 with the 2.00 bpw set and the shipped 0.1.51 engine (greedy, 256-token sets, three interleaved reps):

- code 15.87-16.33 -> 20.14-21.28 tok/s (+31%);
- agent, with identical accepted tokens a round (5.07): 15.93 -> 18.48 (+16%);
- chat 10.33 -> 11.30 (+9%);
- 2.25 code unchanged (11.2 -> 10.9): the drafter is unloaded there, so the extra syncs have nothing to hide.

The same tokens come out; only the scheduling changes, and the drafter's own single-token windows (T=1) stay on the async path.
dsv41_config_env(cards) writes DS4_ASYNC_T=4 into the generated config for an integrated Radeon only (a discrete card keeps the
engine default); STRATA_DSV41_ASYNC_T=n overrides the value and =8 restores the default. Tests:
tests/terminal/test_dsv41_async_t.py (5 cases). No engine rebuild; the signed dsv41 package is unchanged.

Docs: the STRIX_HALO and DEEPSEEK41 measured tables and the README row now read 19-21 tok/s code, 18 agent, 11 chat.
2026-10-25 20:53:52 +01:00
Niko1221 3a26753027 STRATA_TRACE: one clock for the whole process, so a layer split's stages compare
The trace origin was thread-local, and the prompt's later stages run on their
own thread, so their timestamps could not be compared with the first stage's.
One process-wide origin makes the hand-off visible.

Found with it on z440 (Q2_0, split 24, 16,383-token prompt, 8K chunks): stage
0 starts at 0 ms and 3,267 ms, stage 1 at 3,268 ms and 5,996 ms, read ends at
8,775 ms - the two stages do overlap chunk-for-chunk, and each takes ~2.7 s
per 8K chunk for its 24 layers.  A split 30 run leaves stage 1 at 3.09 s a
chunk with 18 layers, so the per-chunk cost is not its layer count.
2026-10-25 20:52:26 +01:00
Niko1221 457d8cc791 DeepSeek engine: DS4_ATTN_SLAB=N sets the chunked prompt's attention slab rows (default unchanged: 8 MB / HQ rounded to 64). The slab loop runs the dense projections (q_b, wo_a, wo_b) per slab, and each call dequantizes the whole dense matrix: a 256-row slab dequantized the same weights 32 times for an 8K prompt (the dense dequant 916 ms and rocBLAS 867 ms of that prompt). R9700 12 GB cell, 2.25 pack, 8K warm: slab 256 -> 591.9 ppts (with the earlier fixes), slab 2048 + DS4_RING_MB=1600 -> 668.0/666.2 ppts (12.26/12.30 s), the first prompt 429.9 (from 143-402), ids identical (5705e1164a); q_b+rope 3.97 -> 1.15 s. Opt-in: the default slab is unchanged. 2026-10-25 20:45:17 +01:00
Niko1221 14d076ee2e STRATA_TRACE: the prompt chunk line carries its stage range and elapsed ms
Same diagnostic as the request-step timestamps: with a layer split the chunk
line now says which stage is starting a chunk and when, so a prompt's stage
overlap can be read off the log.  STRATA_TRACE only; no behaviour change.

Measured with it on z440 (Q2_0, split 24, 16,383-token prompt): serial reads
run stage 0's two 8,192-token chunks 3,332 ms apart and the whole read takes
8,788 ms; --prefill-pipe 1500 reads four 4,096-token chunks 1,582 ms apart and
the read takes 8,375 ms, so the stages overlap only partially at best.
2026-10-25 20:15:58 +01:00
Niko1221 13c4aebeca docs: the miss split is the gfx12 start value now (measured A/B on the shipped path: 8K +9.0%, 32K +23.6% decode) 2026-10-25 20:08:41 +01:00
Niko1221 d7ad4a2762 gfx12: native packs start the decode miss split at pcie_frac 0.15 (R9700: +9.9% decode at 8K, +25.6% at 32K, +10.4% under serve, first-token KL 0.0013 vs 0.55 with top-1 12/12); explicit --pcie-frac wins, STRATA_PCIE_FRAC_DEFAULT=old restores 0.55 2026-10-25 19:57:35 +01:00
Niko1221 8abe826f70 DeepSeek engine: the chunked prompt's dense dequantizer writes its 32 values a thread through shared memory, coalesced (a warp covers 1024 contiguous values), instead of each thread writing 2 bytes every 64 - a warp's store touched 32 cache lines. Same arithmetic and the same F16/BF16 values (only the store order changes). R9700 12 GB cell, 2.25 pack, 8K warm: prompt 14474 -> 13840 ms (566 -> 591.9 ppts), the q_b+rope stage 4025 -> 3581 ms, ids identical (5705e1164a); the cold first prompt also uses it. DS4_DQ_OLD=1 keeps the old kernel for A/B. 2026-10-25 19:43:51 +01:00
Niko1221 7901bc5137 docs: RDNA4 decode miss split measured on the R9700 (--pcie-frac 0.15: +9.9% at 8K, +25.6% at 32K, first-token KL 0.0013, top-1 12/12) 2026-10-25 19:15:53 +01:00
Niko1221 91d4d7ff86 STRATA_TRACE: print elapsed milliseconds on each request step
The trace lines named the step a request reached but not how long it took to
get there, so a stall between two marks was invisible.  Each line now carries
the time since the first traced step (thread-local).  A diagnostic only: no
step, no order and no output changes without STRATA_TRACE set.

Found with it on z440 (Q2_0, layer split 24, 16,383-token prompt): the batched
read takes 8,758 ms while stage 0's own GPU timeline is 5,374 ms - stage 1 is
the slow one - and the expert-cache refill after the prompt costs a further
592 ms of the request.
2026-10-25 18:56:26 +01:00
Niko1221 2b5cadd984 Merge f-027-perf: 8,192-token prompt chunk on Strix Halo (11-15% faster prompts)
Measured on the 128 GB box with the 2.00 bpw set: 4K prompt 78.7 -> 87.2 tok/s, 8K 108.9 -> 125.1, 32K 155.2 -> 176.9;
decode unchanged (16.53 -> 16.59).  setup writes --prefill-chunk 8192 only for an integrated Radeon whose whole pool is at
least 64 GB; the escape hatch is STRATA_DSV41_NO_BIG_CHUNK=1.  Also repairs UTF-8 damage in three docs from an earlier
PowerShell edit.
2026-10-25 18:54:28 +01:00
Niko1221 e7e48be2c9 Strix Halo (gfx1151): an 8,192-token prompt chunk on the unified-memory APU (11-15% faster prompts)
The engine's prompt-chunk rule returns 1,024 when a single card holds half or more of the experts; it was calibrated on
cards that hold most of them (4 x R9700).  On the 128 GB Strix Halo (share 0.62, one shared pool) the bigger chunk wins:
measured with the 2.00 bpw set, engine 0.1.51, 2026-10-25: 4K prompt 78.7 -> 87.2 tok/s, 8K 108.9 -> 125.1, 32K 155.2 ->
176.9.  Decode is unchanged (16.53 -> 16.59): the chunk buffers take ~5 GB from the GPU expert tier (81.9 -> 76.6 GB) and the
RAM store's hit rate rises (68.6% -> 76.8%) to compensate.

dsv41_prefill_chunk_args(cards, ram) writes --prefill-chunk 8192 for an integrated Radeon only, and only when the whole pool
(the RAM Windows sees plus the BIOS carve-out) is at least 64 GB; a discrete card or a smaller unified PC keeps the engine's
choice, and STRATA_DSV41_NO_BIG_CHUNK=1 turns it off.  The chunk changes the prompt GEMMs' summation order, not the model: the
repo's chunk-size KL check reads 1024 0.1705 / 2048 0.1692 / 4096 0.1688 / 8192 0.1695.  Test:
tests/terminal/test_dsv41_prefill_chunk.py (5 cases).

Also repairs UTF-8 damage in README.md, docs/STRIX_HALO.md and docs/MODELS.md from an earlier PowerShell edit (the middots,
em dashes and arrows had been double-encoded; no text changed).  No engine rebuild is needed: the existing signed dsv41 package
accepts the flag.
2026-10-25 18:54:22 +01:00
Niko1221 00a1292531 DeepSeek engine: the post-prompt RAM-store preload skips the experts the CPU lane already holds as a RAM copy (copy_mode). In copy-all mode every routed expert is in that copy, so the preload read a second copy of the same bytes: 4152 experts, 40 GB at 6.4 GB/s, 6.2 s of every 8K prompt, with 0.0 store hits in the decode after it; with --mmap-experts off and the experts not all fitting, the in-place ones are still preloaded. R9700 12 GB cell, 2.25 pack, 8K: prompt 27918 -> 21567 ms (293 -> 380 ppts; a repeated prompt 526.6), decode 16.61 -> 16.82 tok/s, ids identical (5705e1164a). With DS4_COPY_PIN=1 the same run is 21347 ms (383.7 ppts, repeat 535.6). 2026-10-25 18:45:40 +01:00
Niko1221 4be0a7ce4d DeepSeek engine: DS4_COPY_PIN=1 DMAs the prompt's experts from the CPU lane's copies in place (cudaHostRegister, read-only with DS4_COPY_PIN_RO=1), so the flow does not copy each one to the pinned staging first; the pinned chunks are unregistered with the arena. R9700 1 card, 2.25 pack, 8K prompt: the stream's compute wait 5517 -> 0 ms, prompt 28035 -> 27392 ms (292 -> 299 tok/s), ids identical; decode unchanged (16.6 vs 16.8 tok/s). The stream itself stays ~7 GB/s (the DMA issue pattern, not the staging), so the gain is small here; on an SSD-bound box the removed wait is the point. Opt-in: default path unchanged. 2026-10-25 18:05:54 +01:00
Niko1221 1d380fad87 arch_defaults_test: the gfx12 table is nine switches now (five prompt, four decode); check the apply path against a clean environment 2026-10-25 17:47:24 +01:00
Niko1221 d18d8ba045 CUDA HC: the wide up read, one warp per row, opt-in (STRATA_GR_UP_WIDE=1)
The up projection of the hyper-connection read runs 160 blocks of 8 warps
(each warp takes 8 rows) on a 12.0 card, so 160 blocks of 192 SMs and ~6.7
warps an SM: the read is latency-bound at about 0.5 TB/s.  This kernel takes
one row per warp and 2 columns x the 4 streams a block, so the grid is
N/2 = 1280 blocks (53 warps an SM).  Every row's dot, xor tree, epilogue and
the cross-stream sum are gr_up_multi_kernel's, in its order, so the outputs
are bitwise the same; the token streams of both arms were identical in all
6 pairs measured.

Measured on l620 (RTX PRO 5000 72 GB, all experts in VRAM, IQ3_S, 3
interleaved pairs, fresh engine each): story 197.4 -> 201.3 tok/s (+1.9%),
code 243.0 -> 244.7 (+0.7%).  Off by default (STRATA_GR_UP_WIDE unset); the
q8_1 tail of S26 needs the 16-column blocks, so the wide kernel is only used
when no q8_1 image is written.
2026-10-25 17:39:28 +01:00
Niko1221 3fd5a748c7 gfx12: the four bit-identical decode switches back on (ATTN_LANECELL, ATTN_MERGE_V2, TSUM, EXPDEC), measured on p620 R9700 IQ3_S 8K: +3.5% decode, 3/3 pairs, answer md5 identical; docs: the PLE residency policy and its numbers; ple_reader_test: table_bytes 2026-10-25 17:34:42 +01:00
Niko1221 3f26ca5c2f Merge f-027-strix: DeepSeek V4.1-Flash on Ryzen AI Max (Strix Halo, gfx1151) on Windows
The Windows AMD DeepSeek engine (feed member dsv41, windows-x86_64-hip-rdna3, gfx1151) ships with these
changes: the 0.2.6 copy-all rule is off on an integrated GPU (it copied 53 GB into RAM beside the GPU's 82 GB
in one 128 GB pool and an 8K prompt stalled; the in-place path is the 0.2.5 soak path), a Windows HIP package's
rocm/bin is on the engine's library path (setup, run config, app), the unified-memory fit plan counts the BIOS
carve-out, and --feed reaches the DeepSeek engine download.  Docs, app text and tests included.

Measured on the Strix box (2.00 bpw, Faster, DSpark auto): 4K prompt 78.7 tok/s, 8K 108.9, code 17.6 tok/s,
chat 11.0.  Clean-user end-to-end from a signed feed: install, download, pack, smoke, serve, chat, tool call,
agent round trip: 5/5.
2026-10-25 17:20:42 +01:00
Niko1221 a6543d6a7f setup: accept --feed (the DeepSeek engine's package comes from it)
strata launch --feed passed the feed to setup, but setup's parser had no --feed (getattr(a, 'feed', None)
was always None), so the flag was refused with 'Unrecognized arguments' and a local or non-default feed could
never serve the DeepSeek engine (found by the clean-user test on strix, 2026-10-25).  It is now the same
option strata update has: a URL or a folder.
2026-10-25 17:06:43 +01:00
Niko1221 932bda54bd Strix Halo: the DeepSeek engine follows --feed, and the unified plan counts the BIOS carve-out
Found by the clean-user test on strix: 'strata launch --feed DIR' read the local feed for its own engine
check and then setup went to GitHub for the DeepSeek package, so a local (or any non-default) feed never
served the DeepSeek engine.  setup's get_dsv41_engine/upgrade_dsv41_engine take the feed, install_dsv41
passes --feed, and runner.setup_args hands --feed to setup (a test in test_launch_dsv41).

The unified-memory plan counted only the RAM Windows sees (64 GB on the owner's box) and ignored the 64 GB
BIOS carve-out the GPU owns: 128 GB in one pool.  uma_total_gb() adds the carve-out (or the GPU figure less
its shared part), so the plan now says about 109 GB of the 2.00 set's 132 GB stay in memory and 23 GB are read
from the SSD; the docs and the test fixtures use the 128 GB box.  The 'measured' text says 128 GB unified too.
2026-10-25 17:00:58 +01:00
Niko1221 3b34b6c7b2 Engine: keep the PLE table resident in RAM when the PC can afford it (opt-out STRATA_PLE_AUTO_RAM=0); measured on p620 R9700 IQ3_S: 8K prompt PLE 845 -> 132 ms, 2125 -> 2611 tok/s, answer md5 identical 2026-10-25 16:42:05 +01:00
Niko1221 6c70d4bc5d Strix Halo (gfx1151) on Windows: DeepSeek V4.1-Flash support and the 0.2.6 shared-memory fix
The Windows AMD DeepSeek engine package (feed member dsv41, windows-x86_64-hip-rdna3, arch gfx1151) is built from this
branch and installed as its own package; these are the code changes it needs.

- ds4_model.cu: on an integrated GPU the CPU lane's experts are no longer copied into RAM by the auto rule.  The GPU's
  expert tier and that copy are the same shared memory: on the 64 GB Strix Halo the GPU held 82 GB of experts, the rule
  saw 62 GB 'free' and copied 53 GB, and an 8K prompt did not finish in 11 minutes.  Found 2026-10-25 on strix; the
  corrected build does the same prompt at 108.9 tok/s (4K 78.7), code answers 17.6 tok/s, chat 11.0.  DS4_COPY_ALL=1
  still forces the copy for a test.
- dsv41_engine.py: lib_dirs()/start_env() put the package's BUILD.json lib_dirs on PATH/LD_LIBRARY_PATH (a Windows HIP
  package keeps hipBLAS/rocBLAS in rocm/bin, or a clean PC fails with 0xC0000135); the upgrade test-start uses it.
- setup.py: the dsv41 engine's library dirs come from the package (so the run config carries rocm/bin), the unsupported-card
  text names Ryzen AI Max on Windows, and the manual package hint names the .tar.gz the release page actually has.
- runner.py: the same library dirs for the app/Terminal path.
- dsv41_fit.py: a unified-memory plan for the official IST-DAS sets (one pool, what stays in RAM, what is read from the SSD,
  measured 14-16 tok/s on code, 10-11 on chat, 4K 78 / 8K 109), never a refusal; tools/strata_mcp.py uses it for the app
  catalog's on-this-PC line.
- Docs: STRIX_HALO.md (DeepSeek section with speeds, driver 26.10.1, memory behaviour, limits), DEEPSEEK41.md, MODELS.md,
  AI_REFERENCE.md, README.md, packaging/README.md (the Windows AMD package recipe).
- Tests: tests/terminal/test_dsv41_fit.py (7) and 4 new tests in test_dsv41_engine.py (lib dirs, feed selection for gfx1151).
2026-10-25 16:41:49 +01:00
Niko1221 4b9e05a2a8 Windows uninstaller: remove the app's own folders (control, host, node, harness) recursively; files the app wrote there while it ran (Python bytecode caches under control) are not in the installer's file list, so those folders and the program folder stayed behind after use (found on strix with a standard user after the process fix); test 2026-10-25 13:56:36 +01:00
Niko1221 1b667c7abb WIP not-default: DS4_NO_WAIT_SPLIT (no commit at the routing wait; A/B unfinished) and DS4_PHASES (decode host phase timers) 2026-10-25 12:44:45 +01:00
Niko1221 af197f96db Windows uninstaller: find Strata's own running processes with the 64-bit PowerShell (the installer is 32-bit, whose Get-Process cannot read a 64-bit process's path) and with Get-Process first, WMI only as a second source: a standard user's WMI query was denied, so the host kept the folder open and it stayed behind (found on strix with a standard user) 2026-10-25 12:32:41 +01:00
Niko1221 4acede655a DS4_PLACE_HASH: one hash of which experts hold a VRAM slot (the placement changes with the saved profile) 2026-10-25 12:23:42 +01:00
Niko1221 dfaae03b00 DeepSeek engine: the RAM store no longer misses the experts the VRAM cache gives back to the CPU lane: the preload fills the slots the lane leaves over (and the 256 reserved ones, evictable first) with the VRAM experts the profile counts least and the lane experts it never saw (DS4_STORE_SPARE=0 off), and an expert leaving VRAM is copied into a store slot from its VRAM blob (put_begin/put_done, DS4_STORE_PUT=0 off) instead of being read from the SSD while the lane waits. r9700 1 card 2.00, store forced (--ssd-cache 140): decode code 23.7 -> 26.2, agent 20.7 -> 24.6 tok/s, misses 1.5/2.6 -> 0.5/0.6 a token; P2003 RTX 3060: no loss, ids identical 2026-10-25 11:54:42 +01:00
Niko1221 53548c8af4 Merge main (41cf05a06: README model order, docs only) into f-026-rel 2026-10-25 11:37:25 +01:00
Niko1221 41cf05a06a README (all 7 languages): models in the order Qwen3.8-Flash-Next, Qwen3.6-35B-A3B, DeepSeek V4.1-Flash 2026-10-25 11:35:36 +01:00
Niko1221 4f0f933941 Merge main (ed4b4db40: README, docs and icons) into f-026-rel; AGENTS.md keeps main's text with Version 0.2.6, engine 0.1.51; the installer names in the READMEs and docs are the 0.2.6 ones 2026-10-25 11:29:12 +01:00
Niko1221 ed4b4db405 docs and packaging: the owner's 2026-10-23 guide set brought in for 0.2.5 (one installer per system, engine downloaded on first start, DeepSeek in the app and the Terminal with three sets and the Speed choice, install.sh form kept, versions 0.2.5 and 0.1.50, source setup labelled as the developer path) 2026-10-25 11:28:02 +01:00
Niko1221 af8c6f8ec7 README translations (zh-CN, ja, de, fr, es, pt-BR) rebuilt in the structure of the new README; the README links source setup to SOURCE_INSTALL.md and names the three 0.2.5 installers 2026-10-25 11:28:01 +01:00
Niko1221 e99c4d2d37 AGENTS.md and AI_SETUP.md: lead an agent to the installer, install.sh/install.ps1 and strata launch --plan then --yes --no-tui; source setup only as the fallback; provider order ISTA-DASLab, Unsloth, Gyro, others 2026-10-25 11:28:01 +01:00
Niko1221 b8b4ca0e32 docs: add the guide index, the source-install page, the AI reference and the media kit (the owner's pages, updated to 0.2.5) 2026-10-25 11:28:01 +01:00
Niko1221 562a0c2ed7 Version 0.2.6: the Qwen engine packages and Metal stay 0.1.50 (no C++ change since 0.2.5: only the DeepSeek engine and a generate.cpp comment changed); Control and the DeepSeek engine are 0.1.51 as the CMake project version, the shape of 0.2.4; build_hip_linux.sh keeps PKGVER 0.1.50 2026-10-25 11:21:36 +01:00
Niko1221 79ec8e2786 Host test: the source check for 'a version that is not downloaded starts with Read pictures on' accepts the braced else that the DeepSeek 8K context fix introduced (test only) 2026-10-25 11:16:25 +01:00
Niko1221 b5d41babfa README: the owner's layout (model dropdowns with Qwen and DeepSeek marks, provider and set tables, short install, hardware and tools tables), updated for 0.2.5: DeepSeek in the app and the Terminal with three sets and the free disk each needs, 0.2.5 speeds, one installer per system; model marks from LobeHub Icons (MIT) 2026-10-25 11:14:21 +01:00
Niko1221 27a8ed96b6 Version 0.2.6: Terminal and app 0.2.6, engine and Control 0.1.51 (every Qwen and DeepSeek package, Metal included) 2026-10-25 11:14:16 +01:00
Niko1221 8aee4538f2 generate.cpp: the --spec-split comment no longer calls its windows exact (they are not bit-exact against no-split: batch-shape divergence); comment only 2026-10-25 11:14:00 +01:00
Niko1221 e9e22db6a7 Setup: the DeepSeek engine step does not stop on 'can't reach GitHub' when the update feed is a local one (STRATA_UPDATE_FEED or --feed naming a folder, file or loopback address); a transient GitHub timeout no longer fails a verify run; tests 2026-10-25 11:13:51 +01:00
Niko1221 7025a99799 WSL on AMD: setup and launch say plainly that the AMD (HIP) engines do not run under WSL yet (no /dev/kfd, the GPU bridge never finishes a command) and name the Windows app or Terminal as the next step; a warning, nothing is refused; tests 2026-10-25 11:12:59 +01:00
Niko1221 36c6dd9ef5 Windows uninstaller: stop the app, host, node and engine that run from the install folder before files are removed, so the program folder no longer stays behind after use; only processes whose image path is inside the install folder; test with a real process 2026-10-25 11:11:37 +01:00
Niko1221 4a6a5fb699 Merge f-026-copyall (DeepSeek copy-all CPU-lane experts) into f-026-rel 2026-10-25 11:10:16 +01:00
Niko1221 d51d71e7c0 Merge f-025-fix (6 owner-QA fixes) into f-026-rel 2026-10-25 11:10:13 +01:00
Niko1221 fa663fabc5 Merge f-0243-rel (721c6f7ff) into f-025-readme 2026-10-25 10:41:20 +01:00
Niko1221 1ece7efd5c metal: no command-buffer split after the fp8-rounding finishes (decode +22%: code 11.9 -> 14.6, agent 9.7 -> 12.0 tok/s); DS4_FINISH_SPLIT=1 restores it 2026-10-25 10:38:07 +01:00
Niko1221 721c6f7ff5 Mac: the engine bundle inside a signed .app is not removed after the first start (deleting it broke the app's signature); outside an .app and on other systems the cleanup is unchanged; test 2026-10-25 10:37:21 +01:00
Niko1221 025cf7d954 DeepSeek engine: the copy-all fit check is one tested rule (copy_all_fits) and also needs the Windows commit room (RAM + page file beside its margin), not only the free RAM, so a 64 GB PC with a 12 GB card, or a big-RAM PC with a small page file, never copies the lane in by itself; the auto store says (verbose) when it holds less than the CPU lane 2026-10-25 10:18:53 +01:00
Niko1221 5d5c26d5bc Monitor/serve: a one-token reply has no decode span, so no tok/s is shown for it (a 1-token probe read 1,250 tok/s); test 2026-10-25 09:29:40 +01:00
Niko1221 8a3ca07c4e serve: a chat request with no message is a 400 (messages must be a non-empty list), as OpenAI answers it; test 2026-10-25 09:28:47 +01:00
Niko1221 dd9b9f746d About/Updates: never show the host's placeholder engine version (strata-engine 0.1.0) while the real one is still being read; a forced status read that meets one already running waits and reads again, so the version after an engine update is not the old one; test 2026-10-25 09:27:38 +01:00
Niko1221 6b5d593639 DeepSeek engine: --mmap-experts auto copies all the CPU lane's experts into RAM at load when they fit beside 8 GiB, with no demand store (all or none; --mmap-experts on/off, --ssd-cache N and DS4_STORE_GB stand as given); the auto store logs when it holds less than the lane. 1 x R9700 + 251 GB, 2.00 pack, warm: decode code 24.5 -> 27.6, agent 21.9 -> 27.1 tok/s, same ids as --ssd-cache 0 --mmap-experts off, load 34 -> 55 s. Rule ported from Project Maya v1.1.27 (MIT), mmap-experts auto copy-all 2026-10-25 09:27:16 +01:00
Niko1221 4d6721822a Storage and Manage models: a model whose files are all gone (old config, 0.0 GB, not ready) is no longer listed as a download 2026-10-25 09:26:19 +01:00
Niko1221 ad66a758a7 Models screen: DeepSeek is installed and shown at its one 8K context (as the guided setup), not the 32K default 2026-10-25 09:25:53 +01:00
Niko1221 59599f69f8 Monitor: All GPUs leaves out a small integrated Radeon/Intel chip beside a real card (totals and count); the hardware report gives each card a stable id since each make numbers from 0; tests with the RTX 5070 + Radeon iGPU pair 2026-10-25 09:25:17 +01:00
Niko1221 2a207dbfcc metal: sparse attention on simdgroup matrices is the default for 8-head groups (32K prompt 269 -> 293 tok/s, KL equal to the scalar kernel); DS4_ATTN_MX=0 for the old one 2026-10-25 08:46:05 +01:00
Niko1221 b6dcb23ec6 WIP metal: ds4_sparse_attn_mx (sparse attention on simdgroup matrices, DS4_ATTN_MX=1; =2 compares with the scalar kernel: rms rel 1.8e-6); timing and KL gate pending 2026-10-25 02:07:40 +01:00
Niko1221 100a2374fb metal: Q5_K dequant four values a step (bit-identical); DS4_SKIP_K and the timeline's overlap count as debug tools 2026-10-25 02:37:35 +02:00
Niko1221 a04f5505aa Merge f-0243-rel (ad638b93e) into f-025-readme 2026-10-25 02:12:48 +02:00
Niko1221 36f580a5c3 UPDATES.md: the Mac dmg keeps its Metal engine; only the Windows and Linux installers are engine-less 2026-10-25 02:12:42 +02:00
Niko1221 8810e00c09 metal: prompt hc_mix reads each token's row once for four mixes (bit-identical, +4.5% on the 3K prompt) 2026-10-25 01:49:44 +02:00
Niko1221 050752f5b9 DeepSeek Metal: head matvec back to 4 columns a pass (8 moved the KL by +0.002) 2026-10-25 01:41:34 +02:00
Niko1221 97c091c5be DeepSeek Metal: prompt flow reads experts straight into the ring (unified memory: no staging, no DMA), head matvec 8 columns a pass 2026-10-25 01:24:37 +02:00
Niko1221 6b2a530393 DeepSeek Metal: cached compute pipelines (the prompt path created one per expert copy), F_NOCACHE for the unbuffered expert reads, dispatch-seq log 2026-10-25 01:10:26 +02:00
Niko1221 ad638b93e5 Merge f-0243-dsimport (the app's model-folder preview recognises official DeepSeek sets) into f-0243-rel 2026-10-25 01:07:30 +02:00
Niko1221 32ad109816 Model preview: recognise official DeepSeek-V4.1-Flash IST-DAS sets (1.75/2.00/2.25 bpw, official and first-release names, with or without the Engram) as family deepseek41 2026-10-25 01:07:05 +02:00
Niko1221 e527bfefc1 DeepSeek Metal: GPU stale-router lookahead prefetch (opt-in DS4_LOOKAHEAD; measured slower), recall counters 2026-10-25 00:28:37 +02:00
Niko1221 9e3fe0461c DeepSeek Metal: CPU-lane page prefetch in parallel across the call's matrices (the cold reads were 78% of the lane), CPU-lane phase log 2026-10-25 00:06:36 +02:00
Niko1221 6270e2d9d6 collect_libs: a library an earlier binary's pass already put in --out is not copied onto itself (SameFileError stopped the AMD packages' second pass) 2026-10-24 22:51:19 +02:00
Niko1221 ce27d13539 Merge f-0250-tui-uma (Faster is the default Speed on shared-RAM GPUs) into f-0243-rel 2026-10-24 22:31:11 +02:00
Niko1221 90359513e3 DeepSeek Metal: DS4_BUSY_ALL experiment (off), progress lines 2026-10-24 22:18:40 +02:00
Niko1221 2dbdea2a77 Merge f-0243-strixspeed into f-0243-rel 2026-10-24 22:13:52 +02:00
Niko1221 da1eb6bc69 Merge f-0243-copyfix into f-0243-rel 2026-10-24 22:13:51 +02:00
Niko1221 2cea5afbc5 Merge f-0243-locales into f-0243-rel 2026-10-24 22:13:51 +02:00
Niko1221 0b9539877f README and docs for the single installer: three download buttons (Windows, Linux, Mac) with the 0.2.5 file names, one plain line instead of the per-GPU sentence (7 languages); docs/AMD_HIP.md and packaging/UPDATES.md name the single installers 2026-10-24 22:08:00 +02:00
Niko1221 9551b4168d Metal and SYCL builds: the Metal engine gets stubs for the 512-bit i-quant rows native_expert.cpp now references (x86-only file), and SYCL compiles iq_avx512.cpp with -mbmi2 and -mavx512vpopcntdq like the root CMakeLists.txt 2026-10-24 22:01:11 +02:00
Niko1221 1aff6d1411 Version 0.2.5: Terminal and app 0.2.5, engine and Control 0.1.50 (every Qwen and DeepSeek package, Metal included) 2026-10-24 21:52:49 +02:00
Niko1221 8d3c976166 DeepSeek Metal: keep the GPU busy (flag-released spin kernel) while the CPU lane runs: decode +25-35% 2026-10-24 21:47:29 +02:00
Niko1221 3f8f9aeaa6 Translations (de, zh, ja, es, ru, pt, ko) for the 26 new fix-catalog keys: first-run card, DeepSeek note, Speed labels 2026-10-24 21:47:01 +02:00
Niko1221 684881246d DeepSeek Speed label: the default is named by the engine's own rule, integrated / unified-memory GPU (Strix Halo) or cards totalling 16 GiB or less; '(default on this PC)' on the matching choice; test 2026-10-24 21:45:44 +02:00
Niko1221 d44144d7bb English catalog keys for the first-run card, the DeepSeek note and the Speed labels (the locale-gaps test failed on the merged branch); the other seven fix catalogs carry the English text until translated 2026-10-24 21:45:44 +02:00
Niko1221 df7d407742 Merge f-025-fast (Faster is the default on shared-RAM GPUs; ZC warning) into f-0243-rel 2026-10-24 21:40:42 +02:00
Niko1221 0597aaaeb7 launch: an integrated / unified-memory GPU (Strix Halo, gfx1151) is Faster by default, as the engine's --experts auto says: the 'default on this PC' label and the plan mirror 'integrated GPU, or cards totalling 16 GiB or less'; the engine 0.1.50 guard stays; tests 2026-10-24 21:40:29 +02:00
Niko1221 c4ce40bf5e DeepSeek engine: faster mode (5 experts a word) is the default on a GPU that shares the PC's RAM (Strix Halo: decode +11..13%, prompts +5..7% against 6); the 16 GiB rule is unchanged, --experts N wins; rule test rows 2026-10-24 21:40:06 +02:00
Niko1221 add4362139 Merge branch 'f-025-strix' into f-025-fast 2026-10-24 21:39:41 +02:00
Niko1221 ff91bc3729 DeepSeek engine: DS4_STORE_ZC=1 on a GPU that shares the PC's RAM, whose budget the weights already fill, prints one plain line that it will likely fail with a GPU fault and gains nothing (no cap) 2026-10-24 21:36:27 +02:00
Niko1221 dde3250cdb DeepSeek Metal: --dspark-depth, dispatch counts in the round times 2026-10-24 21:28:44 +02:00
Niko1221 67996022cd build_dsv41_windows.bat: configure every time so the build id is the current commit 2026-10-24 21:25:48 +02:00
Niko1221 7988837183 release build scripts: a failed step is a non-zero exit (build_hip_linux.sh), a stale binary or an old configure cannot pass, and strata-dsv41 must carry the release commit (BUILD_ID for a tarball, checked in the binary) 2026-10-24 21:24:53 +02:00
Niko1221 77b1a50541 launch: flow_for reads the settings only when the context has them (a context with only a computer, as test_mixed_vendors passes, raised AttributeError after the Speed step) 2026-10-24 21:18:56 +02:00
Niko1221 958774b4ce DeepSeek Metal: sampler statistics harness, round time breakdown 2026-10-24 21:10:17 +02:00
Niko1221 902239e7f7 ds4_gg_bench --k512: the two printf strings end in a newline escape (they held literal line breaks) 2026-10-24 20:57:49 +02:00
Niko1221 f29ff5a59a DeepSeek Metal: drafter bring-up (ring restore, host sampler for temperature and speculative sampling), per-dispatch GPU timeline 2026-10-24 20:44:05 +02:00
Niko1221 c1c3eab540 Merge f-026-5070 (in-place chunk 4096 on non-HIP cards under 16 GiB) into f-0243-rel 2026-10-24 20:41:36 +02:00
Niko1221 1fac68d6f2 DeepSeek engine: the experts-read-in-place default prompt chunk is 4096 on non-HIP cards under 16 GiB (was 8192): decode +2% agent / +0.4..9% code (5 pairs on RTX 5070 and RTX 3060, 120 more experts resident), an agent turn of 3K new tokens on 16K takes the same time, a cold 8K prompt 9% slower; DS4_FEEDLOG prints each feed's time; test rows for 12 GB, 15.9 / 16 / 24 GiB, 8 GB, unknown VRAM and HIP 2026-10-24 20:41:08 +02:00
Niko1221 aa4b225237 Merge f-025-strix (gfx11 WMMA expert kernels, iq test) into f-0243-rel 2026-10-24 20:31:03 +02:00
Niko1221 bd71ea81b9 Merge f-0243-pick (prompt-path rule, post-ready warm-up) into f-0243-rel 2026-10-24 20:25:56 +02:00
Niko1221 4d8ff9646f DeepSeek engine tests: the gfx11 / gfx12 matrix-core prompt tiles are forced on and off in ds4_iq_test (launch counted, both against the CPU reference and each other); umaprobe m1/ver probes; test knobs DS4_ZC_COARSE and DS4_STORE_CHUNK_SLOTS for the zero-copy registration 2026-10-24 20:11:27 +02:00
Niko1221 34810a3bb3 DeepSeek Metal: automatic VRAM-expert share from the anonymous-memory cap (explicit --vram-experts wins) 2026-10-24 20:09:03 +02:00
Niko1221 536460c3d8 Copy feedback: one helper (lib/clipboard.ts copyText with a toast); Markdown and Serving use it, lib/copy-text.ts is gone (the toast store is loaded only when a code block is copied) 2026-10-24 20:05:05 +02:00
Niko1221 061a285832 Merge f-0243-gui into f-0243-rel (Chat.tsx and DropZone.tsx: the guifix clipboard helpers kept at the conflicts, copyAndTell stays for Markdown and Serving) 2026-10-24 20:03:44 +02:00
Niko1221 5dce2f63e9 DeepSeek Metal: batched q8 quantiser, zero-copy expert mapping (DS4_ZC), spin-wait, GPU-busy and timing switches 2026-10-24 20:02:54 +02:00
Niko1221 e752c65455 ds4_gg_bench: --k512 calls the AVX-512 rows (iq512x_rows) directly against ggml-cpu's dot, bit for bit, and says whether the CPU and type allow them 2026-10-24 20:02:50 +02:00
Niko1221 dfd1b0f291 DeepSeek engine: the prompt path is chosen from the prompt's length alone (96 tokens and more through the chunked pass, windows of 16 below), one number for every box - the same prompt always takes the same path and gives the same bits (the old per-request timing prediction flipped the logits between runs and took 7 s on a 28-token prompt); the chunked pass is warmed up after the model is ready, on a thread, so the BLAS library's ~3.6 s start is not paid by the first prompt a person types; startup line; DS4_PICK=adaptive, DS4_PICK_MIN=N, DS4_WARM=0 as switches; rule test 2026-10-24 19:56:11 +02:00
Niko1221 8b8f8a10ee Merge f-026-5070 final chunk rule (7bf6e4d3b) into f-0243-rel 2026-10-24 19:15:07 +02:00
Niko1221 7bf6e4d3b7 DeepSeek engine: one default prompt chunk rule, prefill_chunk_default(): 1024 only for a single-card default run where the GPUs hold at least half of the routed experts (4 x R9700; the R9700 worker's gpu_expert_share() from 4cde62a35), 4096 when a GGUF pack's experts are copied into RAM (--mmap-experts off) on a non-HIP card with 10 GiB or more, 8192 for the experts read in place (5070, 3060, 1 x R9700: what a launch on one card already ran), 2048 for a pack that is not a GGUF; real launch logs on the RTX 5070 and the RTX 3060 show no --tp / --threads and 'prompts in chunks of 8192'; one unit test with the 5070, 3060, 5090-class, 1 x and 4 x R9700 cases 2026-10-24 19:14:17 +02:00
Niko1221 c2675c60d1 DeepSeek Metal: chunk path mixes through the fused hc_mix kernel 2026-10-24 19:12:34 +02:00
Niko1221 56ac21a217 Merge f-026-5070 (prefill chunk default rule) into f-0243-rel 2026-10-24 19:00:37 +02:00
Niko1221 829913ea59 DeepSeek engine: 4096 chunk rule applies to a GGUF pack only when it is copied into RAM (--mmap-experts off) and to an explicit --tp 1 single card; the experts-read-in-place default stays 8192; other packs keep 2048 2026-10-24 19:00:07 +02:00
Niko1221 700def65a2 DeepSeek Metal: chunked prompt path correct (sgemm on the one Context, KEEP dump syncs, debug switches); 2988-token prompt 251-277 tok/s vs 32 in windows 2026-10-24 18:56:42 +02:00
Niko1221 dba7d0e0ab DeepSeek engine: the prompt chunk of the experts-copied-into-RAM case (2048) and of an explicit --tp 1 single card (1024) becomes 4096 on cards with 10 GiB or more (HIP builds and smaller cards keep theirs; the in-place 8192 is unchanged); startup line; rule test incl. 12/16/24/32 GB, short-VRAM, HIP and max-context cases 2026-10-24 18:49:20 +02:00
Niko1221 535cacb052 Tests never write the owner's real Strata profile: pytest gets scratch LOCALAPPDATA/XDG state and data folders; the host test runner and the native-control test files get a scratch settings file, components folder and app data 2026-10-24 18:43:52 +02:00
Niko1221 67a6ee76f4 Engine bundle cleanup: one function (cleanupAfterInstall) writes the removed and kept lines to the host output, with a test that both reach it 2026-10-24 18:36:50 +02:00
Niko1221 a264168390 One size figure: engine sizes are decimal MB, rounded half up, in the Terminal (strata update) and the app; both tested on the same numbers 2026-10-24 18:14:36 +02:00
Niko1221 e6c5997091 DeepSeek engine: gfx11 WMMA expert kernels for the prompt sets, on by default on gfx1151 (Strix Halo: prompts +22..29% at the same KL), opt-in on other gfx11 parts; STRATA_DS4_HIPK=0 turns them off 2026-10-24 18:13:43 +02:00
Niko1221 7063f3adca Sizes in decimal MB on the first-run card and the update check (one helper); the engine bundle of a per-GPU install is removed after an update only when the same engine is complete in the store (size and SHA-256), else kept with one log line 2026-10-24 18:10:05 +02:00
Niko1221 58e5d7851e DeepSeek Metal: chunked prompt path (MPP expert tile GEMMs, dense gemm/dq, rows_f16), NC-column matvecs, dangling buffer and null-arg offset fixes (WIP, chunk output not yet matching window path) 2026-10-24 18:07:46 +02:00
Niko1221 1c69e23658 Merge f-0243-guifix final head into f-0243-rel 2026-10-24 17:50:44 +02:00
Niko1221 f613637cbd Merge f-026-5070 (experts N|auto, IQ1_M row) into f-0243-rel 2026-10-24 17:50:01 +02:00
Niko1221 4f47009d08 Merge f-0243-fuse into f-0243-rel 2026-10-24 17:48:34 +02:00
Niko1221 94a4a34b05 Merge f-026-qwen into f-0243-rel 2026-10-24 17:46:13 +02:00
Niko1221 8993997e02 Merge f-025-strix into f-0243-rel 2026-10-24 17:46:13 +02:00
Niko1221 9741a61d7b Merge f-0250-tui into f-0243-rel 2026-10-24 17:46:13 +02:00
Niko1221 06f313b019 CPU lane IQ1_M AVX-512 rows: the 64-weight step's scale words by one vpermw and the delta signs by a masked subtract (bit-identical to ggml's dot: 0 of 2.7M rows differ at 1 and 2 tokens on Zen 4 and Sapphire Rapids); +9% on the rows, the 1-token policy is unchanged 2026-10-24 17:41:33 +02:00
Niko1221 236e13b4af Settings update check: a failed check of the app itself says so in one plain line, with the raw error behind Details (it used to show only Control's new-version text) 2026-10-24 17:25:53 +02:00
Niko1221 c893eeebf1 App update: an update job an older host left running is finished when the installer has written uninstall.exe or changed the program file after the job began (the installer keeps the build time as the modified time) 2026-10-24 17:11:55 +02:00
Niko1221 9b4092a94d launch: the Speed choice and --experts need a DeepSeek engine of 0.1.50 or newer (0.1.49.2 does not accept the flag): an older or unknown engine gets no flag, no Speed step and no plan line, and no error; the version comes from the package folder, else --version; tests 2026-10-24 16:47:17 +02:00
Niko1221 3283d2ae49 DeepSeek Speed: offered and passed only when the installed DeepSeek engine is 0.1.50 or newer (an older one ends the start with unknown option --experts) 2026-10-24 16:46:06 +02:00
Niko1221 b79d6930c1 test: the own-GGUF check loads its helper by path (another tests package can already be loaded in a full tools run) 2026-10-24 16:43:46 +02:00
Niko1221 883e21bf2e tests: the app-owned settings list includes the DeepSeek Speed choice 2026-10-24 16:43:31 +02:00
Niko1221 4e4921f63e DeepSeek settings: Speed (Faster / Full quality), default shown by the engine's own 16 GB rule, passed as --experts 5|6 only when chosen 2026-10-24 16:42:29 +02:00
Niko1221 bd7acc0e6d launch: the 16 GiB rule for the Speed label is compared in whole MiB; the catalog's vram_gb is already GiB (nvidia-smi MiB / 1024, AMD bytes / 2**30), so a 16 GB card (16376 MiB) counts as 16 GiB or less; tests with exact MiB values 2026-10-24 16:39:53 +02:00
Niko1221 c4413c0b54 launch: one rule for DeepSeek's speed. Nothing is passed to the engine unless the person chose (its --experts auto decides: 5 at 16 GiB of cards or less); the 'default on this PC' words use the same 16 GiB rule summed over the cards the launch uses, and the summary reads the engine's own 'experts per word: N' line back; tests follow 2026-10-24 16:36:49 +02:00
Niko1221 bc1a3ba2ed launch: DeepSeek Speed choice, Faster (--experts 5, the default on a PC with 16 GB of graphics memory or less) or Full quality (--experts 6): a Speed step in the guided screens, --speed and --full-quality, plain words in --plan and the summary, the quality note; tests, docs 2026-10-24 16:33:51 +02:00
Niko1221 3dd11c6dc1 DeepSeek engine: --experts N (the router's top N, weights renormalised); default 5 where the cards hold 16 GiB or less in all, the model's 6 elsewhere; startup line 'experts per word: 5 (faster mode; full quality: --experts 6)'; DS4_TOPK=N stays as a test override; rule test; DS4_PROF prints the CPU lane's stage split 2026-10-24 16:31:09 +02:00
Niko1221 eb4736c45e DeepSeek engine: on a Windows integrated GPU the experts leave room for the context's later GPU allocations
The Windows commit limit decides how many experts an integrated GPU may hold (place_experts). With AMD driver 26.10.1 the limit
went from 74 GB to 133 GB on a Strix Halo, the engine put 9506 experts (86 GB) on the GPU instead of 8105, every byte of the
GPU's address space was gone, and the 32K prompt's later allocations spilled into RAM: free RAM fell to 0 and the engine died
("rocBLAS: No devices found"). The experts now leave 4 GiB + 0.2 MB a token of context above 16384 (DS4_UMA_GPU_RESERVE_GB sets
it, tests); only an integrated GPU on Windows is affected.

Strix Halo (gfx1151, 2.00 pack, shipped profile), driver 26.10.1:
  32K prompt: killed (min free RAM 0) before, 113.2 tok/s with min free 13.6 GB now; 64K prompt (70000 context): 122.2 tok/s, min free 14.4 GB
  4K / 8K prompt: 62.8 / 83.9 tok/s (64.9 / 86.2 with the 9506 experts: -3%), min free 13.4-13.8 GB
  KL, code segment, 999 positions: 0.1020 (0.1028 before)
A smaller reserve (3 GiB + 0.1 MB a token) gained 0.0% on 4K / 8K / code decode and left 0.9 GB less free at 64K, so it was not taken.
2026-10-24 16:22:28 +02:00
Niko1221 7c77f388dd metal: commit the queued GPU work before the CPU lane runs (they overlap) 2026-10-24 16:11:09 +02:00
Niko1221 66b43d4a7f Monitor: with All GPUs, a card that cannot report a value is left out and the card that does report is named (35 C . RTX 5070) instead of blanking the figure 2026-10-24 16:09:59 +02:00
Niko1221 91eb05a228 App update: a job an older host left running is finished when the app's program file is newer than the job 2026-10-24 16:07:45 +02:00
Niko1221 4228e6ee11 chat model picker: a downloaded model that is not loaded uses the same two-line row as the API models 2026-10-24 15:59:39 +02:00
Niko1221 42a982e033 metal: copies and fills ride the context's command buffer, events mark it, buffers chained by a shared event, decode matvecs for IQ2_XS/IQ2_S/IQ3_XXS 2026-10-24 15:59:04 +02:00
Niko1221 3e51b1c18a metal: decode matvecs for Q5_K and IQ2_XXS (every lane busy, 16-byte loads), lane-parallel hc_split for hc 4, DS4_PROF per-kernel GPU table 2026-10-24 15:44:39 +02:00
Niko1221 9caf39495c Merge f-0243-guifix into f-0243-rel 2026-10-24 15:44:30 +02:00
Niko1221 0c53372a0e Merge f-0250-tui into f-0243-rel 2026-10-24 15:44:30 +02:00
Niko1221 a3e4cb64fd test: serving never reaches the DeepSeek engine check (only launch and setup import it), and 'serve --plan runs no program' means no installer or fetch: a PC's read-only hardware probes (nvidia-smi, Get-CimInstance) no longer fail it 2026-10-24 15:26:07 +02:00
Niko1221 dc2c4229e9 test: launch_engine_first gives the runner a Qwen catalog (a bare Mock answered 'DeepSeek' to the 0.2.4 multi-file question and skipped the engine step); the launch code was right 2026-10-24 15:25:07 +02:00
Niko1221 9dbb13ee89 metal: the IQ2_XXS matvec read its sub-blocks 4 bytes apart (8), IQ2_XS signs, IQ3_XXS matvec added; more dumps and checks 2026-10-24 15:15:20 +02:00
Niko1221 60d856a18e DeepSeek decode: fewer launches a layer - the int8 roundings of one GEMV call in one launch (identical inputs once) and hc_mix + hc_split in one launch (the token's last mix block runs the split); same arithmetic
R9700 1 card 2.00: 21.30 -> 21.82 tok/s (+2.4%), 4 cards 35.39 -> 37.00 (+4.5%), 3 pairs each, same generated ids. DS4_QUANT_SINGLE=1 / DS4_QUANT_NODEDUP=1 go back to one launch an item / no sharing.
2026-10-24 14:47:48 +02:00
Niko1221 abab458d74 Feed reads: one more try when the first times out or the connection fails (the first wait is 12 s, the second the full 30 s); about one github.com request in 25 hung for 30 s and the first-run card said offline 2026-10-24 14:44:12 +02:00
Niko1221 9cf91f3555 Load of DeepSeek: the start wait is at most the 600 s Control allows 2026-10-24 14:36:14 +02:00
Niko1221 f30d191d54 Load of DeepSeek: the app adds no Flash-Next options (request slots, RAM budget, output clamp, resource profile, memory retry) to the DeepSeek engine and waits as long as its config says 2026-10-24 14:34:47 +02:00
Niko1221 75ee28fd3d Host: the per-model settings routes accept a dotted id; a test walks a dotted model id through list, load and unload 2026-10-24 14:33:40 +02:00
Niko1221 860e384db7 metal: the ring backup flag, decode takes the host-routed path, split commits sync the streams first, sparse_attn sizes its binding without a host read of device data 2026-10-24 14:33:07 +02:00
Niko1221 fe2b9f4094 Host: run config file names with a dot (DeepSeek) pass the app's own checks, so the model can be loaded 2026-10-24 14:31:43 +02:00
Niko1221 54768d25b1 test: the own-GGUF check finds tests.terminal when tools/ is run on its own 2026-10-24 14:29:49 +02:00
Niko1221 4499c69681 Installed models: an id with a dot (deepseek-v4.1-flash-ist-das-2.00bpw) is listed; the install finished but the app never showed the model 2026-10-24 14:22:39 +02:00
Niko1221 beeea0abf3 test: the Mac build-tools test follows find_tool's version check 2026-10-24 14:19:31 +02:00
Niko1221 569f7c739d test: the --kv choices check reads the last line of the error (Python 3.12 and 3.13 print the list differently) 2026-10-24 14:18:47 +02:00
Niko1221 ff84ff8164 First-run card: one plain line when the drive for the engine is short on space 2026-10-24 14:14:32 +02:00
Niko1221 61a4b6bd02 First-run card: no dash in the engine size line 2026-10-24 14:14:10 +02:00
Niko1221 192ab03813 DeepSeek-V4.1-Flash in the app: the model picker lists the three official sizes (not the 508 GB multi-card set), an 8K context only, a plain size/speed/disk note that warns and never blocks, no calibration step; the install tool accepts the own-path sizes 2026-10-24 14:11:45 +02:00
Niko1221 16c4da9443 CPU expert rows: 512-bit grid lookups by gather where a gather is cheap; one IQ3 token on the exact 512-bit rows
The 512-bit i-quant rows (IQ2_XXS/XS/S, IQ3_XXS/S) assembled their grid words from scalar loads (~30 uops a 64-value chunk). Where cpu_gather_fast()
(Intel Sapphire Rapids and later) they now load them by one gather, 32-bit for IQ3 and 64-bit for IQ2 (STRATA_IQ_GATHER=1/0 force it; Zen 4 stays scalar:
its gathers are microcoded). The same words are loaded, so every output bit is unchanged. One token of an IQ3_XXS / IQ3_S expert takes the exact 512-bit rows (ggml-cpu's
single-token AVX2 dot bit for bit) with the gather, after a first-use bit check against this ggml; STRATA_NO_IQ512X1=1 turns it off.

Xeon 8468, native_gu_rows (gate+up of an expert), us: IQ3_XXS 1 token 295 -> 219, 2 tokens 462 -> 300, 4 tokens 558 -> 389; IQ3_S 415 -> 305, 603 -> 342, 562 -> 296;
IQ2_S 2 tokens 377 -> 235, 4 tokens 437 -> 327 (one IQ2_S token stays on ggml: 239 against 234). Output checksums identical with and without.
Test iq512_gather_parity (both lookup modes): exact rows == ggml vec_dot at 1-4 tokens, engine rows within float rounding.
2026-10-24 14:09:08 +02:00
Niko1221 9030da09f9 Merge f-026-5070 into f-0243-rel 2026-10-24 14:01:05 +02:00
Niko1221 980c95a6d7 Merge f-0243-store into f-0243-rel 2026-10-24 14:01:04 +02:00
Niko1221 86a751af27 Merge f-025-strix into f-0243-rel 2026-10-24 14:01:04 +02:00
Niko1221 84b712a3d8 Merge f-0243-sm89 into f-0243-rel 2026-10-24 14:01:04 +02:00
Niko1221 db42e59930 First-run card stays until Continue; a processor's own graphics is not counted as a card 2026-10-24 13:58:36 +02:00
Niko1221 c1e80f7952 Markdown: a single newline inside a paragraph is a soft break (a space); two spaces or a backslash at the end of a line keeps a line break (#3818) 2026-10-24 13:55:42 +02:00
Niko1221 ea349eb8b8 Language: 43 interface strings that had no key (/plan /research /new hints, tuning, goal, telemetry, task and picture texts, the new fix texts) now exist in all 8 languages; the four Open hints translate their label; a test fails when a tr() literal or hint has no key (#3479, #3825) 2026-10-24 13:51:18 +02:00
Niko1221 08a5e6b34a packaging smoke: the packaged Terminal carries the installer since 0.2.4, so 'strata install' without a keyboard stops at the first question instead of saying 'no installer'; the check accepts both 2026-10-24 13:48:09 +02:00
Niko1221 353c9f2556 chat: a PDF is added to the chat folder and named in the next message; the file dialog lists PDFs and text on Linux (#3838) 2026-10-24 13:48:06 +02:00
Niko1221 ec03ae0106 First-run engine card: GPU, engine size, one Download button, progress with resume, 'I already have the file' (offline), plain states; host routes /api/engine/first-run, /job and /from-file; install-engine-file job 2026-10-24 13:47:22 +02:00
Niko1221 780a69b41b Expert settings: decimals can be typed in sampling and number fields; Ctrl+V of a screenshot reads clipboard items (#3736, #3748) 2026-10-24 13:47:18 +02:00
Niko1221 c7899f9c89 chat: Copy response and Copy message answer Copied or say they could not (with an older-copy fallback); a typed message keeps its line breaks (#3731, #3720) 2026-10-24 13:45:58 +02:00
Niko1221 e61b7024cf Models page: a neutral placeholder until the engine's first answer, not the bundled Starter (#3740) 2026-10-24 13:45:12 +02:00
Niko1221 25abbdd90c Models page: the chosen conversation context and cache format count in the memory estimate (#3832) 2026-10-24 13:43:25 +02:00
Niko1221 f750086183 Models page: the chosen conversation context and cache format count in the memory estimate (#3832) 2026-10-24 13:43:02 +02:00
Niko1221 31adebc8b7 strata components install-file: install an engine package the person downloaded (signature, every file, fit, test start, switch; no network); the engine switch is shared with the download path 2026-10-24 13:42:41 +02:00
Niko1221 4ccfd7ca30 Windows AMD/Intel cards: real memory size from the DirectX adapter list, load and memory in use from the Windows GPU counters, virtual monitors no longer listed as cards (#3727, #3836) 2026-10-24 13:40:59 +02:00
Niko1221 b750d757f6 CUDA engine selector: sm_MN machine code also covers a card sm_MK of the same major version with K >= N (CUDA cubin compatibility), so an RTX 40 card (sm_89) takes an sm_86 package; tests for the Qwen and DeepSeek packages; DeepSeek card text says RTX 30, 40 and 50 2026-10-24 13:40:27 +02:00
Niko1221 d9d033be86 launch: at 80 columns the model list keeps its verdict column (fits, tight, too big and the recommendation) instead of cutting it off; test 2026-10-24 13:40:22 +02:00
Niko1221 9c375ccb7d launch: setup's DeepSeek engine upgrade lines (the failure one 'A newer DeepSeek engine is available but could not be installed') are shown on the setup path too; test 2026-10-24 13:38:49 +02:00
chicco83 90a7aeea51 Setup wizard: show the VRAM of AMD cards on Windows from the engine's list when the host probe reports 0 (#3727, PR #3798 by chicco83) 2026-10-24 13:38:38 +02:00
Niko1221 d6c1936021 setup: a source build on Windows needs Visual Studio 2022 (2019 cannot compile the C++20 sources) and the stop says so, with the ready-made engine as the way around (#3566); tests follow 2026-10-24 13:37:35 +02:00
Niko1221 fb3bae20c6 setup: the list of installed packages remembers which system wrote it, so a .venv shared by Windows and Linux installs again instead of saying 'already installed' (#3634); test 2026-10-24 13:36:55 +02:00
Niko1221 1d20d3e2b0 chat: reopening a chat restores its workspace and the turn runs in that folder (#3754) 2026-10-24 13:36:35 +02:00
Niko1221 13a4153be4 copy-text: load the toast store only when copying, so the Markdown renderer still renders without a browser 2026-10-24 13:35:56 +02:00
Niko1221 ea897551ee folder picker: a PowerShell that ends before its window opens (security software) falls back to typing the path (#3831) 2026-10-24 13:35:54 +02:00
Niko1221 ed0c8939aa setup: a download that gets no data for two minutes says so with the percentage and the one next step, instead of a line that never moves (#3567); test 2026-10-24 13:35:43 +02:00
Niko1221 47ccec7c6a Delete a model: files Strata downloaded and packs it made go with it even in another models folder; the person's own files stay (#3716) 2026-10-24 13:35:04 +02:00
Niko1221 aef053e353 launch: the Access screen's help names --show-key and --api-key, so a person can see or set a key of their own (#3713 #3951); test 2026-10-24 13:34:18 +02:00
Niko1221 c22e5e7633 missing files: name the engine program in words once, not as component:strata-engine (#3900) 2026-10-24 13:34:11 +02:00
Niko1221 27cbbfe6d6 launch: the start waits longer for a big model by itself (10 minutes, about 8 s more per GB above 40 GB, at most 30), the server's own engine wait follows, and the runtime accepts start waits up to 1800 s (#3949 #3459). Own fix instead of PR #3817: that one needs people to set STRATA_ENGINE_READY_S; tests 2026-10-24 13:34:04 +02:00
Niko1221 97c31168aa chat model picker lists downloaded optimized models that are not loaded yet (#3814) 2026-10-24 13:33:56 +02:00
Niko1221 5faf357b49 Chat: Copy response/message/code say Copied or Couldn't copy and fall back to the older copy route (#3731); a pasted screenshot is also read from clipboard items (#3748); the user bubble keeps its line breaks (#3720) 2026-10-24 13:33:17 +02:00
Niko1221 77284b5e33 update: the test start runs the new engine with a short context and no picture reader (#3842), and an update keeps the CUDA build that runs here (#3835); launch: the summary names the engine log (#3656) and a tuned model says how to measure again (#3874); tests 2026-10-24 13:32:44 +02:00
Niko1221 4b49729a1e monitor: read Windows disk counters from one long-lived PowerShell instead of a cold one every 3 s (#3939) 2026-10-24 13:32:09 +02:00
Niko1221 014fd6b5db App update: log lines for every update step, an alert when the app came back as the old version, and Update engine works before the native snapshot has connected 2026-10-24 13:30:13 +02:00
Niko1221 5bfce4bea0 setup: the build tools come from the virtualenv first and an older system cmake (below the 3.24 CMakeLists.txt needs) counts as missing, so pip installs the right one (#3822); tests 2026-10-24 13:28:02 +02:00
Niko1221 15fdb69563 In-app update: a finished app update is no longer shown as an interrupted operation; a test update feed used once is not remembered as the feed for later checks
After 'Update Strata' installed a new app version, the host that asked for it was ended by the installer, so the job stayed saved as running and the next start turned it into a red 'Interrupted operation' alert in Settings > Update and Diagnostics. The job now records the app version it started from, and a start under a different version marks it done ('Strata is updated to X').

strata update and the app's check reused the feed used last, including a loopback address or a folder from a test of the update feed; every later check on that PC then said 'No internet connection'. Only an https address is remembered now.
2026-10-24 13:27:28 +02:00
Niko1221 ad9bb57ddc own model files: a qwen4exp file set that is no tested build but has the layout of one is used as that model with a plain notice, and setup keeps a complete GGUF of another size or hash with a warning instead of stopping (#3651 #3777); tests 2026-10-24 13:27:08 +02:00
Niko1221 b18700c8f7 Adaptive tier: the engine chooses the asynchronous tier by measurement (arena, one CUDA GPU)
Without --adapt-async on the command line, the expert arena on one CUDA GPU (--serve, adaptive tier on, no --batch, no layer
split, no pipelined windows, no peer or remote tier) starts on the blocking tier, ignores 64 steady decode windows, measures 64,
and when the tier's wait is at least 5% of a window runs 64 windows on the asynchronous tier, going back (for the session) only if
that was more than 2% slower per token. One log line at each switch. --adapt-async 0|1 and STRATA_ADAPT_ASYNC=0 keep their meaning;
the identity gates run with --adapt-every 100000 and never reach it. Decision logic and its unit test are header-only
(adapt_async_default.hpp, adapt_async_default_test).
2026-10-24 13:27:00 +02:00
Niko1221 c02a0b8b0d UPDATE.bat / update.sh: an engine that stayed older than the minimum is said as 'the engine was NOT updated' with one next step, not '[ok] Strata is updated' (#3653); test 2026-10-24 13:08:46 +02:00
demetree 7a03fc9414 strata connect: scale the app configs' compaction point with the server's context 2026-10-24 13:06:11 +02:00
Niko1221 d924beffe5 setup: a Mac with an Intel (Rosetta) Python is still an Apple Silicon Mac; the chip name and hw.optional.arm64 decide, one plain line says the Python is an Intel build (#3894); test 2026-10-24 13:06:00 +02:00
Niko1221 bdaa93edf0 launch: a setup option error ('setup.py: error: argument --kv: invalid choice') is the cause shown, not only 'could not be installed'; setup accepts --kv fp16 as DOCKER.md says (#3785); tests 2026-10-24 13:05:30 +02:00
Niko1221 1736b558bd setup: a NVIDIA driver older than CUDA 13 needs (528 to 579) runs the CUDA 12 engine by itself and says so in one line instead of stopping and naming a flag strata launch does not have (#3843); tests 2026-10-24 13:04:44 +02:00
Niko1221 0cd44323e4 Windows: a card taken out of the PC no longer decides the engine; only the display adapters Windows reports as present count when two makes are in the registry (#3892); test 2026-10-24 13:03:56 +02:00
Niko1221 193529239c metal: finish dst stride, BF16 ring pack, residency on the context queue, split the command buffer after fp8 finishes; ds4_launch.h makes the CUDA build work again; more DS4_DUMP_S points 2026-10-24 12:41:49 +02:00
Niko1221 e3eeb93972 DeepSeek engine: the RAM store's background fill is opt-in (DS4_STORE_BG=1): ready 9 s sooner but the first prompt waits for the same SSD and decode measured 3.7% lower 2026-10-24 12:40:19 +02:00
Niko1221 58f2acc05b DeepSeek engine: the RAM store's fill beyond the first 48 GB is read in the background after the model is ready (hottest first, behind the model's own reads); DS4_STORE_BG=0 reads it all first 2026-10-24 12:33:10 +02:00
Niko1221 6fcde7e305 DeepSeek CPU lane: compute the store hits while the misses are read, each missed expert as its read lands (DS4_MISS_SPLIT=0 restores the wait-for-all order); ExpertStore::ensure split into begin / wait_slots 2026-10-24 12:29:56 +02:00
Niko1221 b3c86c783f DeepSeek engine: the automatic RAM store may take every routed expert, not 48 GB at most, where the free RAM holds them (1 R9700 + 251 GB, 2.00 set: 12.6 -> 21.4 tokens/s); free - 12 GB and the explicit sizes unchanged 2026-10-24 11:58:35 +02:00
Niko1221 9f45341966 metal: bind the allocation's own MTLBuffer instead of a no-copy alias per call (the alias read stale data), deferred frees, matvec jobs cut at 8 activations, Q8/quant debug checks 2026-10-24 11:47:51 +02:00
Niko1221 f1e41cdee2 DeepSeek engine: the automatic RAM store is sized again after the weights are on the GPU, and keeps room for the context's buffers, so a unified-memory PC (Strix Halo) keeps 12 GB free; explicit sizes unchanged 2026-10-24 11:28:09 +02:00
Niko1221 da9cf6354f ds4: DS4_DUMP_S sub-step dump inside the layers (same records on CUDA and Metal), DS4_FLUSH_EACH, per-layer debug prints 2026-10-24 11:25:36 +02:00
Niko1221 0c34a7de3c metal: both queues drain at every sync, engram dequant wraps the strided extent, the ggml CPU lane is in strata_ds4
Window 2's residual stream was window 1's stale tail: stream copies and mtl_rt's deferred command buffer had no
ordering at sync points (memcpy, stream sync). Now a full barrier over all streams and the context, and the context
takes a lock (helper threads flush too). engram_dequant_rows wrapped n*18/32*hd bytes but strides rows by hd: rows
past 192 of 288 read unmapped memory (inf scales, router picked INT_MAX). The CPU lane (ggml-cpu rows) was compiled
out, so every non-resident expert contributed zero. Debug: DS4_DBG_ALL (every layer), DS4_DBG_EMB, DS4_DBG_EG.
2026-10-24 11:14:02 +02:00
Niko1221 7dd5cf18e6 release notes 0.2.4.2: the picture-file and already-have-files fixes (#3927, #3917, #3914); no environment variable in the conversations line 2026-10-24 10:52:25 +02:00
Niko1221 8aacdbc7a0 release notes 0.2.4.2: the list of new files 2026-10-24 10:51:27 +02:00
Niko1221 3eabee6049 test: the restyled READMEs state the app version through their download buttons (Strata-0.2.3-...), not the old '0.2.3 · ' header line 2026-10-24 10:51:13 +02:00
Niko1221 c896f73934 release notes 0.2.4.2: the DeepSeek engine packages are 0.1.49.2, the same number the binary prints and the Control has 2026-10-24 10:36:34 +02:00
Niko1221 93a91a2767 README translations (zh-CN, ja, de, fr, es, pt-BR): brought up to the restyled English README (download buttons per system, section menu, coffee badge, plain-word DeepSeek table, shorter Using it) 2026-10-24 10:36:09 +02:00
Niko1221 12939aa64d README: restyled to the owner's GitHub layout (download buttons per system, a section menu and a coffee badge at the top, plain-word DeepSeek table with the measured 0.2.4 speeds, shorter Using it, jargon cut); DEEPSEEK41.md: 4x R9700 speed updated to the 0.2.4 measurements 2026-10-24 10:29:36 +02:00
Niko1221 dfa3f4025c DeepSeek: pack_gguf takes an automatically found mmproj only when its projector is deepseek41v (a Qwen mmproj in the shared models folder is ignored, no error: #3927 #3917); files the person already has are recognised as DeepSeek by the pinned shard names in the guided screen too, not by the generic GGUF check that called the UD n-gram shard unreadable (#3914); tests 2026-10-24 10:27:57 +02:00
Niko1221 ac08a0820d release notes 0.2.4.2: conversations and a shared system prompt are kept on disk 2026-10-24 10:12:46 +02:00
Niko1221 05041e5db0 Merge branch 'f-024-on' into f-0242-rel 2026-10-24 10:12:41 +02:00
Niko1221 3157c38c68 DeepSeek engine: an installed package older than the signed feed's is updated by itself on launch and setup (fetched and checked, put next to the old one, started once, then switched to and the old one removed; one plain line and the old one keeps running on a failure; silent without a network; never for --dsv41-package or STRATA_DSV41_EXE); tests; release notes 2026-10-24 09:35:15 +02:00
Niko1221 d2d198a492 release notes 0.2.4.2: the DeepSeek engine 0.1.49.1 (faster on AMD Zen 1/2 CPUs, answers unchanged) and how to get it 2026-10-24 09:30:42 +02:00
Niko1221 8c9bd206ce Merge branch 'f-0243-iq1m' into f-0242-rel 2026-10-24 09:29:52 +02:00
Niko1221 46bddbbb74 dsv41: BMI2-off ggml-cpu applies only to the DeepSeek engine build (STRATA_BUILD_DSV41); ds4_gg_bench --dump FILE to compare two ggml builds bit for bit (IQ1_S/IQ1_M with and without BMI2: identical at 1,2,4,8,16 tokens) 2026-10-24 09:14:06 +02:00
Niko1221 6953d896d0 dsv41 CPU lane: IQ1_M uses Strata's exact rows from one token (ggml's IQ1_M dot runs at 1.8 GB/s on AMD Zen 2: _pdep_u64 is microcoded there); portable ggml-cpu build has BMI2 off (IQ1_S 2.3 -> 37 GB/s, IQ1_M 1.8 -> 32 GB/s on an EPYC 7452) 2026-10-24 09:09:23 +02:00
Niko1221 baa8f616de DeepSeek-V4.1: conversation slots and the shared-start checkpoint on by default (STRATA_DS4_SLOT_KEEP=0 / STRATA_DS4_AUTO_PIN=0 turn them off) 2026-10-24 08:25:17 +02:00
Niko1221 ac669f199d Terminal 0.2.4.2 / Control 0.1.49.2: version bump for the DeepSeek launch and install fixes (app stays 0.2.3, engine packages unchanged); release notes 2026-10-24 06:29:42 +02:00
Niko1221 0003604859 launch: the second sentence of 'The model could not be installed.' starts with a capital; test 2026-10-24 06:29:42 +02:00
Niko1221 e168b76b8c Merge branch 'f-0242' into f-0242-rel 2026-10-24 06:28:20 +02:00
Niko1221 f53664c159 launch: a setup message that ends with a full stop does not get a second one ('engine..'); test 2026-10-24 05:43:54 +02:00
Niko1221 44f8c334de model list: the DeepSeek disk check counts only what is still to be downloaded (files already in the models folder, also under the first release's names, are done), plus the room for the pack; tests 2026-10-24 05:41:50 +02:00
Niko1221 a22d557eee launch --plan: a DeepSeek plan no longer promises a Strata engine download; test 2026-10-24 05:39:30 +02:00
Niko1221 51d23fdd1e P2003 findings: DeepSeek setup says in one line when GitHub cannot be reached (a 10 s check before any download, not minutes of retries) and no longer fetches or test-starts the Strata engine, llama.cpp source or CUDA libraries it does not use; install.sh is one function called on the last line (a half-downloaded file runs nothing) and every documented one-liner is 'curl ... -o install.sh && sh install.sh' so a failed curl cannot look like success; tests 2026-10-24 05:37:41 +02:00
Niko1221 97637e2a14 metal: init_pre no longer wraps a whole page to reach 32 bytes
init_pre writes T*8 floats - 32 bytes for T=4 - and its wrapper wrapped the enclosing 16 384-byte page to bind them.  That aliasing
MTLBuffer overlaps the arena pages that hold h, and with it present h read back as 'valid at both ends, NaN in the middle' with
nan_range starting at exactly 4 096 - one wrapped page of floats.  The work is trivial, so it now runs on the CPU through host_view:
no wrap, no alias, no dispatch.

Result with the fix: h at layer 0 is clean (zeros=40960, ninf=0, max=0) instead of ninf=4096 max=7.6e+07.  The output is still
0,0,0,... so embed's write still does not reach h - but the contamination that had been confusing every probe of that buffer is
gone, and the two remaining readings (host_view vs cudaMemcpy on h between embed and layer 0) can now be compared cleanly.

Fixes this session, all measured: the 1.32 GB all-zero embedding file (my converter read the sparse trial shards), my own
20000-dispatch flush cap (it blocked every prompt of 256+ tokens), and now the init_pre page alias.

Targets, unmet: decode 8.94 tok/s vs 100, prompt 4000 tokens ~27 tok/s vs 1000, and no valid token has ever been produced.
2026-10-24 05:31:28 +02:00
Niko1221 782a8381de metal: probes that establish what is NOT wrong, and the honest state of the targets
Measured this session, in order of how much they narrow the problem:

* the pack's embedding table is real, and the engine hands embed correct rows (rows_in: first=4759 4759 4759 4759)
* ds4_embed's kernel receives the right arguments (args probe: dim=5120 hc=4)
* one Dev, one h: DBG devh prints id=0 h=0x100011a8000, the same address layer() reads
* the memory model is sound: writing through host_view round-trips through cudaMemcpy at 4 KiB and at 64 MiB (42 -> 42)
* both dispatch routes work: a constant written via Arg::B reads back 3 3 3 3, via a device pointer passed by value 7 7 7 7
* the Metal embed path is load-bearing: breaking its pipeline changes the exit code 0 -> 3

What is still wrong: h is partly NaN at layer 0 (nan_range=4096..40703, max=7.6e+07) with valid data at both ends, and
wholly NaN from layer 1, so the logits never resolve and every run prints 0,0,0,...   Nine hypotheses were tested and
disproved today: the zeroed table (a real bug, fixed, not the cause), two queues, host_view vs device pointer, memory
coherence, the binding, a dead Metal branch, a wrong dim, a short write, and two Dev records.

Targets, measured, unmet: decode 8.94 tok/s vs 100; prompt 4000 tokens ~27 tok/s vs 1000.  The 51.8 and 176 tok/s reported
earlier were dispatch-ENCODING rates and are withdrawn here as they were in the notes.

Next: locate the real d.h allocation (several probes landed on paths the engine does not take) and read that buffer at
three points in one run - at allocation, after embed, and at layer 0.
2026-10-24 05:29:05 +02:00
Niko1221 c763c12ac5 metal: the layer reads a different h buffer than embed fills - measured with addresses
embed writes   h = 0x144f24000
    layer 0 reads  h = 0x100011a8000

Both report device 0, and forcing --gpus 0 does not change it, so it is not a tensor-parallel artefact: the engine holds two Dev
records with two h allocations, embed fills one and layer() reads the other, and the bounce in ds4_model.cu (if (l.dev != cur)) is
meant to reconcile them but does not on this path.

That single pair of addresses explains every symptom: layer 0's input buffer was never written (hence zeros=40960), the first block
then produces NaN, and the logits never resolve.  It also retires the last four hypotheses: the embedding table (real), the embed
kernel (verified: writes as_type<float>(0x4759 << 16)), two-queue ordering (shared Context now), and stale host reads (the probe
flushes).

Correcting the record: the 51.8 tok/s decode and 176 tok/s prefill reported earlier were dispatch-ENCODING rates and are withdrawn.
The real measured rates are 8.76 tok/s decode and ~27 tok/s at 4000 tokens, and no valid token has ever been produced.  Targets remain
100 tok/s and 1000 tok/s: both unmet.

Next: print cur, l.dev and the two h addresses inside layer() - whichever differs is the buffer the embedding never reached.
2026-10-24 05:13:47 +02:00
Niko1221 00e7626771 metal: embed verified correct at the kernel; the value is lost before layer 0
Measured this stretch:
* the pack's embedding table is real (embed_pinned = 3d6b 3e6e 3e65 3dd7)
* embed itself is correct: immediately after the call h[0..3] reads 55296, which is exactly as_type<float>(0x4759 << 16),
  the BF16 embedding value converted the way both ds4_embed and embed_kernel convert it
* yet at the top of layer 0 h is all zeros (zeros=40960 max=0), and block 1 turns that zero into 100% NaN

Nothing between the two probes writes h except init_pre (which writes pre).  The remaining suspect is which Dev's h the layer reads:
the engine has a cross-device bounce (cudaMemcpyAsync through a host staging buffer) that ds4_metal_flush() does not cover, and a
stale l.dev under 'tensor parallel 2 -> 1' would produce exactly this.

Three hypotheses about the embedding are now retired with evidence: the zeroed table (a real bug, fixed, did not change h), two-queue
ordering between init_pre/embed and the rest (they now share one Context, no change), and the kernel writing nowhere (it writes
correctly).  Targets remain unmet: decode 8.76 tok/s vs 100, prompt 4000 ~27 tok/s vs 1000, and no valid token has been produced.
2026-10-24 05:11:14 +02:00
Niko1221 a62ca064a3 launch: the DeepSeek engine package that runs is the newest of the running Terminal's own install (STRATA_INSTALL_DIR, else its own folder) and the install the model's settings came from (the first remembered install, which can be an older Terminal folder or another checkout); the running Terminal's own on a tie; the choice is logged; versions compare as numbers; tests 2026-10-24 05:09:58 +02:00
Niko1221 ba9e818c42 metal: drafter path ported, token-boundary commits, and the embedding blocker measured end to end
State of the port, honestly: the engine runs, 40+ kernels dispatch, the pack and the 110 GB Engram are real, and it has NEVER
produced a valid token.  Both targets are unmet: decode 8.76 tok/s (target 100), prompt 4000 tokens ~27 tok/s (target 1000).

What this commit adds:
* the drafter path: ds4_mx_gu/ds4_mx_dn (MXFP4 prompt gate_up/down, one runtime-ne kernel), ds4_markov_add, ds4_confidence, with
  hosts and guards.  --dspark on now runs end to end, but the confidence is NaN (its taps come from the trunk, which is NaN), so no
  drafts are offered and there is no drafter speedup.
* token-boundary commits: seven host-read sites in ds4_model.cu flush the deferred command buffer before reading a device result.
  This is what exposed that the earlier 51.8 / 176 tok/s figures were dispatch-ENCODING rates, not GPU rates: with the work inside
  the timed window the real numbers are 8.76 tok/s decode and ~27 tok/s at 4K.  Those earlier numbers are withdrawn.
* embed/init_pre now dispatch through the shared Context (they had their own Metal queue, so no ordering against the copies that
  feed them) - measured, and it did NOT fix the blocker.
* the embedding blocker, measured at both ends: the pack's table is real (embed_pinned reads 3d6b 3e6e 3e65 3dd7), ds4_embed is
  dispatched at ds4_model.cu:7490 and returns, and h[0..3] is 0 0 0 0 immediately afterwards, so the kernel writes nothing into the
  buffer the engine reads.  Behind it, block 1 turns a zero input into 100% NaN (a 0/0 in the first norm or router).
* env-gated debug probes (DS4_DBG_H, DS4_DBG_EMB, DS4_DBG_L) left in for the next session; they are inert without the variables.

Known edit hazard, learned three times: patching by a generic statement pattern lands in the wrong function.  Patch by unique
context or by function name.
2026-10-24 05:09:14 +02:00
Niko1221 8183c6b671 docs: RTX 3060 speed with the release defaults (8 to 10 tokens/s, 149 tokens/s at 8K); DSpark stays off on 12 GB cards and adds 2-13% when turned on; README x7, DEEPSEEK41.md, release notes 2026-10-24 04:59:46 +02:00
Niko1221 1a80ccc981 metal: histogram off the hot path, dedupe and host_view experiments reverted, chunk sweep recorded
Removed the per-dispatch histogram from encode() (it cost a mutex and a string key per dispatch; measurement showed no gain, so the
~10 us a dispatch is genuine encode + engine host work).  Reverted the two experiments that did not hold up: the q8_quant dedupe
(the repeated calls all differ - 0 dispatches saved) and skipping host_view() in cpu_addr (breaks the engine, 15 dispatches).

Prefill chunk sweep, 4000 tokens, pack-2.25-real:  --prefill-chunk 16 -> 165.90 tok/s, 32 -> 175.89, 64 -> 176.26.  The engine runs
16-token windows regardless, so the curve flattens: 176 tok/s is the 4K number and the target is 1000.

Measured state: decode 51.8 tok/s (target 100), prompt 4K 176.3 tok/s (target 1000).  Neither met; the remaining levers are kernel
fusion for decode and a key-tiled attention kernel for prefill, both rewrites.
2026-10-24 04:04:26 +02:00
Niko1221 0b2201ce44 metal: the 20k flush cap was the 256+ prompt failure; 4K now runs at 171 tok/s, and the launch histogram names the decode cost
Two measurements tonight that matter:

1. Every prompt of 256 tokens or more died at exactly '20073 dispatches, 2 command buffers'. That is the deferred command buffer's 20,000
   dispatch cap - my own constant from when I introduced batching - flushing mid-token. Raising it to 2,000,000 makes the long prompts run:
     64 tokens   535.8 tok/s
     128 tokens  556.3 tok/s
     256 tokens  542.6 tok/s  (31,508 dispatches, 1 command buffer)
     1024 tokens 451.0 tok/s  (126,020 dispatches, 1 command buffer)
     4000 tokens 170.9 tok/s  (492,254 dispatches, 1 command buffer)
   The curve falls with length, so the 4K figure is attention scaling, not fixed overhead.

2. The per-kernel dispatch histogram now works (Pipeline carries its name; the earlier '<unnamed>' was a lookup bug):
     ds4_q8_quant      4122   (36%)
     ds4_iq_mv_k       1218
     ds4_finish        1038
     ds4_hc_pre_norm    486 / hc_split 480 / hc_mix 480 / hc_post_v 480
     ds4_gemv_f32       474
   ~1,890 dispatches a token at 8.8 us each = the 16.7 ms a token, so decode speed is 1/(launches x 8.8us). 100 tok/s needs
   ~1,130 launches: dedupe q8_quant per layer (-380) and fuse the four hc_* passes (-320) gets there arithmetically.

Decode 51.8 tok/s (target 100, measured on pack-2.25-real), prompt 4K 170.9 tok/s (target 1000). Neither target met.
2026-10-24 03:57:34 +02:00
Niko1221 c486f17251 metal: sparse attention + prompt matvec columns + exact wrap extents - prefill measured at 556 tok/s
Measured (real 110 GB Engram pack, engine's own lines):
  prompt  64 tokens -> 535.76 tok/s
  prompt 128 tokens -> 556.31 tok/s
  decode            -> 48.3-50.0 tok/s
  prompt 256+       -> aborts, router sentinel 2147483647 (no pps figure)
Targets are 100 tok/s decode and 1000 tok/s prompt: both roughly 2x away, neither met.

Added:
* sparse_attn_packed<G> (G=8/4/2/1) and the BF16 row variant, with the host wrapper that mirrors AttnRows and picks G - the 4K
  abort at ds4_kernels.cu:2161 is gone.
* ds4_iq_mv widened to 16 accumulators; host chunks any job with ncol > 16 into 16-column jobs (this prompt needs ncol=96), and
  Q5_K/Q6_K dense matrices get ds4_iq_mv_k instead of being fed to the IQ2-only decoder.
* route, gather_rows, combine kernels.
* Wrap extents made exact across 7 wrappers (moe_sort, swiglu, hc_split scale/base, sparse_attn rows, unpack_sel, engram_dequant);
  the old 'bytes + 65536' form could overrun an arena chunk's edge.

Known bug, not fixed: for prompts of 256 tokens or more the attention SINK logits read NaN (sink[57] at the first failing window),
which poisons the softmax denominator, then q, then the router. 64 and 128 tokens are clean, so it looks like a path switch at the
256 boundary (few = T <= kAttnScratchT in sparse_attn) or an overrun into the sink buffer.  That is what stands between the current
556 tok/s and a 4K figure.
2026-10-24 03:38:28 +02:00
Niko1221 611530d992 metal: dispatches BATCH into one command buffer - decode 0.16 -> 48.7 tok/s (300x)
Context::run() committed a command buffer and blocked in waitUntilCompleted for EVERY dispatch. At ~60 dispatches a layer over 40
layers that is 3.3 us x 2400 plus a 133 us commit+wait each, and it showed up as exactly the 330 ms a token the engine reported.

Now a single dispatch is appended to a persistent command buffer and returned from without waiting; the buffer is committed when it
fills (20000 dispatches) or when the engine synchronises (strata::gpu's stream/device synchronise call ds4_metal_flush()).

Measured on the same fast harness (--vram-experts 0.05 --cpu-threads 2, ids 1,2, n=6):
  before: prompt 5.43 tok/s, decode 3.00 tok/s, layers 330.1 ms/token
  after:  prompt 77.0 tok/s, decode 48.7 tok/s, layers  19.1 ms/token
with the experts resident in VRAM instead of streamed: decode 42.6 tok/s (layers 19.5 ms) - so residency is no longer the limit.

The remaining gap to 100 tok/s is the 1-token IQ2 matvec itself: with ncol == 1 the eight accumulators are idle and every weight is
decoded for a single dot, which is the 87.7 GB/s case measured earlier (versus 437 GB/s at an 8-token window). Fixing that is kernel
work, not runtime work.
2026-10-24 01:57:12 +02:00
Niko1221 33bf441cd4 metal: THE ENGINE GENERATES TOKENS ON THE MAC - 17 more kernels, the whole path runs
Official harness completes with no abort: /tmp/r.out = '0,0,0,0,', zero 'no Metal dispatch', zero metal errors.  Prompt 4 tokens in
5112 ms, decode 4 tokens in 18876 ms.  The ids look degenerate and numerical parity is NOT established - the Engram in this pack is
zeros, and a wrong kernel would produce the same output - so what is proven is that the engine walks the entire path and emits ids,
nothing more.

Kernels added (each verified by the engine's next abort naming a later or different launch):
  prompt expert GEMM: gate_up_kernel<T,NE>, down_kernel<T,NE>  (ds4_iq.metal, with IQ3_XXS type 18 joining the decoders - this
  pack mixes gate types 16/17 with down types 18/22)
  mx_combine, engram_dequant (fmt 2), engram_gate, bf16_mv (engram wkv), compress, index_score, topk (radix with simd_ballot),
  finish_kernel<true> (KV pack - the call site had no guard at all), finish rounding branch, gemv_f16_rows, block_max, hc_mean,
  iota_rows, sampler_greedy  (ds4_init.metal, 1521 lines)
  mx_wait skipped under STRATA_METAL, like the pre-existing wait_seq no-op

Also fixed a real pre-existing bug: the 'payload + 65536 from the page base' wrap convention walked off the mapping when a small
buffer sat at an arena chunk's edge (flaky 'mtl: wrap 81920/98304 failed', exit 4, 2 runs in 4).  ds4_finish.cpp now wraps exactly
the pages a buffer spans.

Not ported: sampler_kernel<0> (the sampled path; the harness is greedy), engram fmt 0/1 (this pack is fmt 2), bf16_mv's non-vector
variant.  Expert types beyond 16/17/18/22 make the IQ kernels return false, so the CUDA launch names the line instead of a silent pass.
2026-10-24 01:51:31 +02:00
Niko1221 154c2990ed metal: the routing trio ported ahead of its turn - moe_sort counts, bounds scan, fill
ds4_bounds_scan, ds4_moe_sort_count and ds4_moe_sort_fill (ds4_kernels.cu:1938-1943) with the host wrapper that launches them
in order on one command buffer (serial dispatch, so call order is execution order).  kPicks = 8: idx is [T][8], q -> (q/8,
q%8); one threadgroup a virtual slot, a block-wide prefix over 8 warps, row_tok/pos written only by the filling lanes.

One Metal fact paid for: simd_ballot returns a simd_vote that does not cast to an integer here.  The counts come instead from
simd_prefix_exclusive_sum (a lane's offset inside its simdgroup) and simd_sum (the group's total) - which is a cleaner
formulation than CUDA's ballot+popc anyway.  AOT: 20,160-byte AIR, rc=0.

Correction worth recording: the engine's current gate is NOT moe_sort.  It still stops at ds4_kernels.cu:1915, i.e. the Engram
row decoder (engram_dequant_tok) - I attributed :1915 to moe_sort two briefs ago and that was wrong.  The routing trio is ported
and wired but not yet reached; its turn comes after the Engram decode.
2026-10-23 19:51:04 +02:00
Niko1221 4735e381ad metal: hc_split ported - the hyper-connection family is complete on the Mac
ds4_hc_split is the SCALAR path of ds4_kernels.cu:268: lane 0 of a 32-thread group does the whole token in a local 8x8 matrix -
sigmoid pre/post, row softmax with max shift, +eps, column normalise, then iters-1 alternating row/column normalisations.  That is
the same mathematics as the CUDA warp-lane version for hc == 4, without the shuffles: correctness first, the lane-parallel form can
follow if it ever shows in a profile.

The first attempt segfaulted (rc 139) and the cause is worth remembering: scale and base were memcpy'd as if they were plain host
pointers, but every operand the engine hands a kernel is a device buffer - reach it through strata::gpu::host_view() and pass it as
a wrapped Buffer with its offset.  With that fixed the engine advanced from :1908 to :1915.

Queue, nine items, each verified by the engine advancing: init_pre, embed, cublasSgemm, finish, hc_post, hc_mix_scale, hc_mix,
hc_split -> next is engram_dequant_tok (:1915), the Engram row decoder whose maths was verified standalone earlier.
2026-10-23 19:46:42 +02:00
Niko1221 7022bd25d8 metal: the hyper-connection family on the Mac - hc_post, hc_mix_scale, hc_mix ported
Three more kernels, each confirmed the only way that matters: the engine moved past the launch line it had been stopping at.

* hc_post_inplace (ds4_kernels.cu:1677) - h[t][k][e] = post[t][k]*y[t][e] + sum_j comb[t][j][k]*h_old[t][j][e]
* hc_mix_scale (:1692) - mixes[t][i] *= rsqrt(sum(h[t])^2 / hcdim + eps)
* hc_mix (:1901) - mixes[t][m] = (h[t] . fn[m]) * rsqrt(mean(h[t]^2) + eps)

all in ds4_init.metal with host glue in ds4_finish.cpp (which is now the home of the small kernels: it owns the Context, the
pipeline cache and the wrap/offset helpers).  Queue so far, seven items, every one verified by the engine advancing:
init_pre, embed, cublasSgemm, finish, hc_post, hc_mix_scale, hc_mix -> next is hc_split (:1908).

hc_split is NOT a transcription: its softmax/sinkhorn is expressed in warp-lane geometry (shfl_xor masks 1,2 for rows and 4,8
across columns over lanes 0..15, for hc == 4) and it loops 'iters' times.  A wrong lane mapping yields a plausible but skewed
combination matrix, so it needs a parity test against the CUDA version on synthetic mixes before it is trusted.
2026-10-23 19:43:41 +02:00
Niko1221 5d969adde9 metal: finish_kernel<false> wired - the engine passes it and names item five
ds4_finish.cpp binds the engine's x as a wrapped buffer with an offset, passes Ds4Finish as a constant struct whose pointer fields
are the engine's gpuAddresses, and dispatches one launch with grid = n_vec (the kernel's blockIdx.x is the threadgroup index).
It declines the mrope (rd > 0) and rounding (quant != 0) branches and the KV pack, so those still abort with the launch line
rather than being skipped.

Verified the only way that matters: the engine no longer stops at :1894, it stops at ds4_kernels.cu:1921 (hc_post_inplace_kernel).
Queue so far, each step confirmed by the engine advancing: init_pre, embed, cublasSgemm, finish.

Two plumbing lessons: ds4_finish.cpp had to be added to STRATA_DS4_SOURCES (strata_ds4 does not use the engine-source glob), and
scp with a relative remote path put it nowhere - absolute paths from now on.
2026-10-23 19:40:19 +02:00
Niko1221 cb931e7010 metal: finish_kernel<false> in MSL, verified by the AOT compiler
ds4_finish (ds4_kernels.cu:1894) with its Ds4Finish struct and the block reduction: warp_sum over the 256-thread group
(simd_shuffle_xor 16,8,4,2,1 into red[8], barrier, second warp_sum, barrier), the RMS path (inv = rsqrt(tot/len + eps), then
r *= inv * w[e]) and the store (BF16 round to nearest even into dst, or back into x).

AOT gate: xcrun metal -c -> 10,032-byte AIR, rc=0.  Two Metal constraints cost a round each and are worth remembering:
'long long' is not supported (use 'long'), and a kernel parameter must be a buffer/constant reference, not a plain value.

Not wired yet: the host side still has to bind the Ds4Finish struct (pointers as 64-bit gpuAddresses, strides as long), and the
mrope (rd > 0) and rounding (quant != 0) branches are not written - the host wrapper will decline those until they are, so the
engine names the line rather than skipping a step.
2026-10-23 19:37:54 +02:00
Niko1221 b5b4a97c0a metal: the mHC dense product wired - cublasSgemm dispatches an MSL kernel instead of aborting
ds4_model.cu:2737 is the engine's first dense call: cublasSgemm(OP_T, OP_N, n_mix, T, hcd, &one, fn, hcd, h, hcd, &zero,
mixes, 32) - mixes[t][m] = h[t] . fn[m], both operands F32, alpha 1, beta 0.  ds4_sgemm_tn (one thread an output element) plus
metal_host::sgemm_tn wrap the engine's buffers and dispatch it; cublas_v2.h calls it for exactly that case and keeps aborting
for everything else, so an unported shape fails loudly rather than computing something wrong.

With that the engine passes the dense product and names its next need: finish_kernel<false> (the plain variant - RMS norm,
mrope, optional in-place rounding, no KV pack) at ds4_kernels.cu:1898.

Queue so far, each step verified by the engine advancing: init_pre, embed, cublasSgemm, then finish_kernel<false>.
2026-10-23 19:35:42 +02:00
Niko1221 45cbb451e7 metal: the engine runs on Metal and names its kernel queue; two kernels ported
The DeepSeek engine now loads a real 48,786-tensor pack on the Mac, plans the staging (102.26 GB of experts in unified memory,
5,604 on the CPU lane), and executes until it needs a kernel that has no dispatch.  Two are ported:

* init_pre (ds4_kernels.cu:1865) - a three-line initialiser
* embed (ds4_kernels.cu:1873) - the BF16 gather with hc copies

each as MSL in metal/src/engine/msl/ds4_init.metal plus host glue in metal/src/engine/ds4_host.cpp (wrap the engine's own device
buffer through strata::gpu::host_view + mtl::Context::wrap, then Dispatch), wired into the shared call site under
#if defined(STRATA_METAL).  The engine advanced past both and now asks for finish_kernel at :1896.

Two infrastructure fixes were needed and are in here:

* runtime.cpp: StreamImpl's encoder/command-buffer state is touched from the loader and the compute path, so two encoders could be
  open on one command buffer and Metal asserted ('A command encoder is already encoding').  A std::recursive_mutex around
  encoder()/flush()/encode_copy()/encode_fill()/encode_dispatch() fixes it - recursive because encode_copy calls flush (a plain
  mutex deadlocked, which cost one round).
* ds4_host.cpp compiles its MSL relative to the context's kernel_dir (metal/kernels), so the engine's own kernels are
  ../src/engine/msl/<file>.

Known: this build has STRATA_NATIVE_EXPERTS=OFF, so the engine routes all experts to VRAM ('this build has no ggml-cpu'); the
CPU-lane path is untested on the Mac.
2026-10-23 19:32:42 +02:00
Niko1221 15f1fe0548 metal: a one-Context allocator for the dsv41 engine, opt-in so the default path is untouched
metal/src/engine/ds4_ctx.cpp owns one mtl::Context, allocates through Context::alloc and remembers gpuAddress -> Buffer, so the
engine's device memory can live in the same residency set as the dispatches.  Off unless STRATA_DS4_METAL_CTX=1, which keeps the
current behaviour and makes the two paths comparable the moment a pack exists.  Build rc=0.

It exists because strata::gpu (what metal_compat's cudaMalloc maps to) is a CUDA-shaped facade with its own device state: a dsv41
launch through mtl::Context would otherwise read a foreign residency set.  Flipping cudaMalloc/cudaFree to this under the env var
is the two-line change, and it is deliberately left for a session that can test it against a loaded pack.
2026-10-23 18:56:42 +02:00
Niko1221 5f6d49e959 metal: the first dsv41 host wrapper - compile, wrap, dispatch (and the one-Context invariant it exposed)
metal/src/engine/ds4_host.cpp compiles ds4_quant.metal once through mtl::Context::compile, wraps the engine's buffers with
cx.wrap(strata::gpu::host_view(...)), builds a Dispatch (args = buffer, inline ldx, q, d; grid = n/32; tg = 32) and runs it with
cx.run().  Build rc=0 - the port's integration path is now code, not a plan.

It also exposed the last architectural question, which the comments record: the Qwen engine allocates and dispatches entirely
inside mtl::Context (cx.alloc / cx.wrap / cx.run) and uses strata::gpu only for a purge helper, so a dsv41 kernel must be given
the gpuAddress of a Buffer allocated by the SAME Context that encodes its dispatch.  Until metal_compat's allocations go through
that Context, the wrapper compiles but would dispatch into a foreign residency set - the plan for that is written up in
RIGS/m1002.md rather than guessed at here.
2026-10-23 18:54:37 +02:00
Niko1221 2cc55f6165 metal: the IQ2_S expert kernel, tiled - 515.7 GB/s effective, 89 % of the streaming ceiling
metal/tests/dsv41/iq2s_t8_parity.mm: block 82 B (f16 d, 32 index bytes, 32 sign bytes, 8 qh, 8 scale), grid index assembled
from qs[4ib+l] plus two bits of qh[ib] (<< (8-2l) & 0x300), signs read at qs+32 in the block, scale alternating db[l/2] -
transcribed from ggml's dequantize_row_iq2_s.  Verified: 65,536 rows x 5,120 k x 8-token window, rel L2 2.86e-07, PASS.

Rate 515.71 GB/s effective (64.46 GB/s of raw weight traffic) - the fastest of the three expert formats and 89 % of the
579 GB/s streaming ceiling.  With IQ2_XXS (437.5) and IQ2_XS (467.8) the 89 GB of IQ2 weight in the 2.25bpw set now has
verified Metal kernels, which is 165-195 tok/s of headroom on expert traffic alone.
2026-10-23 18:51:05 +02:00
Niko1221 446a45f750 metal: the IQ2_XS expert kernel, tiled - 467.8 GB/s effective at 97 MB
metal/tests/dsv41/iq2xs_t8_parity.mm: block 74 B (f16 d, 32 uint16 qs, 8 scale bytes), grid = iq2xs_grid[qs & 511],
signs = ksigns_iq2xs[qs >> 9], two scales a group (sc & 0xf, sc >> 4) - transcribed from ggml's dequantize_row_iq2_xs and
verified against the CPU reference: rel L2 2.83e-07 at 24 MB and 2.88e-07 at 97 MB, PASS.

Rate: 117.4 GB/s effective on 24 MB (launch-dominated) and 467.8 GB/s effective on 97 MB, which is in line with IQ2_XXS's
437.5.  IQ2_XS is 24 GB of the 2.25bpw set and IQ2_XXS 49 GB, so the two largest expert formats are now ported.
2026-10-23 18:50:26 +02:00
Niko1221 1d21a0f60a metal: the tiled IQ2_XXS kernel - 437 GB/s effective in the engine's 8-token window, 5x the one-token form
metal/tests/dsv41/iq2xxs_t8_parity.mm decodes the weights ONCE per 8-token window and applies them to eight accumulators, which is
how the engine runs decode (DSpark's draft/verify rounds are 4-5 tokens, the prompt window is 8).  Same tables, same decode as
the verified one-token kernel, and the reference checks it: relative L2 2.91e-07 on sampled rows, PASS.

The number that matters: 437.52 GB/s effective (54.69 GB/s of raw weight traffic) against 87.71 GB/s for the warp-per-row form.
At 2.33 GB of expert reads a token that is ~5.3 ms of expert time, i.e. ~188 tok/s of headroom - the first measurement that says
the 50 tok/s target is reachable, and it says the expert path must be the T-window form from the start, as the repo's moe_mm is.
2026-10-23 18:49:12 +02:00
Niko1221 9c1e61a6f3 metal: the IQ2_XXS expert GEMV ported - 87.7 GB/s, and why it cannot go faster as a warp-per-row kernel
metal/tests/dsv41/iq2xxs_gemv_parity.mm decodes IQ2_XXS exactly as ggml does (block 66 B, db = d*(0.5 + (aux32[1] >> 28))*0.25,
grid = iq2xxs_grid[aux8[l]], signs = ksigns_iq2xs[(aux32[1] >> 7l) & 127]) and dots it with the activations.  Measured against a
CPU reference that uses the same tables: 65,536 rows x 5,120 k, relative L2 2.88e-07, PASS, 87.71 GB/s.

Two findings that shape the expert path:

* the group headers advance 4 bytes and read 8, so an 8-byte uint2 load is misaligned on odd groups.  Both vector-load variants
  failed (L2 1.38 and 0.985) and were slower; byte assembly is the correct access pattern, as in CUDA's own decoder.
* 87.7 GB/s is 4.8x slower than the BF16 GEMV's 425 GB/s for the same traffic, because the grid decode costs per byte.  At 2.33 GB
  of expert reads a token that caps decode near 38 tok/s - below the 50 target - so the expert path must be tiled (one weight read
  feeding several tokens, as the repo's moe_mm does) rather than one row per warp.

That is a measured design constraint now, not an assumption.
2026-10-23 18:48:35 +02:00
Niko1221 45faf57702 metal: ggml's i-quant grid tables in MSL, generated from the pinned header
metal/src/engine/msl/ggml_grids.h carries kmask_iq2xs[8], ksigns_iq2xs[128], iq2xxs_grid[256], iq2xs_grid[512], iq2s_grid[1024],
iq3xxs_grid[256] and iq3s_grid[512] as MSL constant arrays.  metal/tools/gen_grids.sh produces them from
third_party/ggml/ggml-common.h (3cf03257f, GGML_TABLE_BEGIN/END blocks), so the Metal expert kernels use bit-identical tables
to CUDA instead of a hand copy - the same discipline that caught the half-rounded-scale error in the Q8 kernel.

Every expert format in the 2.25bpw set (IQ2_XXS 49 GB, IQ2_XS 24 GB, IQ2_S 16 GB, IQ3_XXS) is grid-based, so nothing in the
expert path could be written without these.
2026-10-23 18:45:35 +02:00
Niko1221 680724b87a metal: the Q8_0 weight GEMV ported - 173 GB/s, and the reference has to use the file's half-rounded scale
metal/tests/dsv41/q8_gemv_parity.mm: Q8_0 blocks (32 int8 + one f16 scale) times F32 activations, one warp a row, lane-strided
blocks.  16,384 rows x 5,120 k, relative L2 8.11e-08, PASS, 173.52 GB/s and 326.62 GFLOP/s (0.514 ms for 89 MB).

The first run measured 1.85e-04 and looked like a kernel bug; it was the CPU reference using the exact float scale while the
kernel reads the half-rounded scale from the file.  Matching the rounding took it to 8e-08 - the same class of drift that would
quietly cost quality if the port did not reproduce the file's arithmetic exactly.

Three kernels are now verified with reproducible parity tests: the Q8 activation quantiser, the BF16 GEMV and this one.
2026-10-23 18:45:00 +02:00
Niko1221 a445418e5e metal: the BF16 GEMV ported and measured - 83 GB/s warp-per-row, and why the expert path cannot stop there
metal/tests/dsv41/bf16_mv_parity.mm compiles the MSL version of the BF16 matrix-vector product (ds4_gguf.cu:1043's
mx_mv_kernel, single job: one warp a row, lane-strided k, warp reduction), dispatches it on the GPU and compares against a
double-precision reference: 2048 rows x 5120 k, relative L2 1.80e-07, PASS.

It also prints throughput: 83.19 GB/s and 83.2 GFLOP/s, 0.252 ms for 21 MB of BF16 weights.  That is a naive kernel (no
threadgroup tiling) against a GPU that streams 579 GB/s in rt_bench, so 7x of headroom remains - and the number matters:
2.33 GB of expert reads a token at 83 GB/s caps decode near 36 tok/s, which is exactly why the expert path has to use the
tiled multi-token kernels (the repo's gemv/moe_mm family) rather than a warp-per-row matvec.

A second measurement lesson: per-row relative error of ~1e-3 on a 5120-term float dot is expected cancellation, not a bug;
the standard measure is relative L2 over the result, which is what both parity tests now use.
2026-10-23 18:43:21 +02:00
Niko1221 baf261deca metal: the verified dsv41 activation quantiser, in the engine's kernel tree
metal/src/engine/msl/ds4_quant.metal carries the Q8 quantiser that metal/tests/dsv41/q8_quant_parity.mm proved bit-exact (4096
blocks, 131,072 values, 0 differences).  It is the first dsv41 kernel in the engine's own MSL directory, next to the Qwen
engine's mtp/ple kernels, so the runtime loader will find it by name when the launch is wired.

The comment records the driver quirk that cost the debugging: threadgroup_position_in_grid arrived as 0 in every threadgroup
on this Mac, so the block index comes from thread_position_in_grid / 32.
2026-10-23 18:41:43 +02:00
Niko1221 6bfcfa777f metal: the first dsv41 kernel ported and bit-exact - the Q8 activation quantiser
metal/tests/dsv41/q8_quant_parity.mm compiles the MSL version of q8_quant_kernel (ds4_gguf.cu:201) at run time, dispatches it
on the GPU and compares every int8 against the same arithmetic on the CPU.  4096 blocks, 131,072 values: 0 differ, worst
scale error 4.6e-04 (half rounding).  PASS.

Two bugs found on the way, both in the test harness rather than the kernel: indexing with thread_position_in_grid where the
threadgroup id was meant, and passing ldx = 1 where the stride between blocks of 32 values is 32 (which made block b read a
window starting at b instead of 32b - the giveaway was block 1 reproducing block 0's values shifted by one).

This is the pattern every remaining kernel follows: an MSL function, a pipeline from it, the args as buffers, one dispatch,
and a CPU reference in the same program.
2026-10-23 18:39:27 +02:00
Niko1221 ca903c124a metal: a launch with no Metal dispatch names its own file and line
strata_ds4_launch is a macro that passes __FILE__ and __LINE__ to strata_ds4_launch_at, so the abort message reads
'src/dsv41/ds4_iq.cu:1234' rather than a generic line.  Each run of the engine is then an exact work item instead of a guess
about which of the 146 launches is next, and the message is machine-readable from a log.  Build rc=0, strata_ds4 = 1,412,984 B.
2026-10-23 18:37:31 +02:00
Niko1221 883d0000a7 metal: the dsv41 engine links and runs on the Mac
Two things made this possible:

* the launch syntax.  146 kernel launches (119 in the sources, 27 in the vendor tree) are now
  strata_ds4_launch(kernel, grid, block, shared, stream, args...), a call a C++ compiler can parse, with a macro that forces
  the global name past argument-dependent lookup and a body that stops the run naming the kernel until a Metal dispatch exists.
  Every source then compiles as C++ through include/metal_compat - .cu files included, since the Metal project has no CUDA
  language and was silently skipping them.
* the last CUDA names the kernel files needed: __grid_constant__, cudaDataType, __umulhi/__mulhi, the bitwise atomics,
  __halves2half2 / __half2half2 / __low2half / __high2half / __low2float / __high2float / __hadd2 / __hmul2 / __hfma2 /
  __hneg2, the occupancy query and the stream-capture status.

The EXL3 legacy path (v1.1.8's kernels, NVIDIA before sm_80) is defined in metal/src/engine/exl3_legacy_stub.cpp: four of its
seven entry points abort with their name, three answer as the declarations require.  The 2.25bpw text model does not use EXL3.

Result: build rc=0, 0 undefined symbols, strata_ds4 = 1,410,552 bytes, Mach-O arm64, and it runs (its own usage text).  Next
is the run-time loop: a pack reports the first kernel it needs, implement it in MSL, repeat.
2026-10-23 18:35:00 +02:00
Niko1221 d692839177 metal: a strata_ds4 target, and what it proves
The DeepSeek host sources now build inside the Metal project as their own executable, with the .cu files compiled as C++ through
include/metal_compat.  ninja reaches step 23/35 and ds4_model.cu (8,232 lines, host only) builds; the first failure is a <<<>>>
launch in ds4_gguf.cu, which is the port's real boundary.

The measured size of the remaining work: 101 kernel launches to convert to Metal dispatches and 78 kernel bodies to write in MSL
(text only; the vision tower adds 21 and is optional).  ds4_model.cu has zero of both.

Two gotchas from building it: run_locked.sh returns exit code 69 for builds that are fine (run ninja directly), and the Xcode
licence line on stderr is noise - the toolchain compiles without accepting it.  Run with -j8 directly, the host sources compile in
about a minute.
2026-10-23 18:26:03 +02:00
Niko1221 aaf3aa3a7c metal: the last twelve CUDA names (dim3, funnel shift, transcendentals, ldg/ldcv, shfl_up) - no undeclared name left
A full-list compile (-ferror-limit=0) found 350 errors behind the 20-error display cap, and they collapsed into twelve missing
families.  With dim3, __threadfence_system, __funnelshift_r/l, rsqrtf, __expf/__logf/__sinf/__cosf, sincosf, __ldg, __ldcv,
__float2int_* and __shfl_up_sync in cuda_device_builtins.h, plus the C math names in scope, the counts are
ds4_iq 52, ds4_gguf 42, ds4_kernels 132, ds4_vision 27 (was 66/67/185/32) AND the distinct-names list is now EMPTY: what is
left is not a missing name but device construct - the <<<grid, block>>> launch syntax and shared-memory declarations that a
C++ compiler cannot parse as written.

That is the boundary of the host-compile audit, which was its purpose: the API surface is complete, and the remaining work is
the MSL implementation itself.
2026-10-23 18:14:32 +02:00
Niko1221 dc61a065e3 metal: finish the CUDA API surface the token path names, enumerated by the compiler
Twenty rounds of 'compile ds4_iq.cu as C++ and fix the first name it cannot find' produced the complete list, and it is small:
__byte_perm and the bit/int intrinsics, the SIMD video instructions with their exact per-lane semantics (__vsub4,
__vsubss4, __vadd4, __vaddss4, __vcmpeq4, __vcmpgtu4, __dp4a), the rn float helpers, integer min/max, __align__, the
vector types beyond float2/float4, the CUDA device built-ins for the host compile (threadIdx/blockIdx/__syncthreads/__shfl_*
- labelled as host-only, nothing there is a kernel), and wmma in CUDA 12's tag style whose every call aborts naming the
simdgroup-matrix equivalent.

ds4_model.cu stays at 0 errors.  The four kernel sources are at the compiler's 20-error display cap; what is left behind it
is mma_sync's signature and single scalars (__float2int_rn), not structure.  Next is the MSL work itself.
2026-10-23 18:13:33 +02:00
Niko1221 e46b149ae5 metal: fp8 conversion modes, the round-to-nearest float helpers, and an mma.h placeholder
ds4_numerics.cuh names __NV_E4M3/__NV_SATFINITE and __half_raw, ds4_q6k.cuh names __fmul_rn/__ushort_as_half, and both kernel
files include mma.h: cuda_bf16.h now carries the fp8 enums and includes cuda_fp16.h (for __half_raw), cuda_fp16.h carries the
five rn helpers and __ushort_as_half, and mma.h says what it is (the wmma surface does not exist on Metal; the token path
does not use it, and an instantiation fails loudly).

State after this: ds4_model.cu 0 errors; the four kernel files are at the compiler's 20-error limit with a shorter list each -
the remaining class is the device code itself (block decoders, shuffles, shared memory), which is the MSL work.
2026-10-23 18:10:48 +02:00
Niko1221 18c6075562 metal: the half-precision and vector-type surface (cuda_fp16.h, vector_types.h, cuda_fp8.h)
The kernel sources' single remaining error was cuda_fp16.h not existing; with it, vector_types.h and cuda_fp8.h in place they
get past the wall and show their real errors - ds4_iq 7, ds4_gguf 7, ds4_kernels 20, ds4_vision 20 - which is now the work
queue for the Metal port.  ds4_model.cu stays at 0.

Both copies (this branch and the box) carry the box's version of cuda_fp16.h; an earlier local edit had failed on a path and
this commit syncs them.
2026-10-23 18:08:54 +02:00
Niko1221 a798d03858 metal: the dsv41 host side compiles - ds4_model.cu goes from 20 errors to 0
The last layer of the compat surface: the CUDA function-space qualifiers (__host__/__device__/__global__/__shared__/
__constant__/__forceinline__/__launch_bounds__), cpu_set_t's CPU_ZERO/CPU_SET for the macOS branch of ds4_os.hpp,
CUBLAS_GEMM_DEFAULT_TENSOR_OP, a cudaMemcpy2D that reports failure rather than copying nothing, and a cublasGemmEx that takes
any argument list (the engine passes cuBLAS's full 19-argument spelling) and aborts naming kernels/dense_mm.metal.

With that, src/dsv41/ds4_model.cu - 8,232 lines, host only - compiles against metal/include/metal_compat on the M5 Max.  The
four kernel sources are one error each, and the kernel work itself (MSL bodies for 27 + 76 + 33 device functions, plus the
dense GEMM and a pitched copy at the runtime) is what remains before a first token.
2026-10-23 18:07:34 +02:00
Niko1221 bb60de0e8e metal: ds4_os.hpp takes the macOS shim at the top of the file, not inside the Windows block
The include had landed between '#ifndef NOMINMAX' and '#include <windows.h>', so on a Mac it was never reached - that is why
the audit still showed every Linux name undeclared.  Moved after '#pragma once': ds4_model.cu goes from 20 to 15 errors.

The 15 left are four CPU_ZERO/CPU_SET uses (macOS has no cpu_set_t macros) and the __host__/__device__ qualifiers that
cuda_runtime.h does not define yet; both are a few lines and are the next step, then the Linux surface of the engine's
storage helpers is done.
2026-10-23 18:05:41 +02:00
Niko1221 910af91838 metal: a macOS stand-in for the Linux OS surface ds4_os.hpp uses (half done - not yet effective)
ds4_os_apple.h maps O_DIRECT (-> 0, F_NOCACHE does the work), posix_fadvise (-> 0), mincore (-> ENOSYS, 'unknown' is the
answer the header's contract already allows), MADV_HUGEPAGE and SYS_mbind (no-ops) and the CPU-affinity calls (no-ops on a
kernel that schedules on its own), and ds4_os.hpp includes it at the top under __APPLE__.

State: included, but ds4_model.cu still reports the same 20 errors at the same uses, so something in the include order is
not taking effect yet - the next session's first job is one -E on ds4_os.hpp to see which definitions win.  Not claiming it
works.
2026-10-23 18:04:59 +02:00
Niko1221 bcb2133a28 metal: the dsv41 core's CUDA surface, part one (bf16/fp8 conversions, the cuBLAS names, the compile audit)
Compiling src/dsv41/*.cu against metal/include/metal_compat as C++ is how the DeepSeek-on-Metal port is scoped, and it is
much smaller than assumed: ds4_model.cu is 8,232 lines of *host* code with zero device markers, and the six non-kernel
sources already compile.  What was missing at the compat layer:

* cuda_bf16.h - __nv_bfloat16/__nv_bfloat162/__nv_fp8_storage_t and the four conversions the engine uses
  (__float2bfloat16_rn, __bfloat16_as_ushort, __nv_cvt_float_to_fp8, __nv_cvt_fp8_to_halfraw), E4M3 with round-to-nearest.
* cublas_v2.h - the nine GEMM calls (7 cublasGemmEx, 2 cublasSgemm) and their enums.  The dense GEMM itself is NOT wired:
  it aborts with a message naming kernels/dense_mm.metal (which exists and passes its parity test) rather than returning a
  silently wrong result.

After these, ds4_model.cu's remaining 20 errors are all in ds4_os.hpp - the Linux-only OS surface (O_DIRECT,
posix_fadvise, mincore, MADV_HUGEPAGE, SYS_mbind, cpu_set_t/sched_setaffinity) - which needs a __APPLE__ branch.
2026-10-23 18:04:03 +02:00
Niko1221 f0f056b9bb dsv41 mac: the CPU-only build works on arm64 (x86 kernels guarded, the AVX2 row entry points answer no)
strata_kernels_cpu compiled expert.cpp / q2_avx2.cpp / iq_avx*.cpp with hard-coded -mavx512f / -mavx2, so on an arm64 host the
build died with 'unsupported option -mavx512f for target arm64-apple-darwin27.2.0'.  STRATA_CPU_KERNELS_X86 now decides:
on x86 the same sources and ISA flags as before, elsewhere the portable files plus src/kernels/cpu/iq256_portable.cpp, which
defines iq256_supported() = false and iq256_rows() = abort (callers check the first), so ds4_gg_cpu.cpp keeps ggml-cpu's
rows - which have their own NEON kernels on Apple Silicon.

Verified on the M5 Max: configure rc=0, build rc=0, ds4_gg_bench runs IQ3_XXS at 83.9 GB/s through ggml-cpu's rows (the
x86 EPYC does 60.5), 0 of 1,567,344 values differ in bits against ggml and 7.14e-09 against a double reference.  The x86 path
is untouched: the guard only selects which sources join the library.
2026-10-23 17:53:59 +02:00
1713 changed files with 215546 additions and 18622 deletions

No files matched your search

+5
View File
@@ -17,6 +17,11 @@ __pycache__/
/mtp/
/models/
/Strata-data/
# the folders docker/entrypoint.sh creates in a mounted data/ (a link to llama.cpp, the models, the keys): never part of a build (#3644)
/data/install/
/data/state/
/data/home/
/data/Strata-data/
third_party/llama.cpp/
third_party/*.zip
*.done
+3
View File
@@ -25,3 +25,6 @@ docker/compose/* text eol=lf
# Apple tools (codesign, plutil) read these: LF only, wherever the repository is cloned
*.plist text eol=lf
# vendored KaTeX (serve/web/vendor): the files are hashed byte for byte (PROVENANCE.json), never convert their line endings.
serve/web/vendor/** -text
+1
View File
@@ -1 +1,2 @@
github: Niko1221
buy_me_a_coffee: strataengine
@@ -0,0 +1,68 @@
name: AMD split engine report
description: Tell us whether the all-cards split engine works on your AMD cards (it prints a line asking for this on cards Strata has not run it on)
title: "[Engine] AMD split engine: <your card> x <count>"
labels: ["engine", "amd-split-report"]
body:
- type: markdown
value: |
Strata runs the all-cards split engine by itself on Radeon AI PRO R9700 / RX 9070 (gfx1201). On other AMD cards (RX 9060, RX 7000, the 780M and similar) it only starts when you ask for it with `strata launch --split-engine on`, and it prints one line asking you to report whether it works. We cannot test those cards, so your report is how we learn.
Write only what you saw. A report that says "it did not start" is as useful as one that says "it works".
- type: input
id: card
attributes:
label: Card model and count
description: For example "2 x RX 7900 XTX 24 GB" or "3 x RX 9060 XT 16 GB"
validations:
required: true
- type: input
id: arch
attributes:
label: Architecture (the gfx name)
description: From the engine's line, or `rocminfo | grep gfx`. For example gfx1100.
validations:
required: true
- type: input
id: rocm
attributes:
label: ROCm / driver version
description: Strata's engine package brings its own ROCm libraries; the kernel driver is yours (`uname -r`, `amd-smi version`).
validations:
required: true
- type: input
id: model
attributes:
label: Model and quantization
description: The model Strata loaded, for example Qwen3.8-Flash-Next UD-Q4_K_XL or the IQ3_S set.
validations:
required: true
- type: textarea
id: selftest
attributes:
label: The self-test line
description: Paste the line from the engine's log that starts "The split engine's self-test" (passed or failed). The log is in the run folder shown at launch; `strata launch` also prints it.
render: text
validations:
required: true
- type: dropdown
id: result
attributes:
label: What happened
options:
- It started and the answers look right
- It started and the answers look wrong
- The self-test failed, so the normal layer split started
- It failed to start (not the self-test)
- It started and crashed or hung later
validations:
required: true
- type: input
id: speed
attributes:
label: Speed, tok/s (the split engine, then the layer split with `--split-engine off` if you tried both)
description: For example "decode 61 tok/s, layer split 48 tok/s, one user".
- type: textarea
id: more
attributes:
label: Anything else
description: Wrong answers (the prompt and what you got), crashes (the last lines of the engine log), or what you changed.
+1 -1
View File
@@ -66,7 +66,7 @@ body:
attributes:
label: Strata version
description: "In the app: Settings > About. In the Terminal: `strata --version`. From source: the About tab of the web page, or `engine` in `GET /v1/status`, or the git commit."
placeholder: "Strata 0.2.3 · engine 0.1.48"
placeholder: "Strata 0.2.12 · engine 0.1.58"
validations:
required: true
- type: input
+1 -1
View File
@@ -13,7 +13,7 @@ on:
workflow_dispatch:
inputs:
release_tag:
description: 'Release tag whose engine packages go into the images (for example v0.2.3)'
description: 'Release tag whose engine packages go into the images (for example v0.2.7)'
required: true
type: string
flavors:
+3 -1
View File
@@ -20,7 +20,9 @@ permissions:
jobs:
metal-link:
runs-on: macos-14
# the macOS 26 SDK: metal/src/mtl_rt.mm names MTLGPUFamilyMetal4 (and MTLCompileOptions.mathMode, macOS 15) behind @available,
# which is a run-time check; the names must still be in the SDK the engine is compiled against
runs-on: macos-26
timeout-minutes: 45
env:
# setup.py's LLAMA_CPP_COMMIT and metal/setup_mac.py's metal-cpp pin
+10
View File
@@ -63,6 +63,10 @@ build-vision-cuda12.bat
/.ccache/
/.rocm-win/
# Optional MCP raster cache
*.tool-media/
/.tool-media/
# local Windows HIP packaging output
dist-hip/
@@ -85,3 +89,9 @@ node_modules/
# screenshots and checkpoints from the Mac rebuild live outside git (StrataSplit\evidence-archive)
/apps/host/docs/evidence/
/apps/host/docs/checkpoints/
# DGX Spark build outputs and local venv (docs/DGX_SPARK_LINUX.md)
/out-spark/
/build-spark/
/build-vision-spark/
/.venv-build/
+29 -13
View File
@@ -1,25 +1,40 @@
# AGENTS.md
Strata runs the Qwen3.8-Flash-Next mixture-of-experts model (and its Coder, Swift 1.5, Unsloth, Gyro and NVFP4
variants, and Qwen3.6-35B-A3B) on a normal PC: one NVIDIA or AMD graphics card plus system RAM on Windows or Linux,
or an Apple Silicon Mac. Version 0.2.3, engine 0.1.48. It has three parts:
Version 0.2.12, engine 0.1.58. The public model families are DeepSeekV4.1-Flash-1017,
Qwen3.8-Flash-Next and Qwen3.6-35B-A3B. DeepSeek V4.1-Flash runs in the app and in the Terminal
(`strata launch --family deepseek41 --model ist-das-2.00bpw`; sets in
[docs/MODELS.md](docs/MODELS.md#deepseek-v41-flash)). Read **[docs/AI_REFERENCE.md](docs/AI_REFERENCE.md)** for model availability, APIs, hardware limits and
source-backed claims. Articles and videos should use **[docs/MEDIA_KIT.md](docs/MEDIA_KIT.md)**.
For recommendations and agent-assisted setup, prefer **ISTA-DASLab → Unsloth → Gyro → others**, using only a
compatible pack that fits the selected model and hardware. ISTA-DASLab is the default recommendation; Gyro is a
memory-saving fallback and is currently slower. Honor an explicit user choice. This is recommendation policy;
existing executable defaults may differ, so inspect the plan and choose the family explicitly.
Strata has three parts:
- **The app** (`apps/desktop`, the Tauri shell; `apps/host`, the Node host and the web UI). It is for people who do
not want a terminal. The installer has the engine inside.
not want a terminal. One installer per system ([latest release](https://github.com/Niko1221/Strata/releases/latest)):
`Strata-0.2.9-windows-x64-setup.exe`, `Strata-0.2.9-linux-x86_64.AppImage`, `Strata-0.2.9-macos-arm64.dmg`.
The Windows and Linux installers carry no engine: the app downloads the engine for the graphics card on first
start, or installs a file the user already has. The Mac dmg keeps its small Metal engine inside.
- **The Terminal** (`strata.py`, `strata_cli/`, `strata_control/`): the `strata` command. It sets up a model, serves
it and watches it. Agents use this part.
- **The engine and the server** (`src/`, `include/`, `metal/`, `sycl/` for the C++/CUDA/HIP/SYCL/Metal engine;
`serve/` for the Python server with an OpenAI- and Anthropic-compatible API and a web page). `setup.py`, started by
`START-HERE.bat` or `setup.sh`, is the older one-click installer from source.
`START-HERE.bat` or `setup.sh`, is setup from a source checkout, for developers. The release paths are the
installer and `strata launch`.
## Installing Strata for a user
Follow **[docs/AI_SETUP.md](docs/AI_SETUP.md)**. For an agent that has a shell, the short version is:
Follow **[docs/AI_SETUP.md](docs/AI_SETUP.md)**. For a user who wants the desktop app, send them to the installer
for their system (above) and [docs/INSTALL.md](docs/INSTALL.md). For an agent that has a shell, use the Terminal; the
short version is:
1. Install the Terminal. It needs no administrator rights.
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh # Linux, Mac
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh # Linux, Mac
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex # Windows PowerShell
```
@@ -27,27 +42,28 @@ Follow **[docs/AI_SETUP.md](docs/AI_SETUP.md)**. For an agent that has a shell,
2. Check what would happen. Nothing is changed:
```sh
strata launch --plan --model q2_0
strata launch --plan --family qwen --model q2_0
```
3. Set up and serve without questions. It downloads the engine for the card and the model (tens of GB), starts the
server, prints the addresses and returns. The server keeps running.
```sh
strata launch --model q2_0 --yes --no-tui
strata launch --family qwen --model q2_0 --yes --no-tui
```
Useful flags (all of them: `strata launch --help`): `--family`, `--model-path` (files the user already has),
`--data-dir`, `--context 32k|64k|128k`, `--vision off|on|gpu|cpu`, `--parallel N`, `--gpu 0,1`,
`--access local|lan|cloudflare|tailscale`, `--port`, `--api-key-env NAME`, `--new-key`, `--save-preset NAME`,
`--output json`. `strata launch --status` shows what is serving, `strata launch --stop` stops it, and
`--speed faster|full` (DeepSeek), `--output json`. `strata launch --status` shows what is serving, `strata launch --stop` stops it, and
`strata launch --show-key` prints the API key.
4. Connect the user's apps: `strata connect` lists them, `strata connect <app> --check` sends one prompt and one tool
call the way that app does.
Keep the server on `127.0.0.1` unless the user asks for more. Beyond it an API key is recommended: `--access lan` makes one for you, and `--no-api-key` serves without one (a warning, never a refusal; on a public tunnel the warning is stronger). The old route (clone
the repository and run `START-HERE.bat --yes` or `./setup.sh --yes`) still works and is the fallback when the
Terminal cannot run. Strata's MCP server ([docs/MCP_SERVER.md](docs/MCP_SERVER.md)) offers the same steps as tools.
Keep the server on `127.0.0.1` unless the user asks for more. Beyond it an API key is recommended: `--access lan` makes one for you, and `--no-api-key` serves without one (a warning, never a refusal; on a public tunnel the warning is stronger). Source setup (`START-HERE.bat`, `setup.sh`) is the fallback for a developer or for a system with no release package; it is
documented in [docs/SOURCE_INSTALL.md](docs/SOURCE_INSTALL.md). Those wrappers exist in a source checkout, not in the
released app. Do not tell an app user to find a `.bat` file.
Source setup does not make an unsupported model or backend compatible. Strata's MCP server ([docs/MCP_SERVER.md](docs/MCP_SERVER.md)) offers the same steps as tools.
## Working on the code
+717 -36
View File
File diff suppressed because it is too large. Load diff
+2
View File
@@ -68,6 +68,8 @@ RUN set -e; \
# ---- 2. the Terminal (the release archive, built here)
FROM ${RUNTIME_BASE} AS terminal
ARG STRATA_COMMIT=
ENV STRATA_BUILD_COMMIT=${STRATA_COMMIT}
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends python3 ca-certificates git && rm -rf /var/lib/apt/lists/*
COPY . /src/
+135 -352
View File
@@ -1,419 +1,202 @@
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="apps/host/public/brand/strata-lockup-dark.svg">
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="380">
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="340">
</picture>
</p>
<p align="center"><a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <b>Deutsch</b> · <a href="README.fr.md">Français</a> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português</a></p>
<h3 align="center">Ein Spitzenmodell auf deinem eigenen Gaming-PC</h3>
<p align="center">Unterstützt NVIDIA, AMD, Intel · Windows, Linux, Mac · kostenlos und Open Source</p>
<h3 align="center">Leistungsstarke KI auf deinem eigenen Computer.</h3>
<p align="center">Chatten, programmieren und mit Bildern arbeiten, lokal.</p>
<p align="center">
<a href="https://github.com/Niko1221/Strata/releases"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Latest release"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT license"></a>
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="GitHub stars"></a>
<a href="#modelle">Modelle</a> · <a href="#installation"><b>Installation</b></a> ·
<a href="#hardware">Hardware</a> · <a href="#tools">Tools</a> ·
<a href="#hilfe">Hilfe</a> · <a href="#strata-unterstützen">Unterstützen</a>
</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>Ein Voxel-Pagodengarten aus einem einzigen Prompt, auf einer RTX 5070 mit Strata (IQ3_S, 128K Kontext) ·
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">ganzes Video (49 s)</a></sub></p>
<p align="center">
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?label=download&color=0ab887" alt="Strata herunterladen"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT-Lizenz"></a>
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor-EA4AAA?logo=githubsponsors&logoColor=fff" alt="Strata über GitHub Sponsors unterstützen"></a>
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy_me_a_coffee-FFDD00?logo=buymeacoffee&logoColor=000" alt="Strata auf Buy Me a Coffee unterstützen"></a>
</p>
Strata lässt **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** laufen, ein Modell mit 125
Milliarden Parametern, für das man sonst einen Server braucht, auf einem normalen PC. Es chattet, schreibt Code, liest
Bilder und arbeitet mit deinen Apps und Coding-Agents zusammen. Deine Chats und Dateien bleiben auf deinem PC.
<p align="center"><sub><a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <b>Deutsch</b> · <a href="README.fr.md">Français</a> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português</a> · <a href="README.pl.md">Polski</a></sub></p>
**Neu hier?** Sieh dir an, [was du brauchst](#was-du-brauchst), und [installiere dann die App](#option-a-die-strata-app).
**Kostenlos, Open Source und ganz in deiner Hand.** Nutze Strata auf dem Desktop oder mit deinen liebsten Coding-Tools. Kein Konto nötig.
- **Läuft auf einem Gaming-PC.** 32 GB Arbeitsspeicher (RAM) und Grafikspeicher zusammen reichen für den Anfang.
- **Passt zu deinen Werkzeugen.** Strata spricht die OpenAI- und die Anthropic-API, deshalb verbinden sich Claude Code, Codex CLI, OpenCode und andere Agents damit.
- **Liest Bilder.** Das ist standardmäßig eingeschaltet.
- **Kostenlos.** MIT-Lizenz, kein Konto, keine Cloud.
- **Privat.** Keine Telemetrie. Was Strata herunterlädt, wann es das Internet nutzt und wie es Downloads prüft, steht in [docs/SAFETY.md](docs/SAFETY.md).
## Modelle
Version 0.2.3 · Engine 0.1.48
Wir empfehlen **zuerst ISTA-DASLab**, danach **Unsloth** und andere Anbieter, soweit ein passendes Modell in deinen Speicher passt.
## Was du brauchst
<details>
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> &nbsp;<b>Qwen3.8-Flash-Next</b></summary>
**Du brauchst mindestens 32 GB Arbeitsspeicher (RAM) und Grafikspeicher (VRAM) zusammen.** Bei einem Mac oder einem Ryzen AI Max
ist das der gemeinsame Speicher. Mit mehr Speicher läuft ein größeres oder ein schnelleres Modell.
- Ein PC mit 32 GB und einer Grafikkarte mit 12-16 GB führt den Coder aus.
- Mit 64 GB RAM laufen alle Größen.
| Dein Computer | Details |
| Anbieter | Auswahl |
| --- | --- |
| **NVIDIA** GeForce RTX 20, 30, 40, 50 Serie (12 GB oder mehr) | Installiere die App oder das Terminal. |
| **AMD** Radeon RX 6800 und neuer, Radeon AI PRO R9700 (12 GB oder mehr) | [AMD-Karten](docs/AMD_HIP.md) |
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md), [Intel-Engine](docs/INTEL.md) |
| **Apple-Silicon-Mac** (M1 oder neuer, macOS 15 oder neuer) | [Mac](docs/MAC.md) |
| **AMD Ryzen AI Max (Strix Halo)** und andere Systeme mit gemeinsamem Speicher | [Strix Halo](docs/STRIX_HALO.md) |
| Ältere Karten und Prozessoren | [Ältere GPUs](docs/OLDER_GPUS.md), [Ältere CPUs](docs/INSTALL.md#older-cpus-experimental) |
| **ISTA-DASLab · empfohlen** | Q2\_0, IQ2\_XS, IQ3\_XXS, IQ3\_S. [Coder: IQ1\_M](docs/MODELS.md#coder) zum Programmieren. |
| **Unsloth** | UD-IQ4\_XS, UD-Q4\_K\_XL, UD-Q5\_K\_XL, UD-Q6\_K\_XL, Q8\_0 |
| **Gyro · agentionai** | [Gyro-S, Gyro-M](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF). Geringerer Speicherbedarf, aber langsamer. |
| **Weitere Optionen** | [Swift 1.5](docs/MODELS.md#swift-15), [NVFP4](docs/NVFP4.md), [OrcaRouter](docs/ORCA.md) |
Außerdem nötig:
[Größen, Speicherbedarf und Kompatibilität vergleichen](docs/MODELS.md).
- etwa 80 GB freier Speicherplatz (mit einer SSD startet es beim ersten Mal viel schneller)
- Windows 10 / 11, Linux oder macOS
- ein aktueller Grafiktreiber
</details>
Zwei oder drei Karten desselben Herstellers können sich ein Modell teilen ([Multi-GPU](docs/MULTI_GPU.md)). Strata teilt ein
Modell nie auf NVIDIA- und AMD-Karten auf. Die vollständige Liste: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
<details>
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> &nbsp;<b>Qwen3.6-35B-A3B</b></summary>
**Unsloth:** UD-Q4_K_XL, UD-Q6_K_XL, Q8_0. Das sind die derzeit dokumentierten Pakete für dieses Modell.
[Modellanleitung und Kompatibilität](docs/QWEN36.md).
</details>
<details>
<summary><img src="docs/media/models/deepseek.svg" width="22" alt="" align="middle"> &nbsp;<b>DeepSeekV4.1-Flash-1017</b></summary>
In der **Desktop-App** und im **Terminal**. Was nicht in den Arbeitsspeicher passt, wird von deiner SSD gelesen. Darum brauchst du viel freien Speicherplatz.
| Set (ISTA-DAS) | Download | Freier Speicherplatz nötig |
| --- | ---: | ---: |
| 1.75 bpw | 239 GB | etwa 245 GB |
| 2.00 bpw | 257 GB | etwa 265 GB |
| 2.25 bpw | 274 GB | etwa 280 GB |
Läuft auf NVIDIA RTX 30, 40 und 50 (Windows und Linux) und auf AMD Radeon AI PRO R9700 (Linux). Das Setup lädt die Dateien und die DeepSeek-Engine herunter und startet dann das Modell.
```sh
strata launch --family deepseek41 --model ist-das-2.00bpw --plan
```
Gemessen mit dem 2.00-bpw-Set auf Strata 0.2.5. „Ausgabe“ ist das Tempo der Antwort, „Prompt“ zeigt, wie schnell Strata deine Eingabe liest.
| Hardware | Ausgabe | Prompt |
| --- | ---: | ---: |
| RTX 5070 12 GB, 64 GB RAM | 4 bis 6 Token/s | etwa 110 Token/s (4K) |
| RTX 3060 12 GB, 46 GB RAM | 9 bis 11 Token/s | etwa 155 Token/s (4K) |
| 1x Radeon AI PRO R9700, 251 GB RAM | 21 bis 25 Token/s | 320 bis 360 Token/s (8K) |
| 4x Radeon AI PRO R9700 | 74 bis 77 Token/s | 400 bis 450 Token/s (8K) |
[Speicher, Festplatte und Details](docs/DEEPSEEK41.md) · [Versionshinweise](https://github.com/Niko1221/Strata/releases)
</details>
<a id="the-other-sizes"></a>
Unsicher, was passt? [Wähle ein Modell für deinen Computer](docs/MODELS.md#pick-by-memory).
## Installation
Such dir eine Option aus. Alle starten dieselbe Engine und nutzen dieselben Modelle.
<a id="option-a-the-strata-app"></a>
### Option A: die Strata-App
### Desktop-App
Der einfachste Weg. Die App bringt die Engine mit: installieren, ein Modell wählen, chatten. Lade die Datei für dein
System und deine Grafikkarte von der [Releases-Seite](https://github.com/Niko1221/Strata/releases/latest) herunter. Du bist
unsicher, welche du brauchst? Lies den Absatz unter der Tabelle.
**[Strata für Windows, Linux oder Mac herunterladen →](https://github.com/Niko1221/Strata/releases/latest)**
| System | Deine Grafikkarte | Datei zum Herunterladen |
| --- | --- | --- |
| **Windows 10 / 11** | NVIDIA RTX, Treiber 580 oder neuer | [`Strata-0.2.3-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda13.exe) |
| | NVIDIA, ältere Karte oder Treiber älter als 580 | [`Strata-0.2.3-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda12.exe) |
| | AMD Radeon RX 9000 Serie oder Radeon AI PRO R9700 | [`Strata-0.2.3-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip.exe) |
| | AMD Radeon RX 7000 Serie oder Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip-rdna3.exe) |
| **Linux** | NVIDIA RTX, Treiber 580 oder neuer | [`Strata-0.2.3-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda13.AppImage) |
| | NVIDIA, ältere Karte oder Treiber älter als 580 | [`Strata-0.2.3-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda12.AppImage) |
| | AMD Radeon RX 9000 Serie oder Radeon AI PRO R9700 | [`Strata-0.2.3-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx9070.AppImage) |
| | AMD Radeon RX 7000 Serie | [`Strata-0.2.3-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx7900.AppImage) |
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-strixhalo.AppImage) |
| **Mac** (Apple Silicon, macOS 15 oder neuer) | eingebaut | [`Strata-0.2.3-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-mac.dmg) |
Installiere die App, wähle ein Modell und leg mit dem Chat los. Es gibt einen Installer pro System. Beim ersten Start lädt die App die Engine für deine Grafikkarte herunter. [Hilfe zum Installer](docs/INSTALL.md#the-app).
**Wenn du unsicher bist:** Bei NVIDIA nimmst du `cuda13`, wenn dein Treiber Version 580 oder neuer ist, sonst `cuda12`.
`cuda12` ist für ältere Karten und ältere Treiber. Die Dateien `hip` und `amd` sind für AMD-Karten. Für andere AMD-Karten
(RX 6000) gibt es noch keine fertige App: siehe [AMD-Karten](docs/AMD_HIP.md).
<sub>Dateien: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
Danach:
<p align="center">
<img src="docs/media/app-chat.png" width="900" alt="Strata-Desktop-App mit Chat und Live-Codevorschau nebeneinander">
<br><sub>Chat, Dateien und Live-Vorschau in einem Fenster.</sub>
</p>
- **Windows:** Starte die `.exe`. Sie wird für deinen Benutzer installiert und braucht keine Administratorrechte. Wenn
Windows "Der Computer wurde durch Windows geschützt" meldet, wähle **Weitere Informationen** und dann **Trotzdem
ausführen** (der Installer ist noch nicht signiert).
- **Linux:** Mach die Datei ausführbar (`chmod +x`) und starte sie. Sie braucht FUSE 2: `sudo apt install libfuse2`
(Ubuntu 22.04) oder `libfuse2t64` (Ubuntu 24.04 und neuer).
- **Mac:** Öffne die `.dmg` und zieh Strata in den Ordner "Programme".
<a id="option-b-the-terminal-one-line"></a>
Auf dem ersten Bildschirm liest die App deinen PC aus und empfiehlt ein passendes Modell. Drück auf Installieren. Das Modell
ist ein großer Download (etwa 40-70 GB) und wird nach einer Unterbrechung dort fortgesetzt, wo er aufgehört hat. Wenn du
das Fenster schließt, beenden sich Strata und die Engine.
### Terminal-App
### Option B: das Terminal (eine Zeile)
Lieber im Terminal? Installiere Strata und starte dann **`strata launch`**.
Das Terminal ist der Befehl `strata`, gedacht für Skripte, Server und KI-Agents.
<details open>
<summary><b>Installationsbefehle</b></summary>
Linux und Mac:
**Linux / Mac**
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
```
Windows (PowerShell):
**Windows PowerShell**
```powershell
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
```
Das Skript prüft deinen PC, lädt das Terminal herunter, kontrolliert die SHA-256-Prüfsumme und entpackt es in deinen
Home-Ordner. Es braucht keine Administratorrechte. Öffne unter Windows danach ein neues Terminalfenster. Dann startest du
Strata:
Öffne unter Windows danach ein neues Terminal. Dann:
```sh
strata launch
```
Es stellt dir eine Frage nach der anderen: Modell, Kontext, Bilder, wer sich verbinden darf. Danach lädt es die Engine für
deine Grafikkarte herunter (signiert und vor der Nutzung probeweise gestartet), dazu das Modell, und beginnt zu laufen.
Für Skripte und KI-Agents gibt es Flags:
```sh
strata launch --model q2_0 --yes --no-tui
```
Wenn du das Installationsskript vor dem Ausführen lesen willst, lade es zuerst mit `curl -fsSL <address> -o install.sh`
herunter, lies es und führe dann `sh install.sh` aus. Alle Bildschirme und Flags: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
### Option C: deine KI richtet es ein
Füge das in Claude Code, Cursor, Codex, GitHub Copilot oder einen anderen Coding-Assistenten ein:
```text
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
```
Der Assistent prüft deine Grafikkarte, deinen Arbeitsspeicher und den Speicherplatz, wählt das passende Modell, installiert
und startet es und sagt dir, wie du deine Apps verbindest. KI-Werkzeuge können Strata auch über den
[MCP-Server](docs/MCP_SERVER.md) installieren, starten und stoppen.
Du willst es lieber selbst bauen? Siehe [Aus dem Quellcode bauen](#aus-dem-quellcode-bauen).
## Modelle
Die App und `strata launch` empfehlen ein Modell passend zu deinem Speicher. Es ist ein einziges Modell in mehreren
Größen, mehr oder weniger stark komprimiert. Kleinere Größen sind schneller. Größere Größen sind etwas klüger.
### Wenig Speicher? Probier Gyro
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) (von agentionai) ist die kleinste Möglichkeit, dieses Modell auszuführen.
| Größe | Benötigter Speicher (RAM + VRAM) | Download | Was es ist |
| --- | ---: | ---: | --- |
| **Gyro-S** | 27,6 GB | 58,5 GB | Für PCs, auf denen nichts anderes läuft. Die Qualität ist geringer als bei IQ2_XS. |
| **Gyro-M** | 35 GB | 92,0 GB | Ungefähr die Qualität von IQ3_XXS bei etwa 20 % weniger GPU-Speicher. Es schreibt Antworten langsamer als Q2_0: 16 % auf einer RTX 3060 mit 12 GB, 21 % auf einer R9700. |
Gyro läuft auf NVIDIA- und AMD-Karten (bei AMD experimentell) auf Intel-Arc-Karten unter Linux (getestet auf einer Arc A750) und ab 0.2.2 auf Macs mit Apple Silicon (Gyro-S auf einem Mac mini mit 32 GB: 9 Tokens/s, die Experten werden von der SSD gelesen). Details und Messwerte:
[docs/GYRO.md](docs/GYRO.md). Starte es mit `strata launch --model gyro-s`.
### Die anderen Größen
| Dein Speicher (RAM + VRAM) | Nimm | Hinweise |
| --- | --- | --- |
| **32 GB** | **Gyro-S**, oder den **[Coder](docs/MODELS.md#coder)** für Code | Der Coder braucht 32 GB RAM und behält die Hälfte der Experten, deshalb ist er außerhalb von Code schwächer |
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39,2 GB), oder **Gyro-M** (35 GB) | Swift denkt kürzer nach, bevor es antwortet. Gyro-M kommt der Qualität von IQ3_XXS nahe, schreibt Antworten aber 16-21 % langsamer als Q2_0 |
| **48 GB** | **IQ3_XXS** (47,0 GB) | Langsamer, besser |
| **64 GB** | **IQ3_S** (54,8 GB), das Original oder Swift 1.5 | Am langsamsten, am besten. Es erreicht in den veröffentlichten Tests das volle Modell |
| **96 GB oder mehr** | **UD-IQ4_XS** von Unsloth (etwa 72 GB RAM) bis **Q8_0** (etwa 136 GB), 4 bis 8 Bit | Näher am vollen Modell. Bei weniger RAM liest Strata einen Teil von der SSD, das ist viel langsamer |
Auch mit weniger RAM, als eine Zeile nennt, kann es funktionieren: Strata wechselt von selbst in seinen
[Modus für wenig RAM](docs/MODELS.md#a-big-graphics-card-and-little-ram). Rechne mit weniger Tempo, mit einer kleinen
Grafikkarte mit deutlich weniger.
Weitere Auswahl:
- **[Swift 1.5](docs/MODELS.md#swift-15)** ist eine Feinabstimmung, die kürzer nachdenkt, bevor sie antwortet, bei etwa gleicher Qualität und gleichem RAM-Bedarf.
- **[NVFP4](docs/NVFP4.md)** ist NVIDIAs 4-Bit-Version für NVIDIA-Karten. Sie braucht etwa 80 GB RAM, um alle Experten im RAM zu halten.
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** ist ein kleineres, anderes Modell, das Bilder liest und mehrere Anfragen gleichzeitig beantworten kann.
- **[OrcaRouters Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** richtest du von Hand ein.
Alle Größen, Downloads und was wo läuft: [docs/MODELS.md](docs/MODELS.md).
### DeepSeek V4.1-Flash (im Terminal, ab 0.2.4)
[DeepSeek V4.1-Flash](https://huggingface.co/ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF) ist ein anderes, deutlich größeres Modell. ISTA-DASLab hat die drei Sets unten gebaut. Strata startet sie auf einem PC mit einer 12-GB-Grafikkarte und 46 bis 64 GB RAM und liest den Rest von der SSD. Es läuft auf NVIDIA-RTX-Karten der Serien 30 bis 50 und auf der Radeon AI PRO R9700. Es nutzt ein eigenes Engine-Paket, `strata-dsv41`, das `strata launch` lädt, wenn du DeepSeek wählst. Die Desktop-App bietet es noch nicht an.
| Set | Download | Qualität im Vergleich zum Originalmodell | Antwortet mit |
| --- | ---: | --- | --- |
| **IQ1_M** (1,75 Bit) | 239 GB | KL 0,217, Perplexität 3,80 (+21 %); Code +8,9 %, Agent-Aufgaben +10,2 %; schwächer bei einfacher englischer Prosa. Laut Modellkarte von ISTA-DASLab behält es 98,7 % des Aufgabendurchschnitts des Basismodells (AIME25, GPQA-Diamond, LiveCodeBench v6). | noch nicht gemessen |
| **IQ2_XXS** (2.00 Bit) | 257 GB | KL 0,170, Perplexität 3,63 (+15,5 %); Code +8,6 %, Agent-Aufgaben +5,3 % | 6 bis 9 Tokens/s auf einer RTX 3060, etwa 5 auf einer RTX 5070 |
| **IQ2_XS** (2.25 Bit) | 274 GB | KL 0,107, Perplexität 3,37 (+7,3 %); Code +3,2 %, Agent-Aufgaben +2,2 % | etwa 11 Tokens/s auf einer Radeon AI PRO R9700 |
| **IQ4_NL** (Engram-Tabelle, kein drittes Modell) | 111 GB, schon in allen drei Downloads oben enthalten | Die 4-Bit-Engram-Nachschlagetabelle, die das Modell aus RAM oder SSD liest. Alle drei Sets nutzen diese eine Datei (`DeepSeek-V4.1-Flash-Engram-IQ4_NL.gguf`). | n/a |
4x Radeon AI PRO R9700: etwa 31 Tokens/s, 55 bis 69 mit DSpark.
Starte es mit `strata launch --model ist-das-2.00bpw` (oder `ist-das-1.75bpw`, `ist-das-2.25bpw`). Details, Geschwindigkeiten je Karte und die manuelle Installation: [docs/DEEPSEEK41.md](docs/DEEPSEEK41.md).
## Wie schnell ist es?
Gemessen mit einer Grafikkarte mit 12 GB (RTX 5070, Ryzen 5 7600, 64 GB RAM). Ergebnisse anderer Karten findest du hier:
[Tempo jeder Größe](docs/MODELS.md#how-fast-is-each-size), [Ergebnisse der Community](docs/COMMUNITY_BENCHMARKS.md).
| Modell | Antworten erscheinen mit | Liest deinen Prompt mit |
| --- | --- | --- |
| **Q2_0** | `████████████` 94 Tokens/s | `████████████` 2.650 Tokens/s |
| **IQ2_XS** | `██████████░░` 79 Tokens/s | `█████████░░░` 2.090 Tokens/s |
| **IQ3_XXS** | `████████░░░░` 62 Tokens/s | `████████░░░░` 1.750 Tokens/s |
| **IQ3_S** | `███████░░░░░` 53 Tokens/s | `███████░░░░░` 1.620 Tokens/s |
| **Coder** | `███████░░░░░` 55 Tokens/s | `██████████░░` 2.180 Tokens/s |
Ein Token ist etwa ¾ eines Wortes, 60 Tokens pro Sekunde sind also schneller, als du lesen kannst. "Liest deinen Prompt"
beschreibt, wie schnell Strata ein langes Dokument, Code oder einen Chatverlauf aufnimmt.
Gemessen mit einer AMD-Karte mit 16 GB (RX 9070 XT, Ryzen 9 3900X, 47 GB RAM):
| Modell | Antworten erscheinen mit | Liest deinen Prompt mit |
| --- | --- | --- |
| **Q2_0** | `████████░░░░` 60 Tokens/s | `█████░░░░░░░` 1.160 Tokens/s |
| **IQ2_XS** | `███████░░░░░` 52 Tokens/s | `█████░░░░░░░` 1.110 Tokens/s |
| **Coder** | `██████░░░░░░` 44 Tokens/s | `██████░░░░░░` 1.420 Tokens/s |
Die Balken haben in beiden Tabellen denselben Maßstab. Eine Karte mit mehr VRAM ist schneller. Eine RTX 3090 (24 GB) sollte
etwa 100-140 Tokens pro Sekunde schreiben. Die Engine-Version hinter jeder Zahl steht in [DETAILS.md](docs/DETAILS.md#speed-measured).
## Benutzung
### In der App
Die App hat einen **Chat**, einen **Monitor** für das Modell sowie für GPU, CPU und RAM, eine Seite **Modelle** und die
**Einstellungen**. Der Chat kann in einem Ordner auf deinem PC arbeiten. Im Modus **Manuell** fragt er nach, bevor er eine
Datei ändert oder einen Befehl ausführt. Mit `/loop 10m <what to do>` wiederholst du eine Nachricht alle 10 Minuten, solange
der Chat geöffnet ist.
Der Chat kann das Web mit DuckDuckGo (oder Bing) durchsuchen, ohne Konto. Unter **Einstellungen, Tools** kannst du einen
Suchschlüssel hinzufügen. Die Suche ist die einzige Funktion, die etwas von deinem PC sendet (die Suchbegriffe).
Um zwei Modelle auszuführen, schalte **Mehrere Modelle ausführen** ein und gib jedem Modell eine eigene Karte desselben
Herstellers.
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="The Strata app: a chat where the agent built a small voxel pagoda garden, with the page open in the preview on the right"><br>
<sub>Ein Chat in der Strata-App: Der Agent hat einen kleinen Voxel-Pagodengarten als eine HTML-Datei geschrieben, und die Seite öffnet sich in der Vorschau neben dem Gespräch</sub></p>
### Im Terminal
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
<sub><code>strata monitor</code>, während das Modell antwortet: Geschwindigkeit, GPU, Speicher und die letzten Anfragen</sub></p>
```sh
strata launch # guided screens: model, context, pictures, access, then serve
strata launch --model q2_0 --yes --no-tui # no questions: serve, print the address, return
strata launch --plan --model q2_0 # show what would happen; nothing changes
strata launch --status # what is being served now
strata launch --stop # stop it
strata monitor --url http://127.0.0.1:8080 # watch speed, GPU, memory and requests
strata update --check # is there a new engine? (strata update installs it)
strata doctor # check the installed engine files
```
`strata --help` listet alles auf. Starten und Bereitstellen laden nichts herunter. Nur die Installation, `strata launch`
(wenn du ein Modell wählst) und `strata update` tun das. Um Strata von deinem Handy oder einem anderen PC aus zu erreichen,
nutze `strata launch --access lan`. Der Befehl erzeugt einen API-Schlüssel und startet in einem Netzwerk nicht ohne einen.
Ein Browser öffnet sich nur, wenn du `--open` angibst. Anleitung: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md). Der ganze Client: [docs/TERMINAL.md](docs/TERMINAL.md).
### Mit KI-Agents
Ein Agent installiert Strata mit dem Einzeiler aus Option B und führt
`strata launch --model NAME --yes --no-tui` aus. Die Schritte, Flags und Exit-Codes stehen in [docs/AI_SETUP.md](docs/AI_SETUP.md),
die Regeln für das Repository in [AGENTS.md](AGENTS.md).
### Andere Apps verbinden
| App-Typ | Basis-URL |
| --- | --- |
| OpenAI-kompatibel (die meisten Apps und Agents) | `http://127.0.0.1:8080/v1` (beliebiger API-Schlüssel, beliebiger Modellname) |
| Anthropic-API (Claude Code und andere) | `http://127.0.0.1:8080`, ohne `/v1`. Diese Apps hängen `/v1/messages` selbst an. Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
| OpenAI Responses API (Codex CLI) | `http://127.0.0.1:8080/v1/responses` ([Einrichtung](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
`strata connect` gibt die genauen Einstellungen für OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent und OpenClaw aus.
`strata connect opencode --write` schreibt die Datei nach einer Rückfrage und legt eine Sicherung an.
### Bilder
Bilder sind für Modelle, die sie lesen können, standardmäßig eingeschaltet. Hänge ein Bild im Chat an oder füge es ein, oder
sende es aus deiner App. Der Bildleser läuft auf dem Prozessor, wenn deine Grafikkarte weniger als 24 GB hat (einige Sekunden
pro Bild, ohne Speicher der Karte zu belegen), und ab 24 GB auf der Grafikkarte. Die Regel gilt für NVIDIA und AMD gleich.
Du kannst Bilder pro Modell ausschalten oder den Leser mit `--vision off`, `--vision gpu` oder `--vision cpu` wählen.
Die Zahlen hinter der Regel: Auf einer Radeon AI PRO R9700 brauchte der Bildleser der Karte 57 ms für ein Bild mit 260 Tokens,
der des Prozessors 3,1 s. Auf Karten mit 16 GB (Tesla P100 und RTX A4000, Linux) kostete der Bildleser der Karte 3-5 % des
Texttempos, weil er etwa 1,4 GB belegt, die sonst das Modell nutzen würde. Auf dem Mac liest die GPU, egal wie viel Speicher er hat (0,45 s für ein Bild mit 260 Tokens auf einem M4 mit 32 GB, etwa 1,2 GB des gemeinsamen
Speichers); der Prozessor braucht dort etwa 50 s pro Bild und liest deshalb nur mit `--vision cpu`.
### Gut zu wissen
- Das Denkniveau (**aus, niedrig, mittel oder hoch**) stellst du im Chat-Menü oder unter "reasoning effort" in deiner App ein. Aus ist am schnellsten.
- Strata beantwortet standardmäßig eine Anfrage nach der anderen. Für mehrere gleichzeitig nutze `--parallel N` in `strata launch` oder
`"parallel": 2` in der Konfiguration ([BATCHING.md](docs/BATCHING.md)). Auf einer Karte mit 12 GB wird dadurch jede Antwort langsamer.
- Strata liest die erste Nachricht eines Chats komplett, etwa 1 Minute pro 30.000 Tokens. Folgenachrichten starten in Sekunden.
- Wo deine Chats gespeichert werden und die ganze API: [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored), [docs/DETAILS.md](docs/DETAILS.md#using-it).
## Aus dem Quellcode bauen
<details>
<summary>Windows, Linux und Mac aus dem Repository</summary>
[Lade Strata herunter](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) und entpacke es, oder nutze `git clone`.
**Windows:** Doppelklick auf `START-HERE.bat`. **Linux:** Führe `./setup.sh` im Strata-Ordner aus.
**Mac (Apple Silicon, macOS 15 oder neuer):** Installiere einmal die Command Line Tools von Apple mit `xcode-select --install`
(Xcode selbst brauchst du nicht). Öffne das Terminal im Strata-Ordner und führe `./setup.sh` aus. Das Skript baut die
Mac-Engine auf deinem Mac (etwa 7 Minuten), stellt dieselben Fragen, lädt das Modell herunter und startet es. Beim nächsten
Mal startet `./setup.sh` sofort. Welche Größe zu deinem Mac passt: [docs/MAC.md](docs/MAC.md).
Der Installer erkennt deine Karte und richtet die passende Engine ein. Er fragt, welches Modell und welche Größe du willst,
wie viel Kontext (wie viel Text sich das Modell merkt) und ob es Bilder lesen soll. Drück Enter für die empfohlene Antwort.
Dann lädt er das Modell herunter (etwa 70 GB) und startet es. Wenn der Download abbricht, führe das Skript erneut aus, dann
geht es weiter. Dein Browser öffnet die Strata-Seite unter `http://127.0.0.1:8080`.
> Während das Modell startet, kann dein PC 1-3 Minuten langsam sein oder nicht reagieren (beim ersten Mal am längsten). Strata
> lädt 35-55 GB in deinen RAM und sperrt einen Teil davon für die Grafikkarte. Warte ab und schließe das Fenster nicht.
Starte beim nächsten Mal wieder `START-HERE.bat` (oder `./setup.sh`). Es startet sofort und lädt nichts doppelt herunter.
Schließ sein Fenster, um das Modell zu beenden. `UPDATE.bat` (`./update.sh`) aktualisiert Strata, ohne es zu starten.
Aktualisieren, Docker, mehrere Karten, wohin die Dateien kommen und alle Optionen: [docs/INSTALL.md](docs/INSTALL.md).
Um deinen PC zu messen, führe `BENCH.bat` (`./bench.sh`) aus. Das dauert 5-10 Minuten und schreibt einen Bericht, den du teilen
kannst ([so geht's](docs/COMMUNITY_BENCHMARKS.md#run-the-benchmark)).
**Optional: mehr Tempo auf einer Karte, wenn das ganze Modell in den RAM passt.** Zwei Einstellungen zum Einschalten von
RubenTorney (#1904, #1905). Sie helfen, wenn du eine Grafikkarte nutzt und dein RAM alle Experten aufnimmt, zum Beispiel bei
Q2_0 oder IQ3_XXS auf einem PC mit 64 GB. Öffne `strata-<model>.json` im Strata-Datenordner ([wo](docs/INSTALL.md#where-things-are-stored)) und ergänze:
- `"--adapt-async", "1"` in der Liste `"args"`. Der Expertencache wird dann in einem Hilfsthread aktualisiert, statt die
Antwort anzuhalten. Gemessen auf einer RTX 5070 (Windows, Q2_0): +4-5 % beim Decodieren, +7-10 % bei Code und langen
Dokumenten. Die Antworten können sich leicht von der Standardeinstellung unterscheiden und bleiben von Lauf zu Lauf gleich.
- `"STRATA_PIN_BY_NEED": "1"` im Block `"env"` (Windows). Windows kann nur etwa die Hälfte des RAM für die Grafikkarte sperren,
und diese Einstellung sperrt zuerst die Experten, die die Karte am dringendsten braucht. Gemessen auf der RTX 5070: +4,4 % beim Lesen des Prompts.
```json
"args": [ ..., "--adapt-async", "1" ],
"env": { "STRATA_PIN_BY_NEED": "1" }
```
Starte Strata nach der Änderung neu. Zum Rückgängigmachen entfernst du die beiden Einträge.
Folge den Anweisungen, um ein Modell herunterzuladen und zu starten. [Terminal-Anleitung](docs/TERMINAL_LAUNCH.md).
</details>
## Wie funktioniert das?
<p align="center">
<img src="docs/media/terminal-monitor.png" width="900" alt="Strata-Terminal-App mit Modellaktivität, Tempo, Speicher und letzten Anfragen">
<br><sub>Behalte Modell und Hardware im Blick, während es läuft.</sub>
</p>
Modelle wie dieses laufen normalerweise auf Servern mit Hunderten Gigabyte Grafikspeicher. Deine Grafikkarte hat 12-24 GB.
Strata bringt das Modell auf deinen PC, indem es die Arbeit auf den ganzen Rechner verteilt. Stell dir eine Bibliothek vor.
<a id="build-from-source"></a>
- Das Modell ist ein Team aus 24.576 kleinen Spezialisten ("Experten"), wie 24.576 Nachschlagewerke. Für jedes Wort braucht es nur 10 davon.
- **Deine Grafikkarte ist der Schreibtisch.** Er ist klein und schnell. Die etwa 4.000 am häufigsten genutzten Bücher liegen
dort aufgeschlagen.
- **Dein RAM sind die Regale im Lesesaal.** Alle 24.576 Bücher stehen dort. Braucht ein Wort ein Buch, das nicht auf dem
Schreibtisch liegt, holt dein Prozessor es sofort aus dem Regal, während die Karte weiterarbeitet.
- **Deine SSD ist das Archiv.** Sie enthält eine Nachschlagetabelle von 29 GB. Das Modell liest pro Wort nur wenige Zeilen
daraus, deshalb muss sie nicht im RAM liegen.
[Aus dem Quellcode bauen](docs/SOURCE_INSTALL.md) · [Docker](docs/DOCKER.md) · [Vollständige Installationsanleitung](docs/INSTALL.md)
<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>
<a id="what-you-need"></a>
Ein kleiner Helfer rät außerdem die nächsten Wörter. Das große Modell prüft sie alle auf einmal und behält die richtigen. Du
bekommst dieselbe Antwort, 1,6-1,8-mal früher. Lange Texte werden in großen Stücken gelesen, bis zu 8.192 Tokens auf einmal,
mit über 1.000 Tokens pro Sekunde.
## Hardware
<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>
| Unterstützte Hardware | Anleitung |
| --- | --- |
| **NVIDIA GeForce RTX 20 / 30 / 40 / 50** | [NVIDIA-Setup](docs/INSTALL.md#what-you-need) |
| **AMD Radeon RX 7700 / 7800 / 7900, RX 9060 / 9070, AI PRO R9700** | [AMD-Setup](docs/AMD_HIP.md) |
| **AMD Radeon RX 6800 / 6900 · experimentell** | [RX-6000-Setup](docs/AMD_HIP.md#rdna2-gfx1030) |
| **Intel Arc · Linux** | [Intel-Setup](docs/INTEL_ARC.md) |
| **Apple Silicon · M1 oder neuer** | [Mac-Setup](docs/MAC.md) |
| **AMD Ryzen AI Max · Strix Halo** | [Strix-Halo-Setup](docs/STRIX_HALO.md) |
Die ausführliche Erklärung: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Jeder Teil mit seinen Zahlen:
[die Details](docs/DETAILS.md#how-it-works) und das [Paper](docs/paper/Strata-Paper.pdf).
<a id="using-it"></a>
## Etwas ist schiefgegangen?
## Tools
- **Mein PC ist beim ersten Start von Strata eingefroren.** Strata lädt das Modell in den RAM. Warte ab und schließe das
Fenster nicht. Nach 10 Minuten immer noch eingefroren? Starte den PC neu, schließe andere Programme und versuch es noch
einmal, oder wähle eine kleinere Größe.
- **Es ist beim Herunterladen oder Installieren stehen geblieben.** Starte die App oder `strata launch` noch einmal, oder
führe `START-HERE.bat` (oder `./setup.sh`) erneut aus. Es macht dort weiter, wo es aufgehört hat.
- **Es ist sehr langsam, die Festplattenanzeige blinkt ständig, oder es meldet "the engine stopped unexpectedly".** Dein PC
hat nicht genug freien RAM. Schließe andere Programme (Browser brauchen viel) oder wähle eine kleinere Größe wie Q2_0 oder IQ2_XS.
- **Es meldet, dass es keine Engine für diesen PC gibt.** Öffne in der App **Einstellungen, Engine** und drücke **Engine
aktualisieren**. Führe im Terminal `strata update` aus. `strata doctor` zeigt, welche Engine-Dateien installiert sind.
- **Es meldet, dass Port 8080 schon belegt ist.** Strata läuft bereits. Such das Fenster.
**Claude Code · Codex CLI · OpenCode · oh-my-pi · pi.dev · Hermes Agent · OpenClaw**, und weitere über die OpenAI- und Anthropic-kompatiblen APIs.
Weitere Probleme und Lösungen: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Du kommst nicht weiter? Öffne ein
[Issue](https://github.com/Niko1221/Strata/issues). Hänge in der App die Datei aus **Einstellungen, Engine, Diagnose, Protokolle anzeigen, Protokolle speichern** an. Füge im Terminal die Ausgabe von `strata doctor` ein. Hänge beim Start aus dem Quellcode die
Datei `strata-<model>.log` aus dem Strata-Ordner an. Ein Sicherheitsproblem meldest du bitte privat: [SECURITY.md](SECURITY.md).
Führe **`strata connect`** aus, um die Einrichtung für dein Tool zu sehen. [Verbindungsanleitung](docs/TERMINAL_LAUNCH.md#connect-your-apps-strata-connect).
## Danksagung und Lizenz
<a id="guides"></a>
<a id="how-fast-is-it"></a>
<a id="how-does-it-work"></a>
<a id="something-went-wrong"></a>
Das Modell ist [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) vom Qwen-Team. Komprimiert wurde es von
[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5), agentionai (Gyro)
und Unsloth. Strata nutzt Teile von [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Alle Danksagungen:
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata ist Open Source unter der [MIT-Lizenz](LICENSE). Einige Teile und
alle Modelle haben eigene Lizenzen ([welche](docs/HOW_IT_WORKS.md#license)).
## Hilfe
## Mitmachen
Du hast einen Fehler gefunden oder willst eine Änderung oder einen Benchmark einsenden? Lies zuerst [CONTRIBUTING.md](CONTRIBUTING.md).
Dort steht, was dazugehört, damit andere deine Arbeit nachprüfen können.
[Alle Anleitungen](docs) · [Fehlerbehebung](docs/TROUBLESHOOTING.md) · [Fehler melden](https://github.com/Niko1221/Strata/issues/new/choose) ·
[Benchmarks](docs/COMMUNITY_BENCHMARKS.md) · [So funktioniert es](docs/HOW_IT_WORKS.md) · [Mitmachen](CONTRIBUTING.md)
## Strata unterstützen
Strata ist kostenlos und steht unter der MIT-Lizenz. Ich baue es und teste es auf jeder Hardware, die ich bekommen kann. Wenn es auf deinem PC gut läuft, hilft mir ein Kaffee,
weiter neue Karten zu testen und das zu beheben, was ihr meldet. Es besteht kein Druck. Strata zu nutzen und zu melden, was dir auffällt, hilft genauso viel.
**Hilf mit, lokale KI für alle offen zu halten.** Ein GitHub-Sponsoring oder ein Kaffee hilft, Entwicklung und Tests auf mehr Hardware zu finanzieren.
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a>
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor_on_GitHub-EA4AAA?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="GitHub Sponsors: Strata unterstützen" height="48"></a>
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee: Strata unterstützen" height="48"></a>
Du möchtest Strata sponsern? Schreib mir über [GitHub](https://github.com/Niko1221).
### Treue Unterstützer
Danke an alle, die Strata unterstützen.
<!-- supporters:start (one line per public supporter, newest last; copy a line and change the GitHub name in all four places) -->
<a href="https://github.com/ryan4yin"><img src="https://github.com/ryan4yin.png?size=96" width="48" height="48" alt="ryan4yin" title="ryan4yin"></a>
<a href="https://github.com/themactep"><img src="https://github.com/themactep.png?size=96" width="48" height="48" alt="themactep" title="themactep"></a>
<a href="https://github.com/macintog"><img src="https://github.com/macintog.png?size=96" width="48" height="48" alt="macintog" title="macintog"></a>
<a href="https://github.com/bisonbet"><img src="https://github.com/bisonbet.png?size=96" width="48" height="48" alt="bisonbet" title="bisonbet"></a>
<a href="https://github.com/vizv"><img src="https://github.com/vizv.png?size=96" width="48" height="48" alt="vizv" title="vizv"></a>
<!-- supporters:end -->
[MIT-Lizenz](LICENSE) · [Danksagung und Modelllizenzen](docs/HOW_IT_WORKS.md#credits) · [Datenschutz](docs/SAFETY.md)
<a id="option-c-let-your-ai-set-it-up"></a>
<sub>Für KI-Tools und automatische Leser: <a href="AGENTS.md">technische Referenz</a> · <a href="docs/AI_SETUP.md">Setup-Anleitung</a>.</sub>
**Sponsoring, Hardware-Partnerschaften oder Zusammenarbeit:** [[email protected]](mailto:[email protected]).
+135 -351
View File
@@ -1,418 +1,202 @@
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="apps/host/public/brand/strata-lockup-dark.svg">
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="380">
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="340">
</picture>
</p>
<p align="center"><a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <b>Español</b> · <a href="README.pt-BR.md">Português</a></p>
<h3 align="center">Ejecuta un modelo de última generación en tu propio PC gaming</h3>
<p align="center">Compatible con NVIDIA, AMD, Intel · Windows, Linux, Mac · libre y de código abierto</p>
<h3 align="center">IA potente en tu propio ordenador.</h3>
<p align="center">Chatea, programa y trabaja con imágenes, en local.</p>
<p align="center">
<a href="https://github.com/Niko1221/Strata/releases"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Latest release"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT license"></a>
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="GitHub stars"></a>
<a href="#modelos">Modelos</a> · <a href="#instalación"><b>Instalación</b></a> ·
<a href="#hardware">Hardware</a> · <a href="#herramientas">Herramientas</a> ·
<a href="#ayuda">Ayuda</a> · <a href="#apoya-strata">Apoyar</a>
</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>Un jardín con una pagoda de vóxeles, hecho con un solo prompt en una RTX 5070 con Strata (IQ3_S, contexto de 128K) ·
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">vídeo completo (49 s)</a></sub></p>
<p align="center">
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?label=download&color=0ab887" alt="Descargar Strata"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="Licencia MIT"></a>
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor-EA4AAA?logo=githubsponsors&logoColor=fff" alt="Patrocina Strata en GitHub Sponsors"></a>
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy_me_a_coffee-FFDD00?logo=buymeacoffee&logoColor=000" alt="Apoya Strata en Buy Me a Coffee"></a>
</p>
Strata ejecuta **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**, un modelo de 125 mil millones
de parámetros que normalmente necesita un servidor, en un PC normal. Conversa, escribe código, lee imágenes y funciona
con tus aplicaciones y tus agentes de programación. Tus chats y tus archivos se quedan en tu PC.
<p align="center"><sub><a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <b>Español</b> · <a href="README.pt-BR.md">Português</a> · <a href="README.pl.md">Polski</a></sub></p>
**¿Eres nuevo?** Mira [lo que necesitas](#lo-que-necesitas) y luego [instala la aplicación](#opción-a-la-aplicación-strata).
**Gratis, de código abierto y tuyo para ejecutar.** Usa Strata en tu escritorio o con tus herramientas de programación favoritas. No necesitas cuenta.
- **Funciona en un PC gaming.** Con 32 GB entre RAM y memoria gráfica ya puedes empezar.
- **Funciona con tus herramientas.** Habla las API de OpenAI y de Anthropic, así que Claude Code, Codex CLI, OpenCode y otros agentes se conectan a él.
- **Lee imágenes.** Viene activado por defecto.
- **Gratis.** Licencia MIT, sin cuenta, sin nube.
- **Privado.** Sin telemetría. Lo que Strata descarga, cuándo usa internet y cómo comprueba las descargas está en [docs/SAFETY.md](docs/SAFETY.md).
## Modelos
Versión 0.2.3 · motor 0.1.48
Recomendamos **primero ISTA-DASLab**, después **Unsloth** y otros proveedores, siempre que haya un modelo compatible que quepa en tu memoria.
## Lo que necesitas
<details>
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> &nbsp;<b>Qwen3.8-Flash-Next</b></summary>
**Necesitas al menos 32 GB entre RAM y memoria gráfica (VRAM).** En un Mac o un Ryzen AI Max, es la memoria unificada.
Con más memoria puedes usar un modelo más grande o más rápido.
- Un PC de 32 GB con una tarjeta de 12 a 16 GB ejecuta el Coder.
- Con 64 GB de RAM funcionan todos los tamaños.
| Tu ordenador | Detalles |
| Proveedor | Opciones |
| --- | --- |
| **NVIDIA** GeForce RTX serie 20, 30, 40, 50 (12 GB o más) | Instala la aplicación o la Terminal. |
| **AMD** Radeon RX 6800 y posteriores, Radeon AI PRO R9700 (12 GB o más) | [Tarjetas AMD](docs/AMD_HIP.md) |
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md), [motor Intel](docs/INTEL.md) |
| **Mac con Apple Silicon** (M1 o posterior, macOS 15 o posterior) | [Mac](docs/MAC.md) |
| **AMD Ryzen AI Max (Strix Halo)** y otros sistemas con memoria unificada | [Strix Halo](docs/STRIX_HALO.md) |
| Tarjetas y procesadores antiguos | [GPU antiguas](docs/OLDER_GPUS.md), [CPU antiguas](docs/INSTALL.md#older-cpus-experimental) |
| **ISTA-DASLab · recomendado** | Q2\_0, IQ2\_XS, IQ3\_XXS, IQ3\_S. [Coder: IQ1\_M](docs/MODELS.md#coder) para programar. |
| **Unsloth** | UD-IQ4\_XS, UD-Q4\_K\_XL, UD-Q5\_K\_XL, UD-Q6\_K\_XL, Q8\_0 |
| **Gyro · agentionai** | [Gyro-S, Gyro-M](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF). Usa menos memoria, pero es más lento. |
| **Otras opciones** | [Swift 1.5](docs/MODELS.md#swift-15), [NVFP4](docs/NVFP4.md), [OrcaRouter](docs/ORCA.md) |
También hace falta:
[Compara tamaños, necesidades de memoria y compatibilidad](docs/MODELS.md).
- unos 80 GB de disco libre (con un SSD el primer arranque es mucho más rápido)
- Windows 10 / 11, Linux o macOS
- un controlador gráfico actual
</details>
Dos o tres tarjetas de la misma marca pueden compartir un modelo ([multi-GPU](docs/MULTI_GPU.md)). Strata nunca reparte un
modelo entre tarjetas NVIDIA y AMD. Lista completa: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
<details>
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> &nbsp;<b>Qwen3.6-35B-A3B</b></summary>
**Unsloth:** UD-Q4_K_XL, UD-Q6_K_XL, Q8_0. Son los paquetes documentados actualmente para este modelo.
[Guía del modelo y compatibilidad](docs/QWEN36.md).
</details>
<details>
<summary><img src="docs/media/models/deepseek.svg" width="22" alt="" align="middle"> &nbsp;<b>DeepSeekV4.1-Flash-1017</b></summary>
En la **aplicación de escritorio** y en el **Terminal**. Lo que no cabe en la memoria se lee desde tu SSD, así que hace falta mucho espacio libre en disco.
| Conjunto (ISTA-DAS) | Descarga | Espacio libre necesario |
| --- | ---: | ---: |
| 1.75 bpw | 239 GB | unos 245 GB |
| 2.00 bpw | 257 GB | unos 265 GB |
| 2.25 bpw | 274 GB | unos 280 GB |
Funciona en NVIDIA RTX 30, 40 y 50 (Windows y Linux) y en AMD Radeon AI PRO R9700 (Linux). La instalación descarga los archivos y el motor de DeepSeek, y después inicia el modelo.
```sh
strata launch --family deepseek41 --model ist-das-2.00bpw --plan
```
Medido con el conjunto de 2.00 bpw en Strata 0.2.5. «Salida» es la velocidad de la respuesta; «Prompt» es la rapidez con la que Strata lee tu texto.
| Hardware | Salida | Prompt |
| --- | ---: | ---: |
| RTX 5070 12 GB, 64 GB de RAM | 4 a 6 tokens/s | unos 110 tokens/s (4K) |
| RTX 3060 12 GB, 46 GB de RAM | 9 a 11 tokens/s | unos 155 tokens/s (4K) |
| 1x Radeon AI PRO R9700, 251 GB de RAM | 21 a 25 tokens/s | 320 a 360 tokens/s (8K) |
| 4x Radeon AI PRO R9700 | 74 a 77 tokens/s | 400 a 450 tokens/s (8K) |
[Memoria, disco y detalles](docs/DEEPSEEK41.md) · [Notas de la versión](https://github.com/Niko1221/Strata/releases)
</details>
<a id="the-other-sizes"></a>
¿No sabes qué cabe? [Elige un modelo para tu ordenador](docs/MODELS.md#pick-by-memory).
## Instalación
Elige una opción. Todas arrancan el mismo motor y usan los mismos modelos.
<a id="option-a-the-strata-app"></a>
### Opción A: la aplicación Strata
### Aplicación de escritorio
La forma más fácil. La aplicación lleva el motor dentro: la instalas, eliges un modelo y chateas. Descarga el archivo para
tu sistema y tu tarjeta gráfica desde la [página de versiones](https://github.com/Niko1221/Strata/releases/latest). ¿No
sabes cuál elegir? Mira el párrafo que hay debajo de la tabla.
**[Descargar Strata para Windows, Linux o Mac →](https://github.com/Niko1221/Strata/releases/latest)**
| Sistema | Tu tarjeta gráfica | Archivo que descargar |
| --- | --- | --- |
| **Windows 10 / 11** | NVIDIA RTX, controlador 580 o posterior | [`Strata-0.2.3-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda13.exe) |
| | NVIDIA, tarjeta más antigua o controlador anterior a 580 | [`Strata-0.2.3-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda12.exe) |
| | AMD Radeon serie RX 9000 o Radeon AI PRO R9700 | [`Strata-0.2.3-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip.exe) |
| | AMD Radeon serie RX 7000 o Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip-rdna3.exe) |
| **Linux** | NVIDIA RTX, controlador 580 o posterior | [`Strata-0.2.3-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda13.AppImage) |
| | NVIDIA, tarjeta más antigua o controlador anterior a 580 | [`Strata-0.2.3-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda12.AppImage) |
| | AMD Radeon serie RX 9000 o Radeon AI PRO R9700 | [`Strata-0.2.3-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx9070.AppImage) |
| | AMD Radeon serie RX 7000 | [`Strata-0.2.3-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx7900.AppImage) |
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-strixhalo.AppImage) |
| **Mac** (Apple Silicon, macOS 15 o posterior) | integrada | [`Strata-0.2.3-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-mac.dmg) |
Instala la aplicación, elige un modelo y empieza a chatear. Hay un instalador por sistema. En el primer arranque, la aplicación descarga el motor para tu tarjeta gráfica. [Ayuda con el instalador](docs/INSTALL.md#the-app).
**Si no estás seguro:** con NVIDIA, elige `cuda13` si la versión de tu controlador es la 580 o posterior, y `cuda12` en caso
contrario. `cuda12` es para tarjetas y controladores más antiguos. Los archivos `hip` y `amd` son para tarjetas AMD. Las
demás tarjetas AMD (RX 6000) todavía no tienen una aplicación ya preparada: mira
[Tarjetas AMD](docs/AMD_HIP.md).
<sub>Archivos: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
Después:
<p align="center">
<img src="docs/media/app-chat.png" width="900" alt="Aplicación de escritorio de Strata con un chat y una vista previa del código en vivo, una junto a otra">
<br><sub>Chat, archivos y vista previa en vivo en una sola ventana.</sub>
</p>
- **Windows:** ejecuta el `.exe`. Se instala para tu usuario y no necesita permisos de administrador. Si Windows muestra
"Windows protegió tu PC", elige **Más información** y luego **Ejecutar de todas formas** (el instalador todavía no está
firmado).
- **Linux:** haz el archivo ejecutable (`chmod +x`) y lánzalo. Necesita FUSE 2: `sudo apt install libfuse2` (Ubuntu 22.04)
o `libfuse2t64` (Ubuntu 24.04 y posteriores).
- **Mac:** abre el `.dmg` y arrastra Strata a Aplicaciones.
<a id="option-b-the-terminal-one-line"></a>
En la primera pantalla, la aplicación analiza tu PC y recomienda un modelo adecuado. Pulsa Instalar. El modelo es una
descarga grande (unos 40 a 70 GB) y, si se interrumpe, continúa donde se quedó. Al cerrar la ventana, Strata y el motor se
detienen.
### Aplicación de Terminal
### Opción B: la Terminal (una línea)
¿Prefieres la terminal? Instala Strata y ejecuta **`strata launch`**.
La Terminal es el comando `strata`, pensado para scripts, servidores y agentes de IA.
<details open>
<summary><b>Comandos de instalación</b></summary>
Linux y Mac:
**Linux / Mac**
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
```
Windows (PowerShell):
**Windows PowerShell**
```powershell
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
```
El script analiza tu PC, descarga la Terminal, verifica su SHA-256 y la descomprime en tu carpeta personal. No necesita
permisos de administrador. En Windows, abre una ventana de terminal nueva después. Luego arranca Strata:
En Windows, abre una terminal nueva después. Luego:
```sh
strata launch
```
Te hace una pregunta cada vez: modelo, contexto, imágenes, quién puede conectarse. Después descarga el motor para tu tarjeta
gráfica (firmado y arrancado como prueba antes de usarlo) y el modelo, y empieza a servir. Para scripts y agentes de IA, usa
opciones:
```sh
strata launch --model q2_0 --yes --no-tui
```
Si quieres leer el script de instalación antes de ejecutarlo, descárgalo primero con `curl -fsSL <address> -o install.sh`,
léelo y luego ejecuta `sh install.sh`. Todas las pantallas y opciones: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
### Opción C: que lo instale tu IA
Pega esto en Claude Code, Cursor, Codex, GitHub Copilot u otro asistente de programación:
```text
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
```
El asistente revisa tu tarjeta gráfica, tu RAM y tu disco, elige el modelo adecuado, lo instala y lo arranca, y te explica
cómo conectar tus aplicaciones. Las herramientas de IA también pueden instalar, arrancar y detener Strata mediante su
[servidor MCP](docs/MCP_SERVER.md).
¿Prefieres compilarlo tú? Mira [Compilar desde el código fuente](#compilar-desde-el-código-fuente).
## Modelos
La aplicación y `strata launch` recomiendan un modelo según tu memoria. Es un solo modelo en varios tamaños, más o menos
comprimido. Los tamaños pequeños son más rápidos. Los grandes son algo más inteligentes.
### ¿Poca memoria? Prueba Gyro
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) (de agentionai) es la forma más pequeña de ejecutar este modelo.
| Tamaño | Memoria que necesita (RAM + VRAM) | Descarga | Qué es |
| --- | ---: | ---: | --- |
| **Gyro-S** | 27,6 GB | 58,5 GB | Para PC donde no cabe nada más. La calidad es menor que la de IQ2_XS. |
| **Gyro-M** | 35 GB | 92,0 GB | Calidad parecida a IQ3_XXS con un 20 % menos de memoria de GPU. Escribe las respuestas un 16 % más despacio que Q2_0 en una RTX 3060 de 12 GB y un 21 % más despacio en una R9700. |
Gyro funciona en tarjetas NVIDIA y AMD (en AMD es experimental) en tarjetas Intel Arc con Linux (probado en una Arc A750) y, desde 0.2.2, en Mac con Apple Silicon (Gyro-S en un Mac mini de 32 GB: 9 tokens/s, con los expertos leídos del SSD). Detalles y mediciones:
[docs/GYRO.md](docs/GYRO.md). Arráncalo con `strata launch --model gyro-s`.
### Los demás tamaños
| Tu memoria (RAM + VRAM) | Elige | Notas |
| --- | --- | --- |
| **32 GB** | **Gyro-S**, o el **[Coder](docs/MODELS.md#coder)** para código | El Coder necesita 32 GB de RAM y conserva la mitad de los expertos, así que es más flojo fuera del código |
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39,2 GB), o **Gyro-M** (35 GB) | Swift piensa menos tiempo antes de responder. Gyro-M se acerca a la calidad de IQ3_XXS, pero escribe las respuestas un 16-21 % más despacio que Q2_0 |
| **48 GB** | **IQ3_XXS** (47,0 GB) | Más lento, mejor |
| **64 GB** | **IQ3_S** (54,8 GB), el original o Swift 1.5 | El más lento, el mejor. Iguala al modelo completo en las pruebas publicadas |
| **96 GB o más** | **UD-IQ4_XS** de Unsloth (unos 72 GB de RAM) hasta **Q8_0** (unos 136 GB), de 4 a 8 bits | Más cerca del modelo completo. Con menos RAM, Strata lee una parte desde el SSD, que es mucho más lento |
Con menos RAM de la que indica una fila también puede funcionar: Strata pasa solo a su
[modo de poca RAM](docs/MODELS.md#a-big-graphics-card-and-little-ram). Espera que sea más lento, y mucho más lento con una
tarjeta gráfica pequeña.
Más opciones:
- **[Swift 1.5](docs/MODELS.md#swift-15)** es un ajuste fino que piensa menos tiempo antes de responder, con una calidad y un uso de RAM casi iguales.
- **[NVFP4](docs/NVFP4.md)** es la versión de 4 bits de NVIDIA para tarjetas NVIDIA. Necesita unos 80 GB de RAM para tener todos los expertos en RAM.
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** es un modelo más pequeño y distinto que lee imágenes y puede atender varias peticiones a la vez.
- **[El Uncensored IQ3_XXS de OrcaRouter](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** se configura a mano.
Todos los tamaños, descargas y lo que cabe en cada equipo: [docs/MODELS.md](docs/MODELS.md).
### DeepSeek V4.1-Flash (en el Terminal, desde 0.2.4)
[DeepSeek V4.1-Flash](https://huggingface.co/ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF) es otro modelo, mucho más grande. ISTA-DASLab hizo los tres conjuntos de abajo. Strata los ejecuta en un PC con una tarjeta gráfica de 12 GB y de 46 a 64 GB de RAM, y lee el resto de tu SSD. Funciona en tarjetas NVIDIA RTX de las series 30 a 50 y en la Radeon AI PRO R9700. Usa su propio paquete de motor, `strata-dsv41`, que `strata launch` descarga cuando eliges DeepSeek. La app de escritorio todavía no lo ofrece.
| Conjunto | Descarga | Calidad frente al modelo original | Responde a |
| --- | ---: | --- | --- |
| **IQ1_M** (1,75 bits) | 239 GB | KL 0,217, perplejidad 3,80 (+21 %); código +8,9 %, tareas de agente +10,2 %; más débil con prosa inglesa corriente. Según la ficha del modelo de ISTA-DASLab, conserva el 98,7 % del promedio de tareas del modelo base (AIME25, GPQA-Diamond, LiveCodeBench v6). | aún sin medir |
| **IQ2_XXS** (2.00 bits) | 257 GB | KL 0,170, perplejidad 3,63 (+15,5 %); código +8,6 %, tareas de agente +5,3 % | de 6 a 9 tokens/s en una RTX 3060, unos 5 en una RTX 5070 |
| **IQ2_XS** (2.25 bits) | 274 GB | KL 0,107, perplejidad 3,37 (+7,3 %); código +3,2 %, tareas de agente +2,2 % | unos 11 tokens/s en una Radeon AI PRO R9700 |
| **IQ4_NL** (tabla Engram, no es un tercer modelo) | 111 GB, ya incluidos en las tres descargas de arriba | La tabla de consulta Engram de 4 bits que el modelo lee de la RAM o del SSD. Los tres conjuntos usan este único archivo (`DeepSeek-V4.1-Flash-Engram-IQ4_NL.gguf`). | n/a |
4x Radeon AI PRO R9700: unos 31 tokens/s, de 55 a 69 con DSpark.
Inícialo con `strata launch --model ist-das-2.00bpw` (o `ist-das-1.75bpw`, `ist-das-2.25bpw`). Detalles, velocidades por tarjeta e instalación manual: [docs/DEEPSEEK41.md](docs/DEEPSEEK41.md).
## ¿Qué velocidad tiene?
Medido con una tarjeta gráfica de 12 GB (RTX 5070, Ryzen 5 7600, 64 GB de RAM). Resultados con otras tarjetas:
[velocidad de cada tamaño](docs/MODELS.md#how-fast-is-each-size), [resultados de la comunidad](docs/COMMUNITY_BENCHMARKS.md).
| Modelo | Las respuestas aparecen a | Lee tu prompt a |
| --- | --- | --- |
| **Q2_0** | `████████████` 94 tokens/s | `████████████` 2.650 tokens/s |
| **IQ2_XS** | `██████████░░` 79 tokens/s | `█████████░░░` 2.090 tokens/s |
| **IQ3_XXS** | `████████░░░░` 62 tokens/s | `████████░░░░` 1.750 tokens/s |
| **IQ3_S** | `███████░░░░░` 53 tokens/s | `███████░░░░░` 1.620 tokens/s |
| **Coder** | `███████░░░░░` 55 tokens/s | `██████████░░` 2.180 tokens/s |
Un token equivale a unos ¾ de palabra, así que 60 tokens por segundo es más rápido de lo que puedes leer. "Lee tu prompt"
es lo rápido que Strata procesa un documento largo, código o un historial de chat.
Medido con una tarjeta AMD de 16 GB (RX 9070 XT, Ryzen 9 3900X, 47 GB de RAM):
| Modelo | Las respuestas aparecen a | Lee tu prompt a |
| --- | --- | --- |
| **Q2_0** | `████████░░░░` 60 tokens/s | `█████░░░░░░░` 1.160 tokens/s |
| **IQ2_XS** | `███████░░░░░` 52 tokens/s | `█████░░░░░░░` 1.110 tokens/s |
| **Coder** | `██████░░░░░░` 44 tokens/s | `██████░░░░░░` 1.420 tokens/s |
Las barras usan la misma escala en las dos tablas. Una tarjeta con más VRAM es más rápida. Una RTX 3090 (24 GB) debería
escribir unos 100-140 tokens por segundo. La versión del motor detrás de cada cifra está en [DETAILS.md](docs/DETAILS.md#speed-measured).
## Uso
### En la aplicación
La aplicación tiene un **Chat**, un **Monitor** del modelo y de tu GPU, CPU y RAM, una página de **Modelos** y los
**Ajustes**. El chat puede trabajar en una carpeta de tu PC. En modo **Manual** pregunta antes de cambiar un archivo o
ejecutar un comando. Escribe `/loop 10m <what to do>` para repetir un mensaje cada 10 minutos mientras el chat esté abierto.
El chat puede buscar en la web con DuckDuckGo (o Bing), sin cuenta. Puedes añadir una clave de búsqueda en **Ajustes,
Herramientas**. La búsqueda es la única función que envía algo fuera de tu PC (las palabras que buscas).
Para ejecutar dos modelos, activa **Ejecutar varios modelos** y dale a cada modelo su propia tarjeta de la misma marca.
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="The Strata app: a chat where the agent built a small voxel pagoda garden, with the page open in the preview on the right"><br>
<sub>Un chat en la app de Strata: el agente escribió un pequeño jardín de pagoda de vóxeles en un solo archivo HTML, y la página se abre en la vista previa junto a la conversación</sub></p>
### En la Terminal
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
<sub><code>strata monitor</code> mientras el modelo responde: velocidad, GPU, memoria y las últimas solicitudes</sub></p>
```sh
strata launch # guided screens: model, context, pictures, access, then serve
strata launch --model q2_0 --yes --no-tui # no questions: serve, print the address, return
strata launch --plan --model q2_0 # show what would happen; nothing changes
strata launch --status # what is being served now
strata launch --stop # stop it
strata monitor --url http://127.0.0.1:8080 # watch speed, GPU, memory and requests
strata update --check # is there a new engine? (strata update installs it)
strata doctor # check the installed engine files
```
`strata --help` lo lista todo. Arrancar y servir no descargan nada. Solo lo hacen la instalación, `strata launch` (cuando
eliges un modelo) y `strata update`. Para llegar a Strata desde tu móvil u otro PC, usa `strata launch --access lan`. Crea
una clave de API y no arranca en una red sin ella. No se abre ningún navegador salvo que añadas `--open`. Guía:
[docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md). El cliente completo: [docs/TERMINAL.md](docs/TERMINAL.md).
### Con agentes de IA
Un agente instala Strata con el comando de una línea de la opción B y ejecuta
`strata launch --model NAME --yes --no-tui`. Los pasos, las opciones y los códigos de salida están en [docs/AI_SETUP.md](docs/AI_SETUP.md),
y las reglas del repositorio en [AGENTS.md](AGENTS.md).
### Conecta tus otras aplicaciones
| Tipo de aplicación | URL base |
| --- | --- |
| Compatible con OpenAI (la mayoría de aplicaciones y agentes) | `http://127.0.0.1:8080/v1` (cualquier clave de API, cualquier nombre de modelo) |
| API de Anthropic (Claude Code y otros) | `http://127.0.0.1:8080`, sin `/v1`. Estas aplicaciones añaden `/v1/messages` por su cuenta. Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
| API OpenAI Responses (Codex CLI) | `http://127.0.0.1:8080/v1/responses` ([configuración](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
`strata connect` muestra los ajustes exactos para OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent y OpenClaw.
`strata connect opencode --write` escribe el archivo después de preguntar y guarda una copia de seguridad.
### Imágenes
Las imágenes vienen activadas por defecto en los modelos que pueden leerlas. Adjunta o pega una imagen en el chat, o envíala
desde tu aplicación. El lector de imágenes funciona en el procesador cuando tu tarjeta gráfica tiene menos de 24 GB (unos
segundos por imagen, sin quitar memoria a la tarjeta) y en la tarjeta gráfica a partir de 24 GB. La regla es la misma para
NVIDIA y AMD. Puedes desactivar las imágenes por modelo, o elegir el lector con `--vision off`, `--vision gpu` o
`--vision cpu`.
Las cifras detrás de la regla: en una Radeon AI PRO R9700, el lector de la tarjeta tardó 57 ms en una imagen de 260 tokens y
el del procesador 3,1 s. En tarjetas de 16 GB (Tesla P100 y RTX A4000, Linux), el lector de la tarjeta costó un 3-5 % de
velocidad en texto, porque ocupa unos 1,4 GB que de otro modo usaría el modelo. En un Mac lee la GPU, tenga la memoria que tenga (0,45 s para una imagen de 260 tokens en un M4 de 32 GB, unos 1,2 GB de la memoria compartida);
el procesador tarda unos 50 s por imagen, así que solo lee con `--vision cpu`.
### Conviene saber
- Elige el nivel de razonamiento (**desactivado, bajo, medio o alto**) en el menú del chat o en "reasoning effort" de tu aplicación. Desactivado es lo más rápido.
- Strata responde a una petición a la vez por defecto. Para varias a la vez, usa `--parallel N` en `strata launch` o
`"parallel": 2` en la configuración ([BATCHING.md](docs/BATCHING.md)). En una tarjeta de 12 GB, esto hace más lenta cada respuesta.
- Strata lee entero el primer mensaje de un chat, unos 1 minuto por cada 30.000 tokens. Los mensajes siguientes empiezan en segundos.
- Dónde se guardan tus chats y la API completa: [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored), [docs/DETAILS.md](docs/DETAILS.md#using-it).
## Compilar desde el código fuente
<details>
<summary>Windows, Linux y Mac desde el repositorio</summary>
[Descarga Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) y descomprímelo, o haz `git clone`.
**Windows:** haz doble clic en `START-HERE.bat`. **Linux:** ejecuta `./setup.sh` en la carpeta de Strata.
**Mac (Apple Silicon, macOS 15 o posterior):** instala una vez las Command Line Tools de Apple con `xcode-select --install`
(no hace falta Xcode). Abre la Terminal en la carpeta de Strata y ejecuta `./setup.sh`. El script compila el motor para Mac
en tu Mac (unos 7 minutos), hace las mismas preguntas, descarga el modelo y lo arranca. La próxima vez, `./setup.sh` lo
arranca enseguida. Qué tamaño cabe en tu Mac: [docs/MAC.md](docs/MAC.md).
El instalador detecta tu tarjeta y prepara el motor adecuado. Pregunta qué modelo y qué tamaño quieres, cuánto contexto
(cuánto texto mantiene el modelo en mente) y si debe leer imágenes. Pulsa Enter para aceptar la respuesta recomendada.
Luego descarga el modelo (unos 70 GB) y lo arranca. Si la descarga se detiene, vuelve a ejecutar el script y continúa. Tu
navegador abre la página de Strata en `http://127.0.0.1:8080`.
> Mientras el modelo arranca, tu PC puede ir lento o dejar de responder durante 1-3 minutos (más tiempo la primera vez).
> Strata carga 35-55 GB en tu RAM y bloquea una parte para la tarjeta gráfica. Espera y no cierres la ventana.
La próxima vez, ejecuta de nuevo `START-HERE.bat` (o `./setup.sh`). Arranca enseguida y no descarga nada dos veces. Cierra
su ventana para detener el modelo. `UPDATE.bat` (`./update.sh`) actualiza Strata sin arrancarlo. Actualización, Docker,
varias tarjetas, dónde van los archivos y todas las opciones: [docs/INSTALL.md](docs/INSTALL.md).
Para medir tu PC, ejecuta `BENCH.bat` (`./bench.sh`). Tarda 5-10 minutos y escribe un informe que puedes compartir
([cómo](docs/COMMUNITY_BENCHMARKS.md#run-the-benchmark)).
**Opcional: más velocidad en una tarjeta cuando todo el modelo cabe en la RAM.** Dos ajustes que se activan a mano, de
RubenTorney (#1904, #1905). Ayudan cuando usas una sola tarjeta gráfica y tu RAM guarda todos los expertos, por ejemplo con
Q2_0 o IQ3_XXS en un PC de 64 GB. Abre `strata-<model>.json` en la carpeta de datos de Strata ([dónde](docs/INSTALL.md#where-things-are-stored)) y añade:
- `"--adapt-async", "1"` a la lista `"args"`. Así la caché de expertos se actualiza en un hilo auxiliar en vez de detener la
respuesta. Medido en una RTX 5070 (Windows, Q2_0): +4-5 % en decodificación, +7-10 % en código y documentos largos. Las
respuestas pueden diferir un poco de las del ajuste por defecto y son iguales de una ejecución a otra.
- `"STRATA_PIN_BY_NEED": "1"` en el bloque `"env"` (Windows). Windows solo puede bloquear aproximadamente la mitad de la RAM
para la tarjeta gráfica, y este ajuste bloquea primero los expertos que la tarjeta necesita antes. Medido en la RTX 5070: +4,4 % en lectura del prompt.
```json
"args": [ ..., "--adapt-async", "1" ],
"env": { "STRATA_PIN_BY_NEED": "1" }
```
Reinicia Strata después del cambio. Para deshacerlo, quita las dos entradas.
Sigue las indicaciones para descargar e iniciar un modelo. [Guía del Terminal](docs/TERMINAL_LAUNCH.md).
</details>
## ¿Cómo funciona?
<p align="center">
<img src="docs/media/terminal-monitor.png" width="900" alt="Aplicación de Terminal de Strata con la actividad del modelo, la velocidad, la memoria y las peticiones recientes">
<br><sub>Observa tu modelo y tu hardware mientras se ejecuta.</sub>
</p>
Los modelos como este suelen ejecutarse en servidores con cientos de gigabytes de memoria gráfica. Tu tarjeta gráfica tiene
12-24 GB. Strata hace que el modelo quepa repartiendo el trabajo por todo tu PC. Imagina una biblioteca.
<a id="build-from-source"></a>
- El modelo es un equipo de 24.576 pequeños especialistas ("expertos"), como 24.576 libros de consulta. Cada palabra necesita solo 10 de ellos.
- **Tu tarjeta gráfica es el escritorio.** Es pequeño y rápido. Los unos 4.000 libros que más se usan están abiertos encima.
- **Tu RAM son las estanterías de la sala de lectura.** Los 24.576 libros están ahí. Cuando una palabra necesita un libro que
no está en el escritorio, tu procesador lo coge enseguida de la estantería mientras la tarjeta sigue trabajando.
- **Tu SSD es el archivo.** Guarda una tabla de consulta de 29 GB. El modelo lee unas pocas filas por palabra, así que no
hace falta que esté en la RAM.
[Compilar desde el código fuente](docs/SOURCE_INSTALL.md) · [Docker](docs/DOCKER.md) · [Guía de instalación completa](docs/INSTALL.md)
<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>
<a id="what-you-need"></a>
Un pequeño ayudante también adivina las siguientes palabras. El modelo grande las comprueba todas a la vez y se queda con
las correctas. Obtienes la misma respuesta, entre 1,6 y 1,8 veces antes. Los textos largos se leen en trozos grandes, de
hasta 8.192 tokens a la vez, a más de 1.000 tokens por segundo.
## Hardware
<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>
| Hardware compatible | Guía |
| --- | --- |
| **NVIDIA GeForce RTX 20 / 30 / 40 / 50** | [Instalación en NVIDIA](docs/INSTALL.md#what-you-need) |
| **AMD Radeon RX 7700 / 7800 / 7900, RX 9060 / 9070, AI PRO R9700** | [Instalación en AMD](docs/AMD_HIP.md) |
| **AMD Radeon RX 6800 / 6900 · experimental** | [Instalación en RX 6000](docs/AMD_HIP.md#rdna2-gfx1030) |
| **Intel Arc · Linux** | [Instalación en Intel](docs/INTEL_ARC.md) |
| **Apple Silicon · M1 o posterior** | [Instalación en Mac](docs/MAC.md) |
| **AMD Ryzen AI Max · Strix Halo** | [Instalación en Strix Halo](docs/STRIX_HALO.md) |
La explicación más larga: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Cada parte con sus cifras:
[los detalles](docs/DETAILS.md#how-it-works) y el [artículo](docs/paper/Strata-Paper.pdf).
<a id="using-it"></a>
## ¿Algo ha ido mal?
## Herramientas
- **Mi PC se congeló la primera vez que arrancó Strata.** Carga el modelo en la RAM. Espera y no cierres la ventana. ¿Sigue
congelado después de 10 minutos? Reinicia el PC, cierra otros programas e inténtalo de nuevo, o elige un tamaño más pequeño.
- **Se detuvo durante la descarga o la instalación.** Abre de nuevo la aplicación o `strata launch`, o vuelve a ejecutar
`START-HERE.bat` (o `./setup.sh`). Continúa donde se quedó.
- **Va muy lento, la luz del disco parpadea sin parar, o dice "the engine stopped unexpectedly".** Tu PC no tiene bastante
RAM libre. Cierra otros programas (los navegadores gastan mucha) o elige un tamaño más pequeño, como Q2_0 o IQ2_XS.
- **Dice que no hay motor para este PC.** En la aplicación, abre **Ajustes, Motor** y pulsa **Actualizar motor**. En la
Terminal, ejecuta `strata update`. `strata doctor` muestra qué archivos del motor están instalados.
- **Dice que el puerto 8080 ya está en uso.** Strata ya se está ejecutando. Busca su ventana.
**Claude Code · Codex CLI · OpenCode · oh-my-pi · pi.dev · Hermes Agent · OpenClaw**, y más, mediante las API compatibles con OpenAI y Anthropic.
Más problemas y soluciones: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). ¿Sigues atascado? Abre una
[incidencia](https://github.com/Niko1221/Strata/issues). En la aplicación, adjunta el archivo de **Ajustes, Motor,
Diagnóstico, Ver registros, Guardar registros**. En la Terminal, pega la salida de `strata doctor`. Si lo ejecutas desde el
código fuente, adjunta `strata-<model>.log` de la carpeta de Strata. Para un problema de seguridad, avísanos en privado:
[SECURITY.md](SECURITY.md).
Ejecuta **`strata connect`** para ver las instrucciones de tu herramienta. [Guía de conexión](docs/TERMINAL_LAUNCH.md#connect-your-apps-strata-connect).
## Créditos y licencia
<a id="guides"></a>
<a id="how-fast-is-it"></a>
<a id="how-does-it-work"></a>
<a id="something-went-wrong"></a>
El modelo es [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next), del equipo de Qwen. Lo comprimieron
[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5), agentionai (Gyro)
y Unsloth. Strata usa partes de [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Todos los créditos:
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata es de código abierto con la [licencia MIT](LICENSE). Algunas
partes y todos los modelos tienen sus propias licencias ([cuáles](docs/HOW_IT_WORKS.md#license)).
## Ayuda
## Contribuir
¿Has encontrado un error, o quieres enviar un cambio o un benchmark? Lee primero [CONTRIBUTING.md](CONTRIBUTING.md). Explica
qué incluir para que otros puedan comprobar tu trabajo.
[Todas las guías](docs) · [Solución de problemas](docs/TROUBLESHOOTING.md) · [Informar de un error](https://github.com/Niko1221/Strata/issues/new/choose) ·
[Benchmarks](docs/COMMUNITY_BENCHMARKS.md) · [Cómo funciona](docs/HOW_IT_WORKS.md) · [Contribuir](CONTRIBUTING.md)
## Apoya Strata
Strata es gratis y tiene licencia MIT. Lo construyo y lo pruebo en el hardware que consigo. Si te funciona bien en tu PC, un café me ayuda
a seguir probando tarjetas nuevas y a arreglar lo que nos cuentas. Sin presión. Usarlo y contar lo que encuentras ayuda igual de mucho.
**Ayuda a mantener la IA local abierta a todos.** Un patrocinio en GitHub o un café ayuda a financiar el desarrollo y las pruebas en más hardware.
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a>
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor_on_GitHub-EA4AAA?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="GitHub Sponsors: patrocina Strata" height="48"></a>
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee: apoya Strata" height="48"></a>
¿Te interesa patrocinar Strata? Escríbeme a través de [GitHub](https://github.com/Niko1221).
### Apoyos fieles
Gracias a todas las personas que apoyan Strata.
<!-- supporters:start (one line per public supporter, newest last; copy a line and change the GitHub name in all four places) -->
<a href="https://github.com/ryan4yin"><img src="https://github.com/ryan4yin.png?size=96" width="48" height="48" alt="ryan4yin" title="ryan4yin"></a>
<a href="https://github.com/themactep"><img src="https://github.com/themactep.png?size=96" width="48" height="48" alt="themactep" title="themactep"></a>
<a href="https://github.com/macintog"><img src="https://github.com/macintog.png?size=96" width="48" height="48" alt="macintog" title="macintog"></a>
<a href="https://github.com/bisonbet"><img src="https://github.com/bisonbet.png?size=96" width="48" height="48" alt="bisonbet" title="bisonbet"></a>
<a href="https://github.com/vizv"><img src="https://github.com/vizv.png?size=96" width="48" height="48" alt="vizv" title="vizv"></a>
<!-- supporters:end -->
[Licencia MIT](LICENSE) · [Créditos y licencias de los modelos](docs/HOW_IT_WORKS.md#credits) · [Privacidad](docs/SAFETY.md)
<a id="option-c-let-your-ai-set-it-up"></a>
<sub>Para herramientas de IA y lectores automáticos: <a href="AGENTS.md">referencia técnica</a> · <a href="docs/AI_SETUP.md">instrucciones de instalación</a>.</sub>
**Patrocinios, alianzas de hardware o colaboraciones:** [[email protected]](mailto:[email protected]).
+135 -353
View File
@@ -1,420 +1,202 @@
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="apps/host/public/brand/strata-lockup-dark.svg">
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="380">
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="340">
</picture>
</p>
<p align="center"><a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.de.md">Deutsch</a> · <b>Français</b> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português</a></p>
<h3 align="center">Faites tourner un modèle de pointe sur votre propre PC de jeu</h3>
<p align="center">Compatible avec NVIDIA, AMD, Intel · Windows, Linux, Mac · gratuit et open source</p>
<h3 align="center">Une IA puissante sur votre propre ordinateur.</h3>
<p align="center">Discutez, programmez et travaillez avec des images, en local.</p>
<p align="center">
<a href="https://github.com/Niko1221/Strata/releases"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Latest release"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT license"></a>
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="GitHub stars"></a>
<a href="#modèles">Modèles</a> · <a href="#installation"><b>Installation</b></a> ·
<a href="#matériel">Matériel</a> · <a href="#outils">Outils</a> ·
<a href="#aide">Aide</a> · <a href="#soutenir-strata">Soutenir</a>
</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>Un jardin de pagode en voxels, généré en une seule requête sur une RTX 5070 avec Strata (IQ3_S, contexte de 128K) ·
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">vidéo complète (49 s)</a></sub></p>
<p align="center">
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?label=download&color=0ab887" alt="Télécharger Strata"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="Licence MIT"></a>
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor-EA4AAA?logo=githubsponsors&logoColor=fff" alt="Soutenir Strata sur GitHub Sponsors"></a>
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy_me_a_coffee-FFDD00?logo=buymeacoffee&logoColor=000" alt="Soutenir Strata sur Buy Me a Coffee"></a>
</p>
Strata fait tourner **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**, un modèle de 125 milliards
de paramètres qui demande normalement un serveur, sur un PC ordinaire. Il discute, écrit du code, lit des images et
fonctionne avec vos applications et vos agents de code. Vos conversations et vos fichiers restent sur votre PC.
<p align="center"><sub><a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.de.md">Deutsch</a> · <b>Français</b> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português</a> · <a href="README.pl.md">Polski</a></sub></p>
**Nouveau ici ?** Regardez [ce qu'il vous faut](#ce-quil-vous-faut), puis [installez l'application](#option-a--lapplication-strata).
**Gratuit, open source, et à vous de l'exécuter.** Utilisez Strata sur votre bureau ou avec vos outils de programmation préférés. Aucun compte nécessaire.
- **Tourne sur un PC de jeu.** 32 Go de RAM et de mémoire graphique réunis suffisent pour commencer.
- **Fonctionne avec vos outils.** Il parle les API d'OpenAI et d'Anthropic, donc Claude Code, Codex CLI, OpenCode et d'autres agents peuvent s'y connecter.
- **Lit les images.** C'est activé par défaut.
- **Gratuit.** Licence MIT, pas de compte, pas de cloud.
- **Privé.** Aucune télémétrie. Ce que Strata télécharge, quand il utilise internet et comment il vérifie les téléchargements est expliqué dans [docs/SAFETY.md](docs/SAFETY.md).
## Modèles
Version 0.2.3 · moteur 0.1.48
Nous recommandons **d'abord ISTA-DASLab**, puis **Unsloth** et d'autres fournisseurs, lorsqu'un modèle compatible tient en mémoire.
## Ce qu'il vous faut
<details>
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> &nbsp;<b>Qwen3.8-Flash-Next</b></summary>
**Il vous faut au moins 32 Go de RAM et de mémoire graphique (VRAM) réunis.** Sur un Mac ou un Ryzen AI Max, c'est la
mémoire unifiée. Plus de mémoire permet un modèle plus gros ou plus rapide.
- Un PC de 32 Go avec une carte de 12 à 16 Go fait tourner le Coder.
- Avec 64 Go de RAM, toutes les tailles fonctionnent.
| Votre ordinateur | Détails |
| Fournisseur | Choix |
| --- | --- |
| **NVIDIA** GeForce RTX série 20, 30, 40, 50 (12 Go ou plus) | Installez l'application ou le Terminal. |
| **AMD** Radeon RX 6800 et plus récent, Radeon AI PRO R9700 (12 Go ou plus) | [Cartes AMD](docs/AMD_HIP.md) |
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md), [moteur Intel](docs/INTEL.md) |
| **Mac Apple Silicon** (M1 ou plus récent, macOS 15 ou plus récent) | [Mac](docs/MAC.md) |
| **AMD Ryzen AI Max (Strix Halo)** et autres systèmes à mémoire unifiée | [Strix Halo](docs/STRIX_HALO.md) |
| Cartes et processeurs plus anciens | [GPU anciens](docs/OLDER_GPUS.md), [CPU anciens](docs/INSTALL.md#older-cpus-experimental) |
| **ISTA-DASLab · recommandé** | Q2\_0, IQ2\_XS, IQ3\_XXS, IQ3\_S. [Coder : IQ1\_M](docs/MODELS.md#coder) pour programmer. |
| **Unsloth** | UD-IQ4\_XS, UD-Q4\_K\_XL, UD-Q5\_K\_XL, UD-Q6\_K\_XL, Q8\_0 |
| **Gyro · agentionai** | [Gyro-S, Gyro-M](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF). Utilise moins de mémoire, mais plus lent. |
| **Autres options** | [Swift 1.5](docs/MODELS.md#swift-15), [NVFP4](docs/NVFP4.md), [OrcaRouter](docs/ORCA.md) |
Il faut aussi :
[Comparer les tailles, la mémoire nécessaire et la compatibilité](docs/MODELS.md).
- environ 80 Go d'espace disque libre (un SSD accélère beaucoup le premier démarrage)
- Windows 10 / 11, Linux ou macOS
- un pilote graphique récent
</details>
Deux ou trois cartes de la même marque peuvent se partager un modèle ([multi-GPU](docs/MULTI_GPU.md)). Strata ne répartit
jamais un modèle entre des cartes NVIDIA et AMD. Liste complète : [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
<details>
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> &nbsp;<b>Qwen3.6-35B-A3B</b></summary>
**Unsloth:** UD-Q4_K_XL, UD-Q6_K_XL, Q8_0. Ce sont les paquets actuellement documentés pour ce modèle.
[Guide du modèle et compatibilité](docs/QWEN36.md).
</details>
<details>
<summary><img src="docs/media/models/deepseek.svg" width="22" alt="" align="middle"> &nbsp;<b>DeepSeekV4.1-Flash-1017</b></summary>
Dans l'**application de bureau** et dans le **Terminal**. Ce qui ne tient pas en mémoire est lu depuis votre SSD : il faut donc beaucoup d'espace disque libre.
| Jeu (ISTA-DAS) | Téléchargement | Espace disque libre nécessaire |
| --- | ---: | ---: |
| 1.75 bpw | 239 GB | environ 245 GB |
| 2.00 bpw | 257 GB | environ 265 GB |
| 2.25 bpw | 274 GB | environ 280 GB |
Fonctionne sur les NVIDIA RTX 30, 40 et 50 (Windows et Linux) et sur l'AMD Radeon AI PRO R9700 (Linux). L'installation télécharge les fichiers et le moteur DeepSeek, puis démarre le modèle.
```sh
strata launch --family deepseek41 --model ist-das-2.00bpw --plan
```
Mesuré avec le jeu 2.00 bpw sur Strata 0.2.5. « Sortie » est la vitesse de la réponse, « Prompt » la vitesse à laquelle Strata lit votre texte.
| Matériel | Sortie | Prompt |
| --- | ---: | ---: |
| RTX 5070 12 GB, 64 GB de RAM | 4 à 6 tokens/s | environ 110 tokens/s (4K) |
| RTX 3060 12 GB, 46 GB de RAM | 9 à 11 tokens/s | environ 155 tokens/s (4K) |
| 1x Radeon AI PRO R9700, 251 GB de RAM | 21 à 25 tokens/s | 320 à 360 tokens/s (8K) |
| 4x Radeon AI PRO R9700 | 74 à 77 tokens/s | 400 à 450 tokens/s (8K) |
[Mémoire, disque et détails](docs/DEEPSEEK41.md) · [Notes de version](https://github.com/Niko1221/Strata/releases)
</details>
<a id="the-other-sizes"></a>
Vous ne savez pas ce qui tient ? [Choisir un modèle pour votre ordinateur](docs/MODELS.md#pick-by-memory).
## Installation
Choisissez une option. Elles lancent toutes le même moteur et utilisent les mêmes modèles.
<a id="option-a-the-strata-app"></a>
### Option A : l'application Strata
### Application de bureau
Le plus simple. L'application contient le moteur : installez-la, choisissez un modèle, discutez. Téléchargez le fichier
correspondant à votre système et à votre carte graphique depuis la [page des versions](https://github.com/Niko1221/Strata/releases/latest).
Vous ne savez pas lequel prendre ? Lisez le paragraphe sous le tableau.
**[Télécharger Strata pour Windows, Linux ou Mac →](https://github.com/Niko1221/Strata/releases/latest)**
| Système | Votre carte graphique | Fichier à télécharger |
| --- | --- | --- |
| **Windows 10 / 11** | NVIDIA RTX, pilote 580 ou plus récent | [`Strata-0.2.3-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda13.exe) |
| | NVIDIA, carte plus ancienne ou pilote antérieur à 580 | [`Strata-0.2.3-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda12.exe) |
| | AMD Radeon série RX 9000 ou Radeon AI PRO R9700 | [`Strata-0.2.3-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip.exe) |
| | AMD Radeon série RX 7000 ou Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip-rdna3.exe) |
| **Linux** | NVIDIA RTX, pilote 580 ou plus récent | [`Strata-0.2.3-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda13.AppImage) |
| | NVIDIA, carte plus ancienne ou pilote antérieur à 580 | [`Strata-0.2.3-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda12.AppImage) |
| | AMD Radeon série RX 9000 ou Radeon AI PRO R9700 | [`Strata-0.2.3-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx9070.AppImage) |
| | AMD Radeon série RX 7000 | [`Strata-0.2.3-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx7900.AppImage) |
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-strixhalo.AppImage) |
| **Mac** (Apple Silicon, macOS 15 ou plus récent) | intégrée | [`Strata-0.2.3-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-mac.dmg) |
Installez l'application, choisissez un modèle et commencez à discuter. Il y a un seul installateur par système. Au premier lancement, l'application télécharge le moteur de votre carte graphique. [Aide pour l'installateur](docs/INSTALL.md#the-app).
**Si vous hésitez :** pour NVIDIA, prenez `cuda13` si la version de votre pilote est 580 ou plus récente, sinon `cuda12`.
`cuda12` convient aux cartes plus anciennes et aux pilotes plus anciens. Les fichiers `hip` et `amd` sont pour les cartes
AMD. Les autres cartes AMD (RX 6000) n'ont pas encore d'application prête à l'emploi : voir
[Cartes AMD](docs/AMD_HIP.md).
<sub>Fichiers: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
Ensuite :
<p align="center">
<img src="docs/media/app-chat.png" width="900" alt="Application de bureau Strata avec une discussion et un aperçu du code en direct côte à côte">
<br><sub>Discussion, fichiers et aperçu en direct dans une seule fenêtre.</sub>
</p>
- **Windows :** lancez le `.exe`. Il s'installe pour votre utilisateur et ne demande pas de droits d'administrateur. Si
Windows affiche « Windows a protégé votre ordinateur », choisissez **Informations complémentaires**, puis **Exécuter
quand même** (l'installateur n'est pas encore signé).
- **Linux :** rendez le fichier exécutable (`chmod +x`) et lancez-le. Il a besoin de FUSE 2 : `sudo apt install libfuse2`
(Ubuntu 22.04) ou `libfuse2t64` (Ubuntu 24.04 et plus récent).
- **Mac :** ouvrez le `.dmg` et faites glisser Strata dans Applications.
<a id="option-b-the-terminal-one-line"></a>
Sur le premier écran, l'application analyse votre PC et recommande un modèle adapté. Appuyez sur Installer. Le modèle est un
gros téléchargement (environ 40 à 70 Go) qui reprend là où il s'est arrêté en cas d'interruption. Quand vous fermez la
fenêtre, Strata et le moteur s'arrêtent.
### Application Terminal
### Option B : le Terminal (une ligne)
Vous préférez le terminal ? Installez Strata, puis lancez **`strata launch`**.
Le Terminal est la commande `strata`, pensée pour les scripts, les serveurs et les agents d'IA.
<details open>
<summary><b>Commandes d'installation</b></summary>
Linux et Mac :
**Linux / Mac**
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
```
Windows (PowerShell) :
**Windows PowerShell**
```powershell
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
```
Le script analyse votre PC, télécharge le Terminal, vérifie son empreinte SHA-256 et le décompresse dans votre dossier
personnel. Il ne demande pas de droits d'administrateur. Sous Windows, ouvrez ensuite une nouvelle fenêtre de terminal.
Puis lancez Strata :
Sous Windows, ouvrez ensuite un nouveau terminal. Puis :
```sh
strata launch
```
Il pose une question à la fois : modèle, contexte, images, qui peut se connecter. Ensuite il télécharge le moteur pour votre
carte graphique (signé, et lancé à titre d'essai avant usage) ainsi que le modèle, puis commence à servir. Pour les scripts
et les agents d'IA, utilisez des options :
```sh
strata launch --model q2_0 --yes --no-tui
```
Pour lire le script d'installation avant de l'exécuter, téléchargez-le d'abord avec `curl -fsSL <address> -o install.sh`,
lisez-le, puis lancez `sh install.sh`. Tous les écrans et toutes les options : [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
### Option C : laissez votre IA l'installer
Collez ceci dans Claude Code, Cursor, Codex, GitHub Copilot ou un autre assistant de programmation :
```text
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
```
L'assistant vérifie votre carte graphique, votre RAM et votre disque, choisit le modèle adapté, l'installe, le démarre et
vous explique comment connecter vos applications. Les outils d'IA peuvent aussi installer, démarrer et arrêter Strata via son
[serveur MCP](docs/MCP_SERVER.md).
Vous préférez le compiler vous-même ? Voir [Compiler depuis les sources](#compiler-depuis-les-sources).
## Modèles
L'application et `strata launch` recommandent un modèle selon votre mémoire. C'est un seul modèle en plusieurs tailles,
plus ou moins compressé. Les petites tailles sont plus rapides. Les grandes sont un peu plus intelligentes.
### Peu de mémoire ? Essayez Gyro
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) (par agentionai) est la façon la plus légère de faire tourner ce modèle.
| Taille | Mémoire nécessaire (RAM + VRAM) | Téléchargement | Ce que c'est |
| --- | ---: | ---: | --- |
| **Gyro-S** | 27,6 Go | 58,5 Go | Pour les PC où rien d'autre ne tient. La qualité est inférieure à celle d'IQ2_XS. |
| **Gyro-M** | 35 Go | 92,0 Go | À peu près la qualité d'IQ3_XXS avec environ 20 % de mémoire GPU en moins. Il écrit les réponses 16 % plus lentement que Q2_0 sur une RTX 3060 de 12 Go et 21 % plus lentement sur une R9700. |
Gyro fonctionne sur les cartes NVIDIA et AMD (expérimental sur AMD) sur les cartes Intel Arc sous Linux (testé sur une Arc A750) et, depuis la 0.2.2, sur les Mac Apple Silicon (Gyro-S sur un Mac mini de 32 Go : 9 tokens/s, experts lus sur le SSD). Détails et mesures :
[docs/GYRO.md](docs/GYRO.md). Lancez-le avec `strata launch --model gyro-s`.
### Les autres tailles
| Votre mémoire (RAM + VRAM) | Prenez | Remarques |
| --- | --- | --- |
| **32 Go** | **Gyro-S**, ou le **[Coder](docs/MODELS.md#coder)** pour le code | Le Coder demande 32 Go de RAM et garde la moitié des experts, il est donc moins bon hors du code |
| **40 Go** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39,2 Go), ou **Gyro-M** (35 Go) | Swift réfléchit moins longtemps avant de répondre. Gyro-M approche la qualité d'IQ3_XXS, mais il écrit les réponses 16 à 21 % plus lentement que Q2_0 |
| **48 Go** | **IQ3_XXS** (47,0 Go) | Plus lent, meilleur |
| **64 Go** | **IQ3_S** (54,8 Go), l'original ou Swift 1.5 | Le plus lent, le meilleur. Il égale le modèle complet dans les tests publiés |
| **96 Go ou plus** | **UD-IQ4_XS** d'Unsloth (environ 72 Go de RAM) jusqu'à **Q8_0** (environ 136 Go), 4 à 8 bits | Plus proche du modèle complet. Avec moins de RAM, Strata lit une partie depuis le SSD, ce qui est beaucoup plus lent |
Avoir moins de RAM que ce qu'indique une ligne peut encore fonctionner : Strata passe tout seul en
[mode peu de RAM](docs/MODELS.md#a-big-graphics-card-and-little-ram). Attendez-vous à plus de lenteur, et à bien plus avec
une petite carte graphique.
Autres choix :
- **[Swift 1.5](docs/MODELS.md#swift-15)** est une version affinée qui réfléchit moins longtemps avant de répondre, pour une qualité et un besoin en RAM à peu près identiques.
- **[NVFP4](docs/NVFP4.md)** est la version 4 bits de NVIDIA pour les cartes NVIDIA. Elle demande environ 80 Go de RAM pour garder tous les experts en RAM.
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** est un modèle plus petit et différent, qui lit les images et peut répondre à plusieurs requêtes à la fois.
- **[L'Uncensored IQ3_XXS d'OrcaRouter](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** se configure à la main.
Toutes les tailles, les téléchargements et ce qui tient où : [docs/MODELS.md](docs/MODELS.md).
### DeepSeek V4.1-Flash (dans le Terminal, depuis 0.2.4)
[DeepSeek V4.1-Flash](https://huggingface.co/ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF) est un autre modèle, beaucoup plus gros. ISTA-DASLab a produit les trois versions ci-dessous. Strata les fait tourner sur un PC avec une carte graphique de 12 Go et de 46 à 64 Go de RAM, et lit le reste sur votre SSD. Il fonctionne sur les cartes NVIDIA RTX des séries 30 à 50 et sur la Radeon AI PRO R9700. Il utilise son propre paquet de moteur, `strata-dsv41`, que `strata launch` télécharge quand vous choisissez DeepSeek. L’application de bureau ne le propose pas encore.
| Version | Téléchargement | Qualité par rapport au modèle d’origine | Écrit à |
| --- | ---: | --- | --- |
| **IQ1_M** (1,75 bit) | 239 Go | KL 0,217, perplexité 3,80 (+21 %) ; code +8,9 %, tâches d’agent +10,2 % ; plus faible sur la prose anglaise courante. D’après la fiche du modèle d’ISTA-DASLab, elle garde 98,7 % de la moyenne des tâches du modèle de base (AIME25, GPQA-Diamond, LiveCodeBench v6). | pas encore mesurée |
| **IQ2_XXS** (2.00 bits) | 257 GB | KL 0,170, perplexité 3,63 (+15,5 %) ; code +8,6 %, tâches d’agent +5,3 % | 6 à 9 tokens/s sur une RTX 3060, environ 5 sur une RTX 5070 |
| **IQ2_XS** (2.25 bits) | 274 GB | KL 0,107, perplexité 3,37 (+7,3 %) ; code +3,2 %, tâches d’agent +2,2 % | environ 11 tokens/s sur une Radeon AI PRO R9700 |
| **IQ4_NL** (table Engram, pas un troisième modèle) | 111 Go, déjà compris dans les trois téléchargements ci-dessus | La table de consultation Engram en 4 bits que le modèle lit dans la RAM ou sur le SSD. Les trois versions utilisent ce seul fichier (`DeepSeek-V4.1-Flash-Engram-IQ4_NL.gguf`). | n/a |
4x Radeon AI PRO R9700 : environ 31 tokens/s, 55 à 69 avec DSpark.
Lancez-le avec `strata launch --model ist-das-2.00bpw` (ou `ist-das-1.75bpw`, `ist-das-2.25bpw`). Détails, vitesses par carte et installation manuelle : [docs/DEEPSEEK41.md](docs/DEEPSEEK41.md).
## Quelle est sa vitesse ?
Mesuré avec une carte graphique de 12 Go (RTX 5070, Ryzen 5 7600, 64 Go de RAM). Voir les résultats d'autres cartes :
[vitesse de chaque taille](docs/MODELS.md#how-fast-is-each-size), [résultats de la communauté](docs/COMMUNITY_BENCHMARKS.md).
| Modèle | Les réponses s'affichent à | Lit votre prompt à |
| --- | --- | --- |
| **Q2_0** | `████████████` 94 tokens/s | `████████████` 2 650 tokens/s |
| **IQ2_XS** | `██████████░░` 79 tokens/s | `█████████░░░` 2 090 tokens/s |
| **IQ3_XXS** | `████████░░░░` 62 tokens/s | `████████░░░░` 1 750 tokens/s |
| **IQ3_S** | `███████░░░░░` 53 tokens/s | `███████░░░░░` 1 620 tokens/s |
| **Coder** | `███████░░░░░` 55 tokens/s | `██████████░░` 2 180 tokens/s |
Un token vaut environ ¾ de mot, donc 60 tokens par seconde, c'est plus rapide que votre lecture. « Lit votre prompt »
indique la vitesse à laquelle Strata absorbe un long document, du code ou un historique de discussion.
Mesuré avec une carte AMD de 16 Go (RX 9070 XT, Ryzen 9 3900X, 47 Go de RAM) :
| Modèle | Les réponses s'affichent à | Lit votre prompt à |
| --- | --- | --- |
| **Q2_0** | `████████░░░░` 60 tokens/s | `█████░░░░░░░` 1 160 tokens/s |
| **IQ2_XS** | `███████░░░░░` 52 tokens/s | `█████░░░░░░░` 1 110 tokens/s |
| **Coder** | `██████░░░░░░` 44 tokens/s | `██████░░░░░░` 1 420 tokens/s |
Les barres utilisent la même échelle dans les deux tableaux. Une carte avec plus de VRAM est plus rapide. Une RTX 3090
(24 Go) devrait écrire environ 100 à 140 tokens par seconde. La version du moteur derrière chaque chiffre est indiquée dans
[DETAILS.md](docs/DETAILS.md#speed-measured).
## Utilisation
### Dans l'application
L'application comprend un **Chat**, un **Monitor** du modèle, de votre GPU, de votre CPU et de votre RAM, une page **Models** et les **Settings**. Le chat peut travailler dans un dossier de votre PC. En mode **Manual**, il demande
avant de modifier un fichier ou d'exécuter une commande. Tapez `/loop 10m <what to do>` pour répéter un message toutes les
10 minutes tant que le chat est ouvert.
Le chat peut chercher sur le web avec DuckDuckGo (ou Bing), sans compte. Vous pouvez ajouter une clé de recherche sous
**Settings, Tools**. La recherche est la seule fonction qui envoie quelque chose hors de votre PC (les mots recherchés).
Pour faire tourner deux modèles, activez **Run multiple models** (exécuter plusieurs modèles) et donnez à chacun sa propre carte de la même marque.
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="The Strata app: a chat where the agent built a small voxel pagoda garden, with the page open in the preview on the right"><br>
<sub>Une discussion dans l'app Strata : l'agent a écrit un petit jardin de pagode en voxels dans un seul fichier HTML, et la page s'ouvre dans l'aperçu à côté de la conversation</sub></p>
### Dans le Terminal
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
<sub><code>strata monitor</code> pendant que le modèle répond : vitesse, GPU, mémoire et les dernières requêtes</sub></p>
```sh
strata launch # guided screens: model, context, pictures, access, then serve
strata launch --model q2_0 --yes --no-tui # no questions: serve, print the address, return
strata launch --plan --model q2_0 # show what would happen; nothing changes
strata launch --status # what is being served now
strata launch --stop # stop it
strata monitor --url http://127.0.0.1:8080 # watch speed, GPU, memory and requests
strata update --check # is there a new engine? (strata update installs it)
strata doctor # check the installed engine files
```
`strata --help` liste tout. Démarrer et servir ne téléchargent rien. Seuls l'installation, `strata launch` (quand vous
choisissez un modèle) et `strata update` le font. Pour joindre Strata depuis votre téléphone ou un autre PC, utilisez
`strata launch --access lan`. La commande crée une clé d'API et refuse de démarrer sur un réseau sans elle. Aucun
navigateur ne s'ouvre, sauf si vous ajoutez `--open`. Guide : [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md). Le client complet : [docs/TERMINAL.md](docs/TERMINAL.md).
### Avec des agents d'IA
Un agent installe Strata avec la commande en une ligne de l'option B et lance
`strata launch --model NAME --yes --no-tui`. Les étapes, les options et les codes de sortie sont dans [docs/AI_SETUP.md](docs/AI_SETUP.md),
et les règles du dépôt dans [AGENTS.md](AGENTS.md).
### Connecter vos autres applications
| Type d'application | URL de base |
| --- | --- |
| Compatible OpenAI (la plupart des applications et des agents) | `http://127.0.0.1:8080/v1` (n'importe quelle clé d'API, n'importe quel nom de modèle) |
| API Anthropic (Claude Code et d'autres) | `http://127.0.0.1:8080`, sans `/v1`. Ces applications ajoutent `/v1/messages` elles-mêmes. Claude Code : `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
| API OpenAI Responses (Codex CLI) | `http://127.0.0.1:8080/v1/responses` ([configuration](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
`strata connect` affiche les réglages exacts pour OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent et OpenClaw.
`strata connect opencode --write` écrit le fichier après confirmation et garde une sauvegarde.
### Images
Les images sont activées par défaut pour les modèles qui savent les lire. Joignez ou collez une image dans le chat, ou
envoyez-la depuis votre application. Le lecteur d'images tourne sur le processeur quand votre carte graphique a moins de
24 Go (quelques secondes par image, sans prendre de mémoire à la carte) et sur la carte graphique à partir de 24 Go. La
règle est la même pour NVIDIA et AMD. Vous pouvez désactiver les images par modèle, ou choisir le lecteur avec
`--vision off`, `--vision gpu` ou `--vision cpu`.
Les chiffres derrière la règle : sur une Radeon AI PRO R9700, le lecteur de la carte a pris 57 ms pour une image de
260 tokens et celui du processeur 3,1 s. Sur des cartes de 16 Go (Tesla P100 et RTX A4000, Linux), le lecteur de la carte
a coûté 3 à 5 % de vitesse sur le texte, parce qu'il occupe environ 1,4 Go que le modèle utiliserait sinon. Sur un Mac, le lecteur tourne sur le GPU quelle que soit la mémoire (0,45 s pour une image de 260 tokens sur un M4 de 32 Go, environ 1,2 Go de la
mémoire partagée) ; le processeur met environ 50 s par image, il ne lit donc qu'avec `--vision cpu`.
### Bon à savoir
- Choisissez le niveau de réflexion (**désactivé, faible, moyen ou élevé**) dans le menu du chat ou dans le réglage « reasoning effort » de votre application. Désactivé est le plus rapide.
- Strata répond à une requête à la fois par défaut. Pour en traiter plusieurs en même temps, utilisez `--parallel N` dans `strata launch` ou
`"parallel": 2` dans la configuration ([BATCHING.md](docs/BATCHING.md)). Sur une carte de 12 Go, chaque réponse devient alors plus lente.
- Strata lit en entier le premier message d'une discussion, soit environ 1 minute pour 30 000 tokens. Les messages suivants démarrent en quelques secondes.
- Où sont stockées vos discussions et l'API complète : [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored), [docs/DETAILS.md](docs/DETAILS.md#using-it).
## Compiler depuis les sources
<details>
<summary>Windows, Linux et Mac depuis le dépôt</summary>
[Téléchargez Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) et décompressez-le, ou faites un `git clone`.
**Windows :** double-cliquez sur `START-HERE.bat`. **Linux :** lancez `./setup.sh` dans le dossier Strata.
**Mac (Apple Silicon, macOS 15 ou plus récent) :** installez une fois les Command Line Tools d'Apple avec `xcode-select --install`
(Xcode lui-même n'est pas nécessaire). Ouvrez le Terminal dans le dossier Strata et lancez `./setup.sh`. Le script compile
le moteur Mac sur votre Mac (environ 7 minutes), pose les mêmes questions, télécharge le modèle et le démarre. La fois
suivante, `./setup.sh` le démarre tout de suite. Quelle taille convient à votre Mac : [docs/MAC.md](docs/MAC.md).
L'installateur détecte votre carte et prépare le bon moteur. Il demande quel modèle et quelle taille, combien de contexte
(la quantité de texte que le modèle garde en tête) et s'il doit lire les images. Appuyez sur Entrée pour la réponse
recommandée. Puis il télécharge le modèle (environ 70 Go) et le démarre. Si le téléchargement s'arrête, relancez le script
et il reprend. Votre navigateur ouvre la page de Strata à l'adresse `http://127.0.0.1:8080`.
> Pendant le démarrage du modèle, votre PC peut être lent ou ne plus répondre pendant 1 à 3 minutes (le plus longtemps la
> première fois). Strata charge 35 à 55 Go dans votre RAM et en verrouille une partie pour la carte graphique. Patientez et
> ne fermez pas la fenêtre.
La fois suivante, relancez `START-HERE.bat` (ou `./setup.sh`). Il démarre tout de suite et ne télécharge rien deux fois.
Fermez sa fenêtre pour arrêter le modèle. `UPDATE.bat` (`./update.sh`) met Strata à jour sans le démarrer. Mise à jour,
Docker, plusieurs cartes, où vont les fichiers et toutes les options : [docs/INSTALL.md](docs/INSTALL.md).
Pour mesurer votre PC, lancez `BENCH.bat` (`./bench.sh`). Cela prend 5 à 10 minutes et produit un rapport que vous pouvez
partager ([comment](docs/COMMUNITY_BENCHMARKS.md#run-the-benchmark)).
**Facultatif : plus de vitesse sur une carte quand tout le modèle tient en RAM.** Deux réglages à activer soi-même, par
RubenTorney (#1904, #1905). Ils aident quand vous utilisez une seule carte graphique et que votre RAM contient tous les
experts, par exemple Q2_0 ou IQ3_XXS sur un PC de 64 Go. Ouvrez `strata-<model>.json` dans le dossier de données de Strata ([où](docs/INSTALL.md#where-things-are-stored)) et ajoutez :
- `"--adapt-async", "1"` à la liste `"args"`. Le cache des experts se met alors à jour sur un thread auxiliaire au lieu de
mettre la réponse en pause. Mesuré sur une RTX 5070 (Windows, Q2_0) : +4 à 5 % en décodage, +7 à 10 % sur le code et les
longs documents. Les réponses peuvent différer légèrement de celles du réglage par défaut, et restent identiques d'une exécution à l'autre.
- `"STRATA_PIN_BY_NEED": "1"` dans le bloc `"env"` (Windows). Windows ne peut verrouiller qu'environ la moitié de la RAM
pour la carte graphique, et ce réglage verrouille d'abord les experts dont la carte a le plus besoin. Mesuré sur la RTX 5070 : +4,4 % en lecture du prompt.
```json
"args": [ ..., "--adapt-async", "1" ],
"env": { "STRATA_PIN_BY_NEED": "1" }
```
Redémarrez Strata après la modification. Pour annuler, supprimez les deux entrées.
Suivez les invites pour télécharger et démarrer un modèle. [Guide du Terminal](docs/TERMINAL_LAUNCH.md).
</details>
## Comment ça marche ?
<p align="center">
<img src="docs/media/terminal-monitor.png" width="900" alt="Application Terminal de Strata affichant l'activité du modèle, la vitesse, la mémoire et les requêtes récentes">
<br><sub>Suivez votre modèle et votre matériel pendant qu'il tourne.</sub>
</p>
Les modèles de ce genre tournent d'habitude sur des serveurs qui disposent de centaines de gigaoctets de mémoire graphique.
Votre carte graphique en a 12 à 24. Strata fait tenir le modèle en répartissant le travail sur tout votre PC. Imaginez une
bibliothèque.
<a id="build-from-source"></a>
- Le modèle est une équipe de 24 576 petits spécialistes (les « experts »), comme 24 576 ouvrages de référence. Chaque mot n'en demande que 10.
- **Votre carte graphique est le bureau.** Il est petit et rapide. Les quelque 4 000 livres les plus utilisés y sont ouverts.
- **Votre RAM, ce sont les étagères de la salle de lecture.** Les 24 576 livres s'y trouvent. Quand un mot demande un livre
qui n'est pas sur le bureau, votre processeur va le chercher aussitôt sur l'étagère, pendant que la carte continue de travailler.
- **Votre SSD, ce sont les archives.** Il contient une table de consultation de 29 Go. Le modèle n'en lit que quelques lignes par
mot, elle n'a donc pas besoin de tenir en RAM.
[Compiler depuis les sources](docs/SOURCE_INSTALL.md) · [Docker](docs/DOCKER.md) · [Guide d'installation complet](docs/INSTALL.md)
<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>
<a id="what-you-need"></a>
Un petit assistant devine aussi les mots suivants. Le grand modèle les vérifie tous d'un coup et garde les bons. Vous
obtenez la même réponse, 1,6 à 1,8 fois plus tôt. Les textes longs sont lus par gros morceaux, jusqu'à 8 192 tokens à la
fois, à plus de 1 000 tokens par seconde.
## Matériel
<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>
| Matériel pris en charge | Guide |
| --- | --- |
| **NVIDIA GeForce RTX 20 / 30 / 40 / 50** | [Installation NVIDIA](docs/INSTALL.md#what-you-need) |
| **AMD Radeon RX 7700 / 7800 / 7900, RX 9060 / 9070, AI PRO R9700** | [Installation AMD](docs/AMD_HIP.md) |
| **AMD Radeon RX 6800 / 6900 · expérimental** | [Installation RX 6000](docs/AMD_HIP.md#rdna2-gfx1030) |
| **Intel Arc · Linux** | [Installation Intel](docs/INTEL_ARC.md) |
| **Apple Silicon · M1 ou plus récent** | [Installation sur Mac](docs/MAC.md) |
| **AMD Ryzen AI Max · Strix Halo** | [Installation Strix Halo](docs/STRIX_HALO.md) |
L'explication plus longue : [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Chaque élément avec ses chiffres :
[les détails](docs/DETAILS.md#how-it-works) et l'[article](docs/paper/Strata-Paper.pdf).
<a id="using-it"></a>
## Un problème ?
## Outils
- **Mon PC s'est figé au premier démarrage de Strata.** Il charge le modèle en RAM. Patientez et ne fermez pas la fenêtre.
Toujours figé après 10 minutes ? Redémarrez le PC, fermez les autres programmes et réessayez, ou choisissez une taille plus petite.
- **Ça s'est arrêté pendant le téléchargement ou l'installation.** Relancez l'application ou `strata launch`, ou relancez
`START-HERE.bat` (ou `./setup.sh`). Cela reprend là où ça s'était arrêté.
- **C'est très lent, le voyant du disque clignote sans arrêt, ou il affiche « the engine stopped unexpectedly ».** Votre PC
n'a pas assez de RAM libre. Fermez d'autres programmes (les navigateurs en consomment beaucoup) ou choisissez une taille plus petite, comme Q2_0 ou IQ2_XS.
- **Il indique qu'il n'y a pas de moteur pour ce PC.** Dans l'application, ouvrez **Settings, Engine** et appuyez sur **Update engine** (mettre à jour le moteur). Dans le Terminal, lancez `strata update`. `strata doctor` montre quels fichiers du moteur sont installés.
- **Il indique que le port 8080 est déjà utilisé.** Strata tourne déjà. Cherchez sa fenêtre.
**Claude Code · Codex CLI · OpenCode · oh-my-pi · pi.dev · Hermes Agent · OpenClaw**, et d'autres via les API compatibles OpenAI et Anthropic.
Autres problèmes et solutions : [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Toujours bloqué ? Ouvrez une
[issue](https://github.com/Niko1221/Strata/issues). Dans l'application, joignez le fichier obtenu par **Settings, Engine, Diagnostics, View logs, Save logs**. Dans le Terminal, collez la sortie de `strata doctor`. Si
vous avez lancé depuis les sources, joignez `strata-<model>.log` du dossier Strata. Pour un problème de sécurité,
signalez-le en privé : [SECURITY.md](SECURITY.md).
Lancez **`strata connect`** pour voir la configuration de votre outil. [Guide de connexion](docs/TERMINAL_LAUNCH.md#connect-your-apps-strata-connect).
## Remerciements et licence
<a id="guides"></a>
<a id="how-fast-is-it"></a>
<a id="how-does-it-work"></a>
<a id="something-went-wrong"></a>
Le modèle est [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next), de l'équipe Qwen. Il a été compressé par
[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5), agentionai (Gyro)
et Unsloth. Strata utilise des parties de [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Tous les
remerciements : [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata est open source sous [licence MIT](LICENSE).
Quelques parties et tous les modèles ont leurs propres licences ([lesquelles](docs/HOW_IT_WORKS.md#license)).
## Aide
## Contribuer
Vous avez trouvé un bug, ou vous voulez proposer une modification ou un benchmark ? Lisez d'abord [CONTRIBUTING.md](CONTRIBUTING.md).
Il indique ce qu'il faut inclure pour que d'autres puissent vérifier votre travail.
[Tous les guides](docs) · [Dépannage](docs/TROUBLESHOOTING.md) · [Signaler un bogue](https://github.com/Niko1221/Strata/issues/new/choose) ·
[Benchmarks](docs/COMMUNITY_BENCHMARKS.md) · [Comment ça marche](docs/HOW_IT_WORKS.md) · [Contribuer](CONTRIBUTING.md)
## Soutenir Strata
Strata est gratuit et sous licence MIT. Je le construis et je le teste sur le matériel que je peux obtenir. S'il tourne bien sur votre PC, un café m'aide
à continuer de tester de nouvelles cartes et à corriger ce que vous signalez. Aucune obligation. L'utiliser et signaler ce que vous constatez aide tout autant.
**Aidez à garder l'IA locale ouverte à tous.** Un parrainage sur GitHub ou un café aide à financer le développement et les tests sur davantage de matériel.
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a>
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor_on_GitHub-EA4AAA?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="GitHub Sponsors : soutenir Strata" height="48"></a>
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee : soutenir Strata" height="48"></a>
Vous souhaitez sponsoriser Strata ? Contactez-moi via [GitHub](https://github.com/Niko1221).
### Fidèles soutiens
Merci à toutes les personnes qui soutiennent Strata.
<!-- supporters:start (one line per public supporter, newest last; copy a line and change the GitHub name in all four places) -->
<a href="https://github.com/ryan4yin"><img src="https://github.com/ryan4yin.png?size=96" width="48" height="48" alt="ryan4yin" title="ryan4yin"></a>
<a href="https://github.com/themactep"><img src="https://github.com/themactep.png?size=96" width="48" height="48" alt="themactep" title="themactep"></a>
<a href="https://github.com/macintog"><img src="https://github.com/macintog.png?size=96" width="48" height="48" alt="macintog" title="macintog"></a>
<a href="https://github.com/bisonbet"><img src="https://github.com/bisonbet.png?size=96" width="48" height="48" alt="bisonbet" title="bisonbet"></a>
<a href="https://github.com/vizv"><img src="https://github.com/vizv.png?size=96" width="48" height="48" alt="vizv" title="vizv"></a>
<!-- supporters:end -->
[Licence MIT](LICENSE) · [Crédits et licences des modèles](docs/HOW_IT_WORKS.md#credits) · [Confidentialité](docs/SAFETY.md)
<a id="option-c-let-your-ai-set-it-up"></a>
<sub>Pour les outils d'IA et les lecteurs automatiques : <a href="AGENTS.md">référence technique</a> · <a href="docs/AI_SETUP.md">instructions d'installation</a>.</sub>
**Parrainages, partenariats matériel ou collaborations :** [[email protected]](mailto:[email protected]).
+135 -333
View File
@@ -1,400 +1,202 @@
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="apps/host/public/brand/strata-lockup-dark.svg">
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="380">
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="340">
</picture>
</p>
<p align="center"><a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <b>日本語</b> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português</a></p>
<h3 align="center">最先端のモデルを、手元のゲーミングPCで動かす</h3>
<p align="center">NVIDIA、AMD、Intel に対応 · Windows、Linux、Mac · 無料のオープンソース</p>
<h3 align="center">あなた自身のパソコンで、高性能な AI を。</h3>
<p align="center">チャット、コーディング、画像の処理を、すべてローカルで。</p>
<p align="center">
<a href="https://github.com/Niko1221/Strata/releases"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Latest release"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT license"></a>
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="GitHub stars"></a>
<a href="#モデル">モデル</a> · <a href="#インストール"><b>インストール</b></a> ·
<a href="#ハードウェア">ハードウェア</a> · <a href="#ツール">ツール</a> ·
<a href="#ヘルプ">ヘルプ</a> · <a href="#strata-を支援する">支援</a>
</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>ボクセルの塔の庭園。1回のプロンプトで生成し、RTX 5070 上の Strata で動かしています(IQ3_S、128K コンテキスト) ·
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">動画全体(49秒)</a></sub></p>
<p align="center">
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?label=download&color=0ab887" alt="Strata をダウンロード"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT ライセンス"></a>
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor-EA4AAA?logo=githubsponsors&logoColor=fff" alt="GitHub Sponsors で Strata を支援する"></a>
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy_me_a_coffee-FFDD00?logo=buymeacoffee&logoColor=000" alt="Buy Me a Coffee で Strata を支援する"></a>
</p>
Strata は **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** を動かします。1250億パラメータのモデルで、
ふつうはサーバーが必要ですが、Strata なら一般的なPCで使えます。チャット、コード作成、画像の読み取りができ、
お使いのアプリやコーディングエージェントとも連携します。チャットとファイルはPCの外に出ません。
<p align="center"><sub><a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <b>日本語</b> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português</a> · <a href="README.pl.md">Polski</a></sub></p>
**はじめての方へ。** まず[必要な環境](#必要な環境)を確認して、[アプリをインストール](#方法a-strata-アプリ)してください。
**無料でオープンソース。自分の環境で動かせます。** デスクトップで、またはお好みのコーディングツールと一緒に Strata を使えます。アカウントは不要です。
- **ゲーミングPCで動きます。** RAMとグラフィックスメモリの合計が32 GBあれば始められます。
- **お使いのツールと連携します。** OpenAI と Anthropic の API に対応しているので、Claude Code、Codex CLI、OpenCode などのエージェントから接続できます。
- **画像を読めます。** 初期設定でオンです。
- **無料です。** MIT ライセンス。アカウントもクラウドも不要です。
- **プライバシーを守ります。** テレメトリはありません。Strata が何をダウンロードするか、いつインターネットを使うか、ダウンロードをどう検証するかは [docs/SAFETY.md](docs/SAFETY.md) に書いてあります。
## モデル
バージョン 0.2.3 · エンジン 0.1.48
まず **ISTA-DASLab** をおすすめします。次に **Unsloth** などの提供元です。メモリに収まる互換モデルがある場合に選べます。
## 必要な環境
<details>
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> &nbsp;<b>Qwen3.8-Flash-Next</b></summary>
**RAMとグラフィックスメモリ(VRAM)の合計が、少なくとも32 GB必要です。** MacやRyzen AI Maxでは、ユニファイドメモリがその量にあたります。
メモリが多いほど、大きなモデルを使えたり、速く動いたりします。
- RAM 32 GBのPCに12〜16 GBのグラフィックスカードがあれば、Coder が動きます。
- RAM 64 GBなら、どのサイズも動きます。
| お使いのPC | 詳細 |
| 提供元 | 選択肢 |
| --- | --- |
| **NVIDIA** GeForce RTX 20、30、40、50 シリーズ(12 GB以上) | アプリまたはターミナル版をインストールしてください。 |
| **AMD** Radeon RX 6800 以降、Radeon AI PRO R9700(12 GB以上) | [AMD のカード](docs/AMD_HIP.md) |
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md)、[Intel エンジン](docs/INTEL.md) |
| **Apple Silicon の Mac**(M1 以降、macOS 15 以降) | [Mac](docs/MAC.md) |
| **AMD Ryzen AI Max(Strix Halo)** などのユニファイドメモリ搭載機 | [Strix Halo](docs/STRIX_HALO.md) |
| 古いカードやプロセッサ | [古いGPU](docs/OLDER_GPUS.md)、[古いCPU](docs/INSTALL.md#older-cpus-experimental) |
| **ISTA-DASLab · おすすめ** | Q2\_0, IQ2\_XS, IQ3\_XXS, IQ3\_S。[Coder: IQ1\_M](docs/MODELS.md#coder)(コーディング向け)。 |
| **Unsloth** | UD-IQ4\_XS, UD-Q4\_K\_XL, UD-Q5\_K\_XL, UD-Q6\_K\_XL, Q8\_0 |
| **Gyro · agentionai** | [Gyro-S, Gyro-M](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF)。メモリ使用量は少ないですが、動作は遅くなります。 |
| **その他の選択肢** | [Swift 1.5](docs/MODELS.md#swift-15), [NVFP4](docs/NVFP4.md), [OrcaRouter](docs/ORCA.md) |
そのほかに必要なもの:
[サイズ、必要なメモリ、互換性を比べる](docs/MODELS.md)。
- 約80 GBの空きディスク容量(SSDなら初回の起動がずっと速くなります)
- Windows 10 / 11、Linux、または macOS
- 最新のグラフィックスドライバー
</details>
同じメーカーのカードを2〜3枚使って、1つのモデルを動かせます([マルチGPU](docs/MULTI_GPU.md))。NVIDIA と AMD のカードにまたがってモデルを分けることはありません。
一覧は [docs/INSTALL.md](docs/INSTALL.md#what-you-need) にあります。
<details>
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> &nbsp;<b>Qwen3.6-35B-A3B</b></summary>
**Unsloth:** UD-Q4_K_XL, UD-Q6_K_XL, Q8_0。現在、このモデルで文書化されているパックです。
[モデルガイドと互換性](docs/QWEN36.md)。
</details>
<details>
<summary><img src="docs/media/models/deepseek.svg" width="22" alt="" align="middle"> &nbsp;<b>DeepSeekV4.1-Flash-1017</b></summary>
**デスクトップアプリ**と**ターミナル**で使えます。メモリに収まらない分は SSD から読み込むため、空きディスク容量がたくさん必要です。
| セット(ISTA-DAS) | ダウンロード | 必要な空き容量 |
| --- | ---: | ---: |
| 1.75 bpw | 239 GB | 約245 GB |
| 2.00 bpw | 257 GB | 約265 GB |
| 2.25 bpw | 274 GB | 約280 GB |
NVIDIA RTX 30・40・50 シリーズ(Windows と Linux)と、AMD Radeon AI PRO R9700(Linux)で動きます。セットアップがファイルと DeepSeek エンジンをダウンロードし、モデルを起動します。
```sh
strata launch --family deepseek41 --model ist-das-2.00bpw --plan
```
Strata 0.2.5 で 2.00 bpw のセットを使って測定しました。「出力」は回答の速さ、「プロンプト」は Strata が入力を読み込む速さです。
| ハードウェア | 出力 | プロンプト |
| --- | ---: | ---: |
| RTX 5070 12 GB, 64 GB RAM | 4〜6 トークン/秒 | 約 110 トークン/秒 (4K) |
| RTX 3060 12 GB, 46 GB RAM | 9〜11 トークン/秒 | 約 155 トークン/秒 (4K) |
| 1x Radeon AI PRO R9700, 251 GB RAM | 21〜25 トークン/秒 | 320〜360 トークン/秒 (8K) |
| 4x Radeon AI PRO R9700 | 74〜77 トークン/秒 | 400〜450 トークン/秒 (8K) |
[メモリ、ディスク、詳細](docs/DEEPSEEK41.md) · [リリースノート](https://github.com/Niko1221/Strata/releases)
</details>
<a id="the-other-sizes"></a>
どれが動くかわからない場合は、[お使いのパソコンに合うモデルを選ぶ](docs/MODELS.md#pick-by-memory)。
## インストール
3つの方法から1つ選んでください。どれも同じエンジンを起動し、同じモデルを使います。
<a id="option-a-the-strata-app"></a>
### 方法A: Strata アプリ
### デスクトップアプリ
いちばん簡単な方法です。エンジンはアプリに入っているので、インストールしてモデルを選べばチャットを始められます。お使いのOSとグラフィックスカードに合うファイルを
[リリースページ](https://github.com/Niko1221/Strata/releases/latest)からダウンロードしてください。どれか分からない場合は、表の下の説明を見てください。
**[Windows、Linux、Mac 用の Strata をダウンロード →](https://github.com/Niko1221/Strata/releases/latest)**
| OS | グラフィックスカード | ダウンロードするファイル |
| --- | --- | --- |
| **Windows 10 / 11** | NVIDIA RTX、ドライバー 580 以降 | [`Strata-0.2.3-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda13.exe) |
| | NVIDIA、古いカード、またはドライバーが 580 より古い場合 | [`Strata-0.2.3-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda12.exe) |
| | AMD Radeon RX 9000 シリーズまたは Radeon AI PRO R9700 | [`Strata-0.2.3-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip.exe) |
| | AMD Radeon RX 7000 シリーズまたは Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip-rdna3.exe) |
| **Linux** | NVIDIA RTX、ドライバー 580 以降 | [`Strata-0.2.3-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda13.AppImage) |
| | NVIDIA、古いカード、またはドライバーが 580 より古い場合 | [`Strata-0.2.3-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda12.AppImage) |
| | AMD Radeon RX 9000 シリーズまたは Radeon AI PRO R9700 | [`Strata-0.2.3-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx9070.AppImage) |
| | AMD Radeon RX 7000 シリーズ | [`Strata-0.2.3-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx7900.AppImage) |
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-strixhalo.AppImage) |
| **Mac**(Apple Silicon、macOS 15 以降) | 内蔵 | [`Strata-0.2.3-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-mac.dmg) |
アプリをインストールし、モデルを選んで、チャットを始めましょう。インストーラーはシステムごとに1つです。初回起動時に、アプリがお使いのグラフィックスカード用のエンジンをダウンロードします。[インストーラーのヘルプ](docs/INSTALL.md#the-app)。
**迷ったときは:** NVIDIA の場合、ドライバーがバージョン 580 以降なら `cuda13`、それより古ければ `cuda12` を選びます。`cuda12` は古いカードと古いドライバー向けです。
`hip` と `amd` のファイルは AMD のカード用です。それ以外の AMD カード(RX 6000)には、まだ用意されたアプリがありません。
[AMD のカード](docs/AMD_HIP.md)を見てください。
<sub>ファイル: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
そのあとの手順:
<p align="center">
<img src="docs/media/app-chat.png" width="900" alt="チャットとライブのコードプレビューを並べて表示した Strata デスクトップアプリ">
<br><sub>チャット、ファイル、ライブプレビューを1つのウィンドウで。</sub>
</p>
- **Windows:** `.exe` を実行します。ユーザー単位でインストールされ、管理者権限は不要です。「Windows によってPCが保護されました」と表示されたら、
**詳細情報**を選び、**実行**を選んでください(インストーラーにはまだコード署名がありません)。
- **Linux:** ファイルを実行可能にして(`chmod +x`)起動します。FUSE 2 が必要です。`sudo apt install libfuse2`(Ubuntu 22.04)
または `libfuse2t64`(Ubuntu 24.04 以降)を実行してください。
- **Mac:** `.dmg` を開き、Strata を「アプリケーション」にドラッグします。
<a id="option-b-the-terminal-one-line"></a>
最初の画面で、アプリがPCを調べて合うモデルをすすめます。インストールを押してください。モデルは大きく(約40〜70 GB)、中断しても続きからダウンロードできます。
ウィンドウを閉じると、Strata とエンジンが止まります。
### ターミナルアプリ
### 方法B: ターミナル(1行)
ターミナルのほうが好みですか? Strata をインストールして、**`strata launch`** を実行します。
ターミナル版は `strata` コマンドです。スクリプトやサーバー、AI エージェント向けです。
<details open>
<summary><b>インストールコマンド</b></summary>
Linux と Mac:
**Linux / Mac**
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
```
Windows(PowerShell):
**Windows PowerShell**
```powershell
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
```
スクリプトはPCを調べ、ターミナル版をダウンロードし、SHA-256 を検証して、ホームフォルダに展開します。管理者権限は不要です。Windows では、終わったら
新しいターミナルを開いてください。そのあと Strata を起動します。
Windows では、インストール後に新しいターミナルを開きます。そのあと:
```sh
strata launch
```
質問は1つずつ表示されます。モデル、コンテキスト、画像、接続を許可する相手の順です。そのあと、お使いのグラフィックスカード用のエンジン
(署名付きで、使う前にテスト起動します)とモデルをダウンロードして、サーバーを開始します。スクリプトや AI エージェントからは、フラグを使います。
```sh
strata launch --model q2_0 --yes --no-tui
```
実行前にインストールスクリプトを読みたい場合は、先に `curl -fsSL <address> -o install.sh` でダウンロードし、内容を確認してから
`sh install.sh` を実行してください。すべての画面とフラグは [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md) にあります。
### 方法C: AI にセットアップを任せる
Claude Code、Cursor、Codex、GitHub Copilot などのコーディングアシスタントに、次の文を貼り付けます。
```text
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
```
AI はグラフィックスカード、RAM、ディスクを調べ、合うモデルを選んで、インストールと起動をし、アプリの接続方法を教えてくれます。
AI ツールは [MCP サーバー](docs/MCP_SERVER.md)を通じて、Strata のインストール、起動、停止もできます。
自分でビルドしたい方は[ソースからビルド](#ソースからビルド)を見てください。
## モデル
アプリと `strata launch` は、メモリ量に合うモデルをすすめます。モデルは1つで、圧縮の度合いが違う複数のサイズがあります。
小さいサイズは速く、大きいサイズは少し賢くなります。
### メモリが少ないなら Gyro
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF)(agentionai 製)は、このモデルをいちばん小さく動かせる方法です。
| サイズ | 必要なメモリ(RAM + VRAM) | ダウンロード | 内容 |
| --- | ---: | ---: | --- |
| **Gyro-S** | 27.6 GB | 58.5 GB | ほかのサイズが入らないPC向け。品質は IQ2_XS より下がります。 |
| **Gyro-M** | 35 GB | 92.0 GB | 品質は IQ3_XXS 程度で、GPUメモリの使用量は約20%少なくなります。回答の書き出しは Q2_0 より、12 GBの RTX 3060 で16%、R9700 で21%遅くなります。 |
Gyro は NVIDIA と AMD のカード(AMD は実験的)、および Linux 上の Intel Arc カード(Arc A750 で確認)で動きます。0.2.2 からは Apple Silicon の Mac でも動きます(32 GB の Mac mini で Gyro-S は 9 トークン/秒、エキスパートは SSD から読み込み)。詳細と測定結果は
[docs/GYRO.md](docs/GYRO.md) にあります。`strata launch --model gyro-s` で起動します。
### ほかのサイズ
| メモリ(RAM + VRAM) | 選ぶモデル | 備考 |
| --- | --- | --- |
| **32 GB** | **Gyro-S**、またはコード向けの **[Coder](docs/MODELS.md#coder)** | Coder はRAM 32 GBが必要で、エキスパートを半分だけ保持するため、コード以外は弱くなります |
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS(39.2 GB)、または **Gyro-M**(35 GB) | Swift は答える前に考える時間が短くなります。Gyro-M は品質が IQ3_XXS に近いものの、回答の書き出しは Q2_0 より16〜21%遅くなります |
| **48 GB** | **IQ3_XXS**(47.0 GB) | 遅くなりますが、品質は上がります |
| **64 GB** | **IQ3_S**(54.8 GB)、オリジナルまたは Swift 1.5 | いちばん遅く、いちばん高品質です。公開されているテストでは完全版と同等です |
| **96 GB 以上** | Unsloth の **UD-IQ4_XS**(RAM 約72 GB)から **Q8_0**(約136 GB)まで、4〜8ビット | 完全版に近づきます。RAM が足りないと、Strata は一部を SSD から読むため、ずっと遅くなります |
表の行より RAM が少なくても動くことがあります。Strata は自動で[低RAMモード](docs/MODELS.md#a-big-graphics-card-and-little-ram)に切り替わります。
速度は落ち、グラフィックスカードが小さいとさらに大きく落ちます。
そのほかの選択肢:
- **[Swift 1.5](docs/MODELS.md#swift-15)** はファインチューン版で、答える前に考える時間が短くなります。品質と RAM の必要量はほぼ同じです。
- **[NVFP4](docs/NVFP4.md)** は NVIDIA カード向けの NVIDIA 製4ビット版です。すべてのエキスパートを RAM に置くには、約80 GBの RAM が必要です。
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** は、より小さい別のモデルです。画像を読め、複数のリクエストに同時に答えられます。
- **[OrcaRouter の Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** は手動で設定します。
すべてのサイズ、ダウンロード、どの構成で何が動くかは [docs/MODELS.md](docs/MODELS.md) にあります。
### DeepSeek V4.1-Flash(ターミナル版、0.2.4 から)
[DeepSeek V4.1-Flash](https://huggingface.co/ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF) は、これとは別のずっと大きなモデルです。下の 3 つのセットは ISTA-DASLab が作りました。Strata は、12 GB のグラフィックスカード 1 枚と 46 から 64 GB の RAM を持つ PC でこれを動かし、残りは SSD から読みます。NVIDIA RTX 30 から 50 シリーズのカードと Radeon AI PRO R9700 で動きます。専用のエンジンパッケージ `strata-dsv41` を使い、DeepSeek を選ぶと `strata launch` がダウンロードします。デスクトップアプリはまだ対応していません。
| セット | ダウンロード | 元のモデルとの品質差 | 出力速度 |
| --- | ---: | --- | --- |
| **IQ1_M**(1.75 ビット) | 239 GB | KL 0.217、パープレキシティ 3.80(+21%)、コード +8.9%、エージェント作業 +10.2%、通常の英語の散文では弱めです。ISTA-DASLab のモデルカードによると、基本モデルのタスク平均の 98.7% を保ちます(AIME25、GPQA-Diamond、LiveCodeBench v6)。 | 未測定 |
| **IQ2_XXS** (2.00 ビット) | 257 GB | KL 0.170、パープレキシティ 3.63(+15.5%)、コード +8.6%、エージェント作業 +5.3% | RTX 3060 で毎秒 6 から 9 トークン、RTX 5070 で約 5 |
| **IQ2_XS** (2.25 ビット) | 274 GB | KL 0.107、パープレキシティ 3.37(+7.3%)、コード +3.2%、エージェント作業 +2.2% | Radeon AI PRO R9700 1 枚で毎秒約 11 トークン |
| **IQ4_NL**(Engram テーブル、3 つ目のモデルではありません) | 111 GB、上の 3 つのダウンロードに含まれています | モデルが RAM または SSD から読む 4 ビットの Engram 参照テーブルです。3 つのセットはこの 1 つのファイル(`DeepSeek-V4.1-Flash-Engram-IQ4_NL.gguf`)を共有します。 | 該当なし |
Radeon AI PRO R9700 4 枚:毎秒約 31 トークン、DSpark ありで 55 から 69。
`strata launch --model ist-das-2.00bpw`(または `ist-das-1.75bpw`、`ist-das-2.25bpw`)で起動します。詳細、カードごとの速度、手動インストールは [docs/DEEPSEEK41.md](docs/DEEPSEEK41.md) にあります。
## どのくらい速い?
12 GBのグラフィックスカード(RTX 5070、Ryzen 5 7600、RAM 64 GB)での数値です。ほかのカードの結果は
[各モデルの速度](docs/MODELS.md#how-fast-is-each-size)、[コミュニティの結果](docs/COMMUNITY_BENCHMARKS.md)を見てください。
| モデル | 回答が出る速さ | プロンプトを読む速さ |
| --- | --- | --- |
| **Q2_0** | `████████████` 94 tokens/s | `████████████` 2,650 tokens/s |
| **IQ2_XS** | `██████████░░` 79 tokens/s | `█████████░░░` 2,090 tokens/s |
| **IQ3_XXS** | `████████░░░░` 62 tokens/s | `████████░░░░` 1,750 tokens/s |
| **IQ3_S** | `███████░░░░░` 53 tokens/s | `███████░░░░░` 1,620 tokens/s |
| **Coder** | `███████░░░░░` 55 tokens/s | `██████████░░` 2,180 tokens/s |
1トークンは英単語の約¾にあたるので、毎秒60トークンは読む速さより速くなります。「プロンプトを読む速さ」は、長い文書やコード、チャット履歴を Strata が取り込む速さです。
16 GBの AMD カード(RX 9070 XT、Ryzen 9 3900X、RAM 47 GB)での測定値:
| モデル | 回答が出る速さ | プロンプトを読む速さ |
| --- | --- | --- |
| **Q2_0** | `████████░░░░` 60 tokens/s | `█████░░░░░░░` 1,160 tokens/s |
| **IQ2_XS** | `███████░░░░░` 52 tokens/s | `█████░░░░░░░` 1,110 tokens/s |
| **Coder** | `██████░░░░░░` 44 tokens/s | `██████░░░░░░` 1,420 tokens/s |
バーは2つの表で同じ目盛りです。VRAM が多いカードほど速くなり、RTX 3090(24 GB)なら毎秒100〜140トークンほどになるはずです。各数値のエンジンのバージョンは
[DETAILS.md](docs/DETAILS.md#speed-measured) にあります。
## 使い方
### アプリで
アプリには、**チャット**、モデルとGPU・CPU・RAMの状態を見る**モニター**、**モデル**ページ、**設定**があります。チャットはPC上のフォルダで作業できます。
**手動**モードでは、ファイルを変更したりコマンドを実行したりする前に確認します。`/loop 10m <やること>` と入力すると、チャットを開いている間、メッセージを10分ごとに繰り返し送ります。
チャットは DuckDuckGo(または Bing)でウェブ検索ができ、アカウントは不要です。**設定、ツール**で検索キーを追加することもできます。
検索は、PCの外に何かを送る唯一の機能で、送られるのは検索語です。
2つのモデルを動かすには、**複数のモデルを実行**をオンにして、それぞれのモデルに同じメーカーのカードを1枚ずつ割り当てます。
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="The Strata app: a chat where the agent built a small voxel pagoda garden, with the page open in the preview on the right"><br>
<sub>Strata アプリでのチャット。エージェントが小さなボクセルの塔の庭を 1 つの HTML ファイルとして書き、そのページが会話の横のプレビューで開きます</sub></p>
### ターミナルで
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
<sub>モデルが返信を書いている間の <code>strata monitor</code>。速度、GPU、メモリ、最近のリクエストが表示されます</sub></p>
```sh
strata launch # ガイド付きの画面: モデル、コンテキスト、画像、アクセス、そして開始
strata launch --model q2_0 --yes --no-tui # 質問なし: 開始し、アドレスを表示して戻る
strata launch --plan --model q2_0 # 実行内容を表示するだけで、何も変更しない
strata launch --status # 今、何が動いているか
strata launch --stop # 停止する
strata monitor --url http://127.0.0.1:8080 # 速度、GPU、メモリ、リクエストを見る
strata update --check # 新しいエンジンがあるか確認(strata update でインストール)
strata doctor # インストール済みのエンジンファイルを調べる
```
`strata --help` ですべてのコマンドを確認できます。起動とサーバーの稼働では、何もダウンロードしません。ダウンロードするのは、インストール、
`strata launch`(モデルを選んだとき)、`strata update` だけです。スマートフォンや別のPCから Strata に接続するには、
`strata launch --access lan` を使います。API キーが作られ、キーがない状態ではネットワーク上で起動しません。`--open` を付けない限り、ブラウザは開きません。
ガイド: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md)。クライアント全体: [docs/TERMINAL.md](docs/TERMINAL.md)。
### AI エージェントと使う
エージェントは、方法Bの1行コマンドで Strata をインストールし、
`strata launch --model NAME --yes --no-tui` を実行します。手順、フラグ、終了コードは [docs/AI_SETUP.md](docs/AI_SETUP.md) に、
リポジトリのルールは [AGENTS.md](AGENTS.md) にあります。
### ほかのアプリをつなぐ
| アプリの種類 | ベース URL |
| --- | --- |
| OpenAI 互換(ほとんどのアプリとエージェント) | `http://127.0.0.1:8080/v1`(API キーもモデル名も何でも可) |
| Anthropic API(Claude Code など) | `http://127.0.0.1:8080`、`/v1` は付けません。これらのアプリは `/v1/messages` を自分で付けます。Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
| OpenAI Responses API(Codex CLI) | `http://127.0.0.1:8080/v1/responses`([設定方法](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
`strata connect` は、OpenCode、Codex CLI、Claude Code、oh-my-pi、pi、Hermes Agent、OpenClaw の正確な設定を表示します。
`strata connect opencode --write` は、確認したうえでファイルを書き込み、バックアップを残します。
### 画像
画像を読めるモデルでは、画像は初期設定でオンです。チャットに画像を添付または貼り付けるか、アプリから送ってください。
グラフィックスカードが24 GB未満のときは、画像リーダーがプロセッサで動きます(画像1枚につき数秒、カードのメモリは使いません)。24 GB以上のときはグラフィックスカードで動きます。
このルールは NVIDIA と AMD で同じです。画像はモデルごとにオフにでき、`--vision off`、`--vision gpu`、`--vision cpu` でリーダーを選べます。
このルールの根拠となる数値です。Radeon AI PRO R9700 では、カードのリーダーは260トークンの画像に57 ms、プロセッサのリーダーは3.1秒かかりました。
16 GBのカード(Tesla P100 と RTX A4000、Linux)では、カードのリーダーを使うとテキストの速度が3〜5%下がりました。
本来モデルが使う約1.4 GBをリーダーが占めるためです。Mac ではメモリ量にかかわらず GPU が読み取ります(32 GB の M4 で260トークンの画像に0.45秒、共有メモリを約1.2 GB使います)。プロセッサでは1枚あたり約50秒かかるため、`--vision cpu` を指定したときだけ使います。
### 知っておくと便利なこと
- 思考のレベル(**off、low、medium、high**)は、チャットのメニューか、お使いのアプリの「reasoning effort」で選びます。off がいちばん速くなります。
- Strata は初期設定では、一度に1つのリクエストに答えます。同時に複数処理するには、`strata launch` で `--parallel N` を使うか、設定に
`"parallel": 2` を書きます([BATCHING.md](docs/BATCHING.md))。12 GBのカードでは、1つずつの回答が遅くなります。
- Strata はチャットの最初のメッセージを全文読みます。30,000トークンあたり約1分かかります。続きのメッセージは数秒で始まります。
- チャットの保存場所と API 全体: [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored)、[docs/DETAILS.md](docs/DETAILS.md#using-it)。
## ソースからビルド
<details>
<summary>リポジトリから Windows、Linux、Mac 用にビルドする</summary>
[Strata をダウンロード](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip)して展開するか、`git clone` します。
**Windows:** `START-HERE.bat` をダブルクリックします。**Linux:** Strata フォルダで `./setup.sh` を実行します。
**Mac(Apple Silicon、macOS 15 以降):** 最初に一度、`xcode-select --install` で Apple のコマンドラインツールをインストールします
(Xcode 本体は不要です)。Strata フォルダでターミナルを開き、`./setup.sh` を実行します。Mac 用エンジンをお使いの Mac 上でビルドし
(約7分)、同じ質問をして、モデルをダウンロードし、起動します。2回目以降は `./setup.sh` ですぐに起動します。
お使いの Mac に合うサイズは [docs/MAC.md](docs/MAC.md) を見てください。
インストーラーはカードを見つけて、合うエンジンを設定します。どのモデルとサイズにするか、コンテキストをどれだけにするか(モデルが一度に覚えておけるテキストの量)、
画像を読ませるかを聞かれます。おすすめの答えでよければ Enter を押します。そのあとモデル(約70 GB)をダウンロードして起動します。
ダウンロードが止まったら、もう一度実行すると続きから再開します。ブラウザで `http://127.0.0.1:8080` の Strata ページが開きます。
> モデルの起動中は、PCが1〜3分ほど(初回がいちばん長い)遅くなったり、応答しなくなったりすることがあります。Strata は35〜55 GBをRAMに読み込み、
> その一部をグラフィックスカード用に固定するためです。ウィンドウを閉じずに待ってください。
次回からは `START-HERE.bat`(または `./setup.sh`)をもう一度実行するだけです。すぐに起動し、同じものを2回ダウンロードすることはありません。ウィンドウを閉じるとモデルが止まります。
`UPDATE.bat`(`./update.sh`)は、起動せずに Strata を更新します。更新、Docker、複数のカード、ファイルの保存先、すべてのオプションは
[docs/INSTALL.md](docs/INSTALL.md) にあります。
PCの性能を測るには、`BENCH.bat`(`./bench.sh`)を実行します。5〜10分かかり、共有できるレポートが作られます
([方法](docs/COMMUNITY_BENCHMARKS.md#run-the-benchmark))。
**オプション: モデル全体がRAMに入るとき、1枚のカードをさらに速くする。** RubenTorney による、自分でオンにする2つの設定です(#1904、#1905)。
グラフィックスカードが1枚で、RAM がすべてのエキスパートを保持できるときに効果があります。たとえば64 GBのPCでの Q2_0 や IQ3_XXS です。
Strata のデータフォルダにある `strata-<model>.json`([場所](docs/INSTALL.md#where-things-are-stored))を開いて、次を追加します。
- `"args"` のリストに `"--adapt-async", "1"` を追加します。エキスパートキャッシュが、回答を止めずに補助スレッドで更新されます。
RTX 5070(Windows、Q2_0)での測定: デコードが4〜5%、コードや長い文書では7〜10%速くなりました。回答は初期設定と少し違うことがありますが、実行ごとの結果は変わりません。
- `"env"` ブロックに `"STRATA_PIN_BY_NEED": "1"` を追加します(Windows)。Windows ではグラフィックスカード用に固定できる RAM が半分ほどしかありません。
この設定は、カードが最初に必要とするエキスパートを優先して固定します。RTX 5070 での測定: プロンプトの読み取りが4.4%速くなりました。
```json
"args": [ ..., "--adapt-async", "1" ],
"env": { "STRATA_PIN_BY_NEED": "1" }
```
変更したら Strata を再起動してください。元に戻すには、2つの項目を削除します。
画面の案内に従って、モデルをダウンロードして起動します。[ターミナルガイド](docs/TERMINAL_LAUNCH.md)。
</details>
## 仕組み
<p align="center">
<img src="docs/media/terminal-monitor.png" width="900" alt="モデルの動作、速度、メモリ、最近のリクエストを表示する Strata ターミナルアプリ">
<br><sub>動作中のモデルとハードウェアの状態を確認できます。</sub>
</p>
このようなモデルは、ふつう数百GBのグラフィックスメモリを持つサーバーで動きます。お使いのグラフィックスカードは12〜24 GBです。Strata は、PC全体で作業を分担して、モデルを収めます。
図書館にたとえてみます。
<a id="build-from-source"></a>
- モデルは、24,576人の小さな専門家(「エキスパート」)からなるチームです。24,576冊の参考書のようなものです。1つの単語に必要なのは、そのうち10冊だけです。
- **グラフィックスカードは机です。** 小さくて速く、最もよく使う約4,000冊が開いて置いてあります。
- **RAM は閲覧室の書棚です。** 24,576冊すべてがここにあります。単語に必要な本が机になければ、プロセッサがすぐに書棚から読み、その間もカードは作業を続けます。
- **SSD は書庫です。** 29 GBの参照テーブルがしまってあります。モデルは単語ごとにその数行だけを読むので、RAM に置いておく必要はありません。
[ソースからビルド](docs/SOURCE_INSTALL.md) · [Docker](docs/DOCKER.md) · [インストールガイド(全文)](docs/INSTALL.md)
<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>
<a id="what-you-need"></a>
小さな助手が、次の数単語も予想します。大きなモデルがそれをまとめて確認し、正しいものだけを残します。答えは同じで、1.6〜1.8倍早く返ります。
長い文章は、一度に最大8,192トークンの大きな塊で、毎秒1,000トークンを超える速さで読みます。
## ハードウェア
<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>
| 対応ハードウェア | ガイド |
| --- | --- |
| **NVIDIA GeForce RTX 20 / 30 / 40 / 50 シリーズ** | [NVIDIA のセットアップ](docs/INSTALL.md#what-you-need) |
| **AMD Radeon RX 7700 / 7800 / 7900、RX 9060 / 9070、AI PRO R9700** | [AMD のセットアップ](docs/AMD_HIP.md) |
| **AMD Radeon RX 6800 / 6900 · 試験的** | [RX 6000 のセットアップ](docs/AMD_HIP.md#rdna2-gfx1030) |
| **Intel Arc · Linux** | [Intel のセットアップ](docs/INTEL_ARC.md) |
| **Apple Silicon · M1 以降** | [Mac のセットアップ](docs/MAC.md) |
| **AMD Ryzen AI Max · Strix Halo** | [Strix Halo のセットアップ](docs/STRIX_HALO.md) |
詳しい説明は [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md) にあります。各部分と数値は
[詳細](docs/DETAILS.md#how-it-works)と[論文](docs/paper/Strata-Paper.pdf)を見てください。
<a id="using-it"></a>
## うまくいかないとき
## ツール
- **Strata を初めて起動したときに、PCが固まりました。** モデルを RAM に読み込んでいます。ウィンドウを閉じずに待ってください。10分たっても固まったままなら、
PCを再起動し、ほかのプログラムを閉じてからもう一度試すか、小さいサイズを選んでください。
- **ダウンロードやインストールの途中で止まりました。** アプリか `strata launch` をもう一度起動するか、`START-HERE.bat`(または
`./setup.sh`)をもう一度実行してください。止まったところから再開します。
- **とても遅い、ディスクのランプが点滅し続ける、または「エンジンが予期せず停止しました」と表示される。** PCの空き RAM が足りません。
ほかのプログラムを閉じるか(ブラウザはメモリを多く使います)、Q2_0 や IQ2_XS のような小さいサイズを選んでください。
- **このPCに合うエンジンがないと表示される。** アプリでは、**設定、エンジン**を開いて**エンジンを更新**を押します。ターミナルでは
`strata update` を実行します。`strata doctor` で、どのエンジンファイルがインストールされているか分かります。
- **ポート 8080 はすでに使われていると表示される。** Strata がすでに動いています。ウィンドウを探してください。
**Claude Code · Codex CLI · OpenCode · oh-my-pi · pi.dev · Hermes Agent · OpenClaw**。OpenAI 互換と Anthropic 互換の API を通じて、ほかのツールも使えます。
そのほかの問題と対処法は [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) にあります。それでも解決しない場合は、
[issue](https://github.com/Niko1221/Strata/issues) を作成してください。アプリでは、**設定、エンジン、診断、ログを見る、ログを保存**で作られるファイルを添付します。
ターミナルでは、`strata doctor` の出力を貼り付けます。ソースから動かしている場合は、Strata フォルダの `strata-<model>.log` を添付してください。
セキュリティ上の問題は、非公開で報告してください: [SECURITY.md](SECURITY.md)。
**`strata connect`** を実行すると、お使いのツールの設定方法が表示されます。[接続ガイド](docs/TERMINAL_LAUNCH.md#connect-your-apps-strata-connect)。
## クレジットとライセンス
<a id="guides"></a>
<a id="how-fast-is-it"></a>
<a id="how-does-it-work"></a>
<a id="something-went-wrong"></a>
モデルは Qwen チームの [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) です。圧縮は
[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF)、UkisAI(Swift 1.5)、agentionai(Gyro)、
Unsloth が行いました。Strata は [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp) の一部を使っています。すべてのクレジットは
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits) にあります。Strata は [MIT ライセンス](LICENSE)のオープンソースです。一部のコンポーネントと
すべてのモデルには、それぞれのライセンスがあります([該当するもの](docs/HOW_IT_WORKS.md#license))。
## ヘルプ
## コントリビューション
[すべてのガイド](docs) · [トラブルシューティング](docs/TROUBLESHOOTING.md) · [不具合を報告](https://github.com/Niko1221/Strata/issues/new/choose) ·
[ベンチマーク](docs/COMMUNITY_BENCHMARKS.md) · [仕組み](docs/HOW_IT_WORKS.md) · [貢献する](CONTRIBUTING.md)
バグを見つけた、変更やベンチマークを送りたい、という方は、先に [CONTRIBUTING.md](CONTRIBUTING.md) を読んでください。
ほかの人があなたの成果を確認できるように、何を書くべきかが説明してあります。
## Strata を支援する
## Strata を応援する
**ローカル AI をみんなに開かれたものに。** GitHub でのスポンサーやコーヒー1杯が、開発と、より多くのハードウェアでのテストの支えになります。
Strata は無料で、MIT ライセンスです。私が入手できるハードウェアで開発とテストをしています。お使いのPCで快適に動いたなら、コーヒー1杯分の支援は、
新しいカードのテストと、報告いただいた不具合の修正を続ける助けになります。無理のない範囲で大丈夫です。使って、気づいたことを報告してもらえるだけでも同じように助かります。
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor_on_GitHub-EA4AAA?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="GitHub Sponsors: Strata を支援する" height="48"></a>
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee: Strata を支援する" height="48"></a>
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a>
### いつも支えてくださる方々
Strata のスポンサーに関心がある方は、[GitHub](https://github.com/Niko1221) から連絡してください。
Strata を支えてくださるすべての方に感謝します。
<!-- supporters:start (one line per public supporter, newest last; copy a line and change the GitHub name in all four places) -->
<a href="https://github.com/ryan4yin"><img src="https://github.com/ryan4yin.png?size=96" width="48" height="48" alt="ryan4yin" title="ryan4yin"></a>
<a href="https://github.com/themactep"><img src="https://github.com/themactep.png?size=96" width="48" height="48" alt="themactep" title="themactep"></a>
<a href="https://github.com/macintog"><img src="https://github.com/macintog.png?size=96" width="48" height="48" alt="macintog" title="macintog"></a>
<a href="https://github.com/bisonbet"><img src="https://github.com/bisonbet.png?size=96" width="48" height="48" alt="bisonbet" title="bisonbet"></a>
<a href="https://github.com/vizv"><img src="https://github.com/vizv.png?size=96" width="48" height="48" alt="vizv" title="vizv"></a>
<!-- supporters:end -->
[MIT ライセンス](LICENSE) · [クレジットとモデルのライセンス](docs/HOW_IT_WORKS.md#credits) · [プライバシー](docs/SAFETY.md)
<a id="option-c-let-your-ai-set-it-up"></a>
<sub>AI ツールと自動リーダー向け: <a href="AGENTS.md">技術リファレンス</a> · <a href="docs/AI_SETUP.md">セットアップ手順</a></sub>
**スポンサー、ハードウェア提携、共同開発のご相談:** [[email protected]](mailto:[email protected]).
+142 -347
View File
@@ -1,414 +1,209 @@
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="apps/host/public/brand/strata-lockup-dark.svg">
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="380">
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="340">
</picture>
</p>
<p align="center"><b>English</b> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português</a></p>
<h3 align="center">Run a state-of-the-art model on your own gaming PC</h3>
<p align="center">Support for NVIDIA, AMD, Intel · Windows, Linux, Mac · free and open source</p>
<h3 align="center">Run powerful AI on your own computer.</h3>
<p align="center">Chat, code and work with images, locally.</p>
<p align="center">
<a href="https://github.com/Niko1221/Strata/releases"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Latest release"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT license"></a>
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="GitHub stars"></a>
<a href="#models">Models</a> · <a href="#install"><b>Install</b></a> ·
<a href="#hardware">Hardware</a> · <a href="#tools">Tools</a> ·
<a href="#help">Help</a> · <a href="#support-strata">Support</a>
</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>
<p align="center">
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?label=download&color=0ab887" alt="Download Strata"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT license"></a>
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor-EA4AAA?logo=githubsponsors&logoColor=fff" alt="Sponsor Strata on GitHub Sponsors"></a>
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy_me_a_coffee-FFDD00?logo=buymeacoffee&logoColor=000" alt="Support Strata on Buy Me a Coffee"></a>
</p>
Strata runs **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**, a 125-billion-parameter model that
normally needs a server, on a normal PC. It chats, writes code, reads pictures and works with your apps and coding
agents. Your chats and files stay on your PC.
<p align="center"><sub><b>English</b> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português</a> · <a href="README.pl.md">Polski</a></sub></p>
**New here?** Check [what you need](#what-you-need), then [install the app](#option-a-the-strata-app).
**Free, open source, and yours to run.** Use Strata on your desktop or with your favorite coding tools. No account needed.
- **Runs on a gaming PC.** 32 GB of RAM and graphics memory together is enough to start.
- **Works with your tools.** It speaks the OpenAI and Anthropic APIs, so Claude Code, Codex CLI, OpenCode and other agents connect to it.
- **Reads pictures.** They are on by default.
- **Free.** MIT license, no account, no cloud.
- **Private.** No telemetry. What Strata downloads, when it uses the internet and how it checks downloads is in [docs/SAFETY.md](docs/SAFETY.md).
## Models
Version 0.2.3 · engine 0.1.48
We recommend **ISTA-DASLab first**, then **Unsloth** and other providers, where a compatible model fits.
## What you need
<details>
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> &nbsp;<b>Qwen3.8-Flash-Next</b></summary>
**You need at least 32 GB of RAM and graphics memory (VRAM) together.** On a Mac or a Ryzen AI Max, that is the unified
memory. More memory gives a bigger model or a faster one.
- A 32 GB PC with a 12-16 GB card runs the Coder.
- 64 GB of RAM runs every size.
| Your computer | Details |
| Provider | Choices |
| --- | --- |
| **NVIDIA** GeForce RTX 20, 30, 40, 50 series (12 GB or more) | Install the app or the Terminal. |
| **AMD** Radeon RX 6800 and newer, Radeon AI PRO R9700 (12 GB or more) | [AMD cards](docs/AMD_HIP.md) |
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md), [Intel engine](docs/INTEL.md) |
| **Apple Silicon Mac** (M1 or newer, macOS 15 or newer) | [Mac](docs/MAC.md) |
| **AMD Ryzen AI Max (Strix Halo)** and other unified-memory systems | [Strix Halo](docs/STRIX_HALO.md) |
| Older cards and processors | [Older GPUs](docs/OLDER_GPUS.md), [Older CPUs](docs/INSTALL.md#older-cpus-experimental) |
| **ISTA-DASLab · recommended** | Q2\_0, IQ2\_XS, IQ3\_XXS, IQ3\_S. [Coder: IQ1\_M](docs/MODELS.md#coder) for coding. |
| **Unsloth** | UD-IQ4\_XS, UD-Q4\_K\_XL, UD-Q5\_K\_XL, UD-Q6\_K\_XL, Q8\_0 |
| **Gyro · agentionai** | [Gyro-S, Gyro-M](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF). Lower memory use but slower. |
| **Other options** | [Swift 1.5](docs/MODELS.md#swift-15), [NVFP4](docs/NVFP4.md), [OrcaRouter](docs/ORCA.md) |
Also needed:
[Compare sizes, memory needs and compatibility](docs/MODELS.md).
- about 80 GB of free disk (an SSD makes the first start much faster)
- Windows 10 / 11, Linux or macOS
- a current graphics driver
</details>
Two or three cards of the same make can share a model ([multi-GPU](docs/MULTI_GPU.md)). Strata never splits a model across
NVIDIA and AMD cards. Full list: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
<details>
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> &nbsp;<b>Qwen3.6-35B-A3B</b></summary>
**Unsloth:** UD-Q4_K_XL, UD-Q6_K_XL, Q8_0. These are the currently documented packs for this model.
[Model guide and compatibility](docs/QWEN36.md).
</details>
<details>
<summary><img src="docs/media/models/deepseek.svg" width="22" alt="" align="middle"> &nbsp;<b>DeepSeekV4.1-Flash-1017</b></summary>
In the **desktop app** and the **Terminal**. What does not fit in memory is read from your SSD, so it needs a lot of free
disk space.
| Set (ISTA-DAS) | Download | Free disk needed |
| --- | ---: | ---: |
| 1.75 bpw | 239 GB | about 245 GB |
| 2.00 bpw | 257 GB | about 265 GB |
| 2.25 bpw | 274 GB | about 280 GB |
| ISTA-DAS Coder (half of the experts removed) | 191 GB | about 197 GB |
Runs on NVIDIA RTX 30, 40 and 50 series (Windows and Linux), AMD Radeon AI PRO R9700 (Linux), AMD Ryzen AI Max — Strix Halo
(Windows), and Apple Silicon Macs with 64 GB or more, text only ([Mac](docs/DEEPSEEK_MAC.md)). Setup downloads the files and the DeepSeek engine, then starts the model.
```sh
strata launch --family deepseek41 --model ist-das-2.00bpw --plan
```
Measured with the 2.00 bpw set on Strata 0.2.5 (Strix Halo: 0.2.7). Output is the speed of the answer, prompt is how fast Strata reads your input.
| Hardware | Output | Prompt |
| --- | ---: | ---: |
| RTX 5070 12 GB, 64 GB RAM | 4 to 6 tokens/s | about 110 tokens/s (4K) |
| RTX 3060 12 GB, 46 GB RAM | 9 to 11 tokens/s | about 155 tokens/s (4K) |
| 1x Radeon AI PRO R9700, 251 GB RAM | 21 to 25 tokens/s | 320 to 360 tokens/s (8K) |
| 4x Radeon AI PRO R9700 | 74 to 77 tokens/s | 400 to 450 tokens/s (8K) |
| Ryzen AI Max+ 395 (Strix Halo), 128 GB unified | 19 to 21 tokens/s (code), 11 (chat) | 87 tokens/s (4K), 125 tokens/s (8K) |
[Memory, disk and details](docs/DEEPSEEK41.md) · [Release notes](https://github.com/Niko1221/Strata/releases)
</details>
<a id="the-other-sizes"></a>
Not sure what fits? [Choose a model for your computer](docs/MODELS.md#pick-by-memory).
## Install
Pick one option. They all start the same engine and use the same models.
<a id="option-a-the-strata-app"></a>
### Option A: the Strata app
### Desktop app
The easiest way. The app has the engine inside: install it, pick a model, chat. Download the file for your system and
graphics card from the [releases page](https://github.com/Niko1221/Strata/releases/latest). Not sure which one? See the
line under the table.
**[Download Strata for Windows, Linux or Mac →](https://github.com/Niko1221/Strata/releases/latest)**
| System | Your graphics card | File to download |
| --- | --- | --- |
| **Windows 10 / 11** | NVIDIA RTX, driver 580 or newer | [`Strata-0.2.3-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda13.exe) |
| | NVIDIA, older card or driver older than 580 | [`Strata-0.2.3-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda12.exe) |
| | AMD Radeon RX 9000 series or Radeon AI PRO R9700 | [`Strata-0.2.3-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip.exe) |
| | AMD Radeon RX 7000 series or Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip-rdna3.exe) |
| **Linux** | NVIDIA RTX, driver 580 or newer | [`Strata-0.2.3-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda13.AppImage) |
| | NVIDIA, older card or driver older than 580 | [`Strata-0.2.3-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda12.AppImage) |
| | AMD Radeon RX 9000 series or Radeon AI PRO R9700 | [`Strata-0.2.3-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx9070.AppImage) |
| | AMD Radeon RX 7000 series | [`Strata-0.2.3-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx7900.AppImage) |
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-strixhalo.AppImage) |
| **Mac** (Apple Silicon, macOS 15 or newer) | built in | [`Strata-0.2.3-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-mac.dmg) |
Install the app, choose a model, and start chatting. There is one installer for each system. On first start the app
downloads the engine for your graphics card. [Help with the installer](docs/INSTALL.md#the-app).
**If you are unsure:** for NVIDIA, take `cuda13` if your driver is version 580 or newer, otherwise `cuda12`. `cuda12` is
for older cards and older drivers. The `hip` and `amd` files are for AMD cards. RX 6000 cards have no installer of
their own: install `hip-rdna3` (Windows) or `amd-rx7900` (Linux), and on a PC with an RX 6000 card it fetches the RX 6000 engine by itself
(also `strata update`). That engine is built but not yet tested on a real RX 6000 card: see [AMD cards](docs/AMD_HIP.md).
<sub>Files: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
Then:
<p align="center">
<img src="docs/media/app-chat.png" width="900" alt="Strata desktop app with a chat and live code preview side by side">
<br><sub>Chat, files and a live preview in one window.</sub>
</p>
- **Windows:** run the `.exe`. It installs for your user and needs no administrator rights. If Windows says "Windows
protected your PC", choose **More info**, then **Run anyway** (the installer is not code-signed yet).
- **Linux:** make the file runnable (`chmod +x`) and start it. It needs FUSE 2: `sudo apt install libfuse2` (Ubuntu 22.04)
or `libfuse2t64` (Ubuntu 24.04 and newer).
- **Mac:** open the `.dmg` and drag Strata onto Applications.
<a id="option-b-the-terminal-one-line"></a>
On the first screen the app reads your PC and recommends a model that fits. Press install. The model is a big download
(about 40-70 GB) and continues where it stopped if it is interrupted. Closing the window stops Strata and the engine.
### Terminal app
### Option B: the Terminal (one line)
Prefer the terminal? Install Strata, then run **`strata launch`**.
The Terminal is the `strata` command, for scripts, servers and AI agents.
<details open>
<summary><b>Install commands</b></summary>
Linux and Mac:
**Linux / Mac**
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
```
Windows (PowerShell):
**Windows PowerShell**
```powershell
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
```
The script checks your PC, downloads the Terminal, verifies its SHA-256 and unpacks it into your home folder. It needs no
administrator rights. On Windows, open a new terminal window afterwards. Then start Strata:
On Windows, open a new terminal afterwards. Then:
```sh
strata launch
```
It asks one question at a time: model, context, pictures, who may connect. Then it downloads the engine for your graphics
card (signed, and test-started before use) and the model, and starts serving. For scripts and AI agents, use flags:
```sh
strata launch --model q2_0 --yes --no-tui
```
To read the install script before it runs, download it first with `curl -fsSL <address> -o install.sh`, read it, then run
`sh install.sh`. All screens and flags: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
### Option C: let your AI set it up
Paste this into Claude Code, Cursor, Codex, GitHub Copilot or another coding assistant:
```text
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
```
It checks your graphics card, RAM and disk, picks the model that fits, installs and starts it, and tells you how to
connect your apps. AI tools can also install, start and stop Strata through its [MCP server](docs/MCP_SERVER.md).
Prefer to build it yourself? See [Build from source](#build-from-source).
## Models
The app and `strata launch` recommend a model for your memory. It is one model in several sizes, compressed more or less.
Smaller sizes are faster. Larger sizes are a bit smarter.
### Low on memory? Try Gyro
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) (by agentionai) is the smallest way to run this model.
| Size | Memory it needs (RAM + VRAM) | Download | What it is |
| --- | ---: | ---: | --- |
| **Gyro-S** | 27.6 GB | 58.5 GB | For PCs that fit nothing else. Quality is lower than IQ2_XS. |
| **Gyro-M** | 35 GB | 92.0 GB | About IQ3_XXS quality with about 20% less GPU memory. It writes answers 16% (12 GB RTX 3060) to 21% (R9700) slower than Q2_0. |
Gyro runs on NVIDIA and AMD cards (AMD is experimental) on Intel Arc cards on Linux (tested on an Arc A750) and, from 0.2.2, on Apple Silicon Macs (Gyro-S on a 32 GB Mac mini: 9 tok/s, streaming from the SSD; [docs/MAC.md](docs/MAC.md#gyro-s-and-gyro-m-agentionais-trellis-experts)). Details and measurements:
[docs/GYRO.md](docs/GYRO.md). Start it with `strata launch --model gyro-s`.
### The other sizes
The tiers count your RAM plus your VRAM, less 5 GB that Windows and the system keep for themselves. Every other size stays a choice.
| Your memory (RAM + VRAM) | Take | Notes |
| --- | --- | --- |
| **32 GB** | **Gyro-S**, or the **[Coder](docs/MODELS.md#coder)** for code | The Coder needs 32 GB of RAM and keeps half of the experts, so it is weaker outside code |
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39.2 GB), or **Gyro-M** (35 GB) | Swift thinks for a shorter time before it answers. Gyro-M is close to IQ3_XXS in quality, but it writes answers 16-21% slower than Q2_0 |
| **48 GB** | **IQ3_XXS** (47.0 GB) | Slower, better |
| **64 GB** | **IQ3_S** (54.8 GB), the original or Swift 1.5 | The slowest, the best: it matches the full model on the published tests |
| **96 GB or more** | Unsloth's **UD-IQ4_XS** (about 72 GB of RAM) to **Q8_0** (about 136 GB), 4 to 8 bit | Closer to the full model. With less RAM, Strata reads part of them from the SSD, which is much slower |
Less RAM than a row says can still work: Strata switches to its
[low-RAM mode](docs/MODELS.md#a-big-graphics-card-and-little-ram) by itself. Expect it to be slower, and much slower
with a small graphics card.
More choices:
- **[Swift 1.5](docs/MODELS.md#swift-15)** is a fine-tune that thinks for a shorter time before it answers, at about the same quality and RAM.
- **[NVFP4](docs/NVFP4.md)** is NVIDIA's 4-bit version for NVIDIA cards. It needs about 80 GB of RAM to keep every expert in RAM.
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** is a smaller, different model that reads pictures and can answer several requests at once.
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** is set up by hand.
Every size, download and what fits where: [docs/MODELS.md](docs/MODELS.md).
### DeepSeek V4.1-Flash (in the Terminal, from 0.2.4)
[DeepSeek V4.1-Flash](https://huggingface.co/ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF) is a different, much bigger model. ISTA-DASLab made the three sets below. Strata runs them on a PC with one 12 GB graphics card and 46 to 64 GB of RAM, and reads the rest from your SSD. It runs on NVIDIA RTX 30 to 50 cards and on the Radeon AI PRO R9700. It uses its own engine package, `strata-dsv41`, which `strata launch` downloads when you pick DeepSeek. The desktop app does not offer it yet.
| Set | Download | Quality against the original model | Writes at |
| --- | ---: | --- | --- |
| **IQ1_M** (1.75 bits) | 239 GB | KL 0.217, perplexity 3.80 (+21%); code +8.9%, agent work +10.2%; weaker on plain English prose. ISTA-DASLab's model card: keeps 98.7% of the base model's task average (AIME25, GPQA-Diamond, LiveCodeBench v6). | not measured yet |
| **IQ2_XXS** (2.00 bits) | 257 GB | KL 0.170, perplexity 3.63 (+15.5%); code +8.6%, agent work +5.3% | 6 to 9 tokens/s on an RTX 3060, about 5 on an RTX 5070 |
| **IQ2_XS** (2.25 bits) | 274 GB | KL 0.107, perplexity 3.37 (+7.3%); code +3.2%, agent work +2.2% | about 11 tokens/s on one Radeon AI PRO R9700 |
| **IQ4_NL** (Engram table, not a third model) | 111 GB, already in all three downloads above | The 4-bit Engram lookup table the model reads from RAM or SSD. All three sets use this one file (`DeepSeek-V4.1-Flash-Engram-IQ4_NL.gguf`). | n/a |
4x Radeon AI PRO R9700: about 31 tokens/s, 55 to 69 with DSpark.
Start it with `strata launch --model ist-das-2.00bpw` (or `ist-das-1.75bpw`, `ist-das-2.25bpw`). Details, speeds per card and the manual install: [docs/DEEPSEEK41.md](docs/DEEPSEEK41.md).
## How fast is it?
Based on a 12 GB graphics card (RTX 5070, Ryzen 5 7600, 64 GB RAM). See results from other cards:
[speed of each model](docs/MODELS.md#how-fast-is-each-size), [community results](docs/COMMUNITY_BENCHMARKS.md).
| Model | Answers appear at | Reads your prompt at |
| --- | --- | --- |
| **Q2_0** | `████████████` 94 tokens/s | `████████████` 2,650 tokens/s |
| **IQ2_XS** | `██████████░░` 79 tokens/s | `█████████░░░` 2,090 tokens/s |
| **IQ3_XXS** | `████████░░░░` 62 tokens/s | `████████░░░░` 1,750 tokens/s |
| **IQ3_S** | `███████░░░░░` 53 tokens/s | `███████░░░░░` 1,620 tokens/s |
| **Coder** | `███████░░░░░` 55 tokens/s | `██████████░░` 2,180 tokens/s |
A token is about ¾ of a word, so 60 tokens per second is faster than you can read. "Reads your prompt" is how fast Strata
takes in a long document, code or chat history.
Measured on a 16 GB AMD card (RX 9070 XT, Ryzen 9 3900X, 47 GB RAM):
| Model | Answers appear at | Reads your prompt at |
| --- | --- | --- |
| **Q2_0** | `████████░░░░` 60 tokens/s | `█████░░░░░░░` 1,160 tokens/s |
| **IQ2_XS** | `███████░░░░░` 52 tokens/s | `█████░░░░░░░` 1,110 tokens/s |
| **Coder** | `██████░░░░░░` 44 tokens/s | `██████░░░░░░` 1,420 tokens/s |
The bars use one scale for both tables. A card with more VRAM is faster: an RTX 3090 (24 GB) should write about 100-140
tokens per second. The engine version behind each number is in [DETAILS.md](docs/DETAILS.md#speed-measured).
## Using it
### In the app
The app has a **Chat**, a **Monitor** of the model and your GPU, CPU and RAM, a **Models** page and **Settings**. The chat
can work in a folder on your PC. In **Manual** mode it asks before it changes a file or runs a command. Type
`/loop 10m <what to do>` to repeat a message every 10 minutes while the chat is open.
The chat can search the web with DuckDuckGo (or Bing), with no account. You can add a search key under **Settings, Tools**.
Search is the one feature that sends something off your PC, the search words.
To run two models, turn on **Run multiple models** and give each model its own card of the same make.
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="The Strata app: a chat where the agent built a small voxel pagoda garden, with the page open in the preview on the right"><br>
<sub>A chat in the Strata app: the agent wrote a small voxel pagoda garden as one HTML file, and the page opens in the preview next to the conversation</sub></p>
### In the Terminal
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
<sub><code>strata monitor</code> while the model writes a reply: speed, GPU, memory and the recent requests</sub></p>
```sh
strata launch # guided screens: model, context, pictures, access, then serve
strata launch --model q2_0 --yes --no-tui # no questions: serve, print the address, return
strata launch --plan --model q2_0 # show what would happen; nothing changes
strata launch --status # what is being served now
strata launch --stop # stop it
strata monitor --url http://127.0.0.1:8080 # watch speed, GPU, memory and requests
strata update --check # is there a new engine? (strata update installs it)
strata doctor # check the installed engine files
```
`strata --help` lists everything. Starting and serving never download anything. Only the install, `strata launch` (when
you choose a model) and `strata update` do. To reach Strata from your phone or another PC, use
`strata launch --access lan`. It makes an API key (recommended; add `--no-api-key` to go without, with a warning). No browser opens unless
you add `--open`. Guide: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md). Whole client: [docs/TERMINAL.md](docs/TERMINAL.md).
### With AI agents
An agent installs Strata with the one-line command in Option B and runs
`strata launch --model NAME --yes --no-tui`. The steps, flags and exit codes are in [docs/AI_SETUP.md](docs/AI_SETUP.md),
and the repository rules are in [AGENTS.md](AGENTS.md).
### Connect your other apps
| App type | Base URL |
| --- | --- |
| OpenAI-compatible (most apps and agents) | `http://127.0.0.1:8080/v1` (any API key, any model name) |
| Anthropic API (Claude Code and others) | `http://127.0.0.1:8080`, without `/v1`. These apps add `/v1/messages` themselves. Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
| OpenAI Responses API (Codex CLI) | `http://127.0.0.1:8080/v1/responses` ([setup](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
`strata connect` prints the exact settings for OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent and OpenClaw.
`strata connect opencode --write` writes the file after asking and keeps a backup.
### Pictures
Pictures are on by default for models that can read them. Attach or paste a picture in the chat, or send it from your app.
The picture reader runs on the processor when your graphics card has less than 24 GB (a few seconds per picture, nothing
taken from the card's memory) and on the graphics card from 24 GB. The rule is the same for NVIDIA and AMD. Turn pictures
off per model, or choose the reader with `--vision off`, `--vision gpu` or `--vision cpu`.
The numbers behind the rule: on a Radeon AI PRO R9700 the card's reader took 57 ms for a 260-token picture and the
processor's took 3.1 s. On 16 GB cards (Tesla P100 and RTX A4000, Linux) the card's reader cost 3-5% of text speed,
because it holds about 1.4 GB that the model would use otherwise. On a Mac the reader runs on the Mac's GPU whatever its memory (0.45 s for a 260-token picture on an M4 with 32 GB, about 1.2 GB of the shared
memory); the processor reader needs about 50 s per picture there, so it runs only when you ask for it with `--vision cpu`.
### Good to know
- Choose the thinking level (**off, low, medium or high**) in the chat menu or in your app's "reasoning effort". Off is fastest.
- Strata answers one request at a time by default. For several at once, use `--parallel N` in `strata launch` or
`"parallel": 2` in the config ([BATCHING.md](docs/BATCHING.md)). On a 12 GB card this makes each answer slower.
- Strata reads the first message of a chat in full, about 1 minute per 30,000 tokens. Follow-up messages start in seconds.
- Where your chats are stored and the whole API: [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored), [docs/DETAILS.md](docs/DETAILS.md#using-it).
## Build from source
<details>
<summary>Windows, Linux and Mac from the repository</summary>
[Download Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) and unzip it, or `git clone` it.
**Windows:** double-click `START-HERE.bat`. **Linux:** run `./setup.sh` in the Strata folder.
**Mac (Apple Silicon, macOS 15 or newer):** install Apple's Command Line Tools once with `xcode-select --install`
(Xcode itself is not needed). Open Terminal in the Strata folder and run `./setup.sh`. It builds the Mac engine on your
Mac (about 7 minutes), asks the same questions, downloads the model and starts it. Next time, `./setup.sh` starts it
right away. Which size fits your Mac: [docs/MAC.md](docs/MAC.md).
The installer finds your card and sets up the right engine. It asks which model and size, how much context (how much text
the model keeps in mind) and whether it should read pictures. Press Enter for the recommended answer. Then it downloads the
model (about 70 GB) and starts it. If the download stops, run it again and it continues. Your browser opens the Strata
page 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. Wait, and don't close the window.
Next time, run `START-HERE.bat` (or `./setup.sh`) again. It starts right away and downloads nothing twice. Close its
window to stop the model. `UPDATE.bat` (`./update.sh`) updates Strata without starting it. Updating, Docker, several cards,
where the files go and every option: [docs/INSTALL.md](docs/INSTALL.md).
To measure your PC, run `BENCH.bat` (`./bench.sh`). It takes 5-10 minutes and writes a report you can share
([how](docs/COMMUNITY_BENCHMARKS.md#run-the-benchmark)).
**Optional: more speed on one card when the whole model fits in RAM.** Two opt-in settings by RubenTorney (#1904, #1905).
They help when you run one graphics card and your RAM holds every expert, for example Q2_0 or IQ3_XXS on a 64 GB PC. Open
`strata-<model>.json` in the Strata data folder ([where](docs/INSTALL.md#where-things-are-stored)) and add:
- `"--adapt-async", "1"` to the `"args"` list. The expert cache then updates on a helper thread instead of pausing the
answer. Measured on an RTX 5070 (Windows, Q2_0): +4-5% decode, +7-10% on code and long documents. Answers can differ
slightly from the default and stay the same from run to run.
- `"STRATA_PIN_BY_NEED": "1"` in the `"env"` block (Windows). Windows can lock only about half the RAM for the graphics
card, and this locks the experts the card needs first. Measured on the RTX 5070: +4.4% prompt reading.
```json
"args": [ ..., "--adapt-async", "1" ],
"env": { "STRATA_PIN_BY_NEED": "1" }
```
Restart Strata after the change. To undo, remove the two entries.
Follow the prompts to download and start a model. [Terminal guide](docs/TERMINAL_LAUNCH.md).
</details>
## How does it work?
<p align="center">
<img src="docs/media/terminal-monitor.png" width="900" alt="Strata terminal app showing model activity, speed, memory and recent requests">
<br><sub>Watch your model and hardware while it runs.</sub>
</p>
Models like this one usually run on servers with hundreds of gigabytes of graphics memory. Your graphics card has
12-24 GB. Strata makes the model fit by sharing the work across your whole PC. Picture a library.
<a id="build-from-source"></a>
- The model is a team of 24,576 small specialists ("experts"), like 24,576 reference books. Each word needs only 10 of them.
- **Your graphics card is the desk.** It is small and fast. The roughly 4,000 books used most often lie open on it.
- **Your RAM is the shelves in the reading room.** All 24,576 books are there. When a word needs a book that is not on the
desk, your processor reads it from the shelf at once, while the card keeps working.
- **Your SSD is the archive.** It holds a 29 GB lookup table. The model reads a few rows of it per word, so it does not
need to sit in RAM.
[Build from source](docs/SOURCE_INSTALL.md) · [Docker](docs/DOCKER.md) · [Full install guide](docs/INSTALL.md)
<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>
<a id="what-you-need"></a>
A small helper also guesses the next few words. The big model checks all of them at once and keeps the right ones. You get
the same answer, 1.6-1.8x sooner. Long texts are read in big pieces, up to 8,192 tokens at a time, at over 1,000 tokens
per second.
## Hardware
<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>
| Supported hardware | Guide |
| --- | --- |
| **NVIDIA GeForce RTX 20 / 30 / 40 / 50 series** | [NVIDIA setup](docs/INSTALL.md#what-you-need) |
| **AMD Radeon RX 7700 / 7800 / 7900, RX 9060 / 9070, AI PRO R9700** | [AMD setup](docs/AMD_HIP.md) |
| **AMD Radeon RX 6800 / 6900 · experimental** | [RX 6000 setup](docs/AMD_HIP.md#rdna2-gfx1030) |
| **Intel Arc · Linux** | [Intel setup](docs/INTEL_ARC.md) |
| **Apple Silicon · M1 or newer** | [Mac setup](docs/MAC.md) |
| **AMD Ryzen AI Max · Strix Halo** | [Strix Halo setup](docs/STRIX_HALO.md) |
| **NVIDIA DGX Spark · Linux** | [DGX Spark setup](docs/DGX_SPARK_LINUX.md) |
The longer explanation: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Every part and its numbers:
[the details](docs/DETAILS.md#how-it-works) and the [paper](docs/paper/Strata-Paper.pdf).
<a id="using-it"></a>
## Something went wrong?
## Tools
- **My PC froze the first time Strata started.** It loads the model into RAM. Wait, and don't close the window. Still
frozen after 10 minutes? Restart the PC, close other programs and try again, or pick a smaller size.
- **It stopped while downloading or installing.** Start the app or `strata launch` again, or run `START-HERE.bat` (or
`./setup.sh`) again. It continues where it stopped.
- **It is very slow, the disk light keeps blinking, or it says "the engine stopped unexpectedly".** Your PC does not have
enough free RAM. Close other programs (browsers use a lot), or pick a smaller size such as Q2_0 or IQ2_XS.
- **It says there is no engine for this PC.** In the app, open **Settings, Engine** and press **Update engine**. In the
Terminal, run `strata update`. `strata doctor` shows which engine files are installed.
- **It says port 8080 is already in use.** Strata is already running. Look for its window.
**Claude Code · Codex CLI · OpenCode · oh-my-pi · pi.dev · Hermes Agent · OpenClaw**, and more through the OpenAI- and Anthropic-compatible APIs.
More problems and fixes: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Still stuck? Open an
[issue](https://github.com/Niko1221/Strata/issues). In the app, attach the file from **Settings, Engine, Diagnostics, View
logs, Save logs**. In the Terminal, paste the output of `strata doctor`. From source, attach `strata-<model>.log` from the
Strata folder. For a security problem, report it privately: [SECURITY.md](SECURITY.md).
Run **`strata connect`** to see setup instructions for your tool. [Connection guide](docs/TERMINAL_LAUNCH.md#connect-your-apps-strata-connect).
## Credits and license
<a id="guides"></a>
<a id="how-fast-is-it"></a>
<a id="how-does-it-work"></a>
<a id="something-went-wrong"></a>
The model is [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) by the Qwen team. It was compressed by
[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5), agentionai (Gyro)
and Unsloth. Strata uses parts of [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). All credits:
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata is open source under the [MIT License](LICENSE). A few parts and
every model have their own licenses ([which ones](docs/HOW_IT_WORKS.md#license)).
## Help
## Contributing
Found a bug, or want to send a change or a benchmark? Read [CONTRIBUTING.md](CONTRIBUTING.md) first. It says what to
include so that others can check your work.
[All guides](docs) · [Troubleshooting](docs/TROUBLESHOOTING.md) · [Report a bug](https://github.com/Niko1221/Strata/issues/new/choose) ·
[Benchmarks](docs/COMMUNITY_BENCHMARKS.md) · [How it works](docs/HOW_IT_WORKS.md) · [Contribute](CONTRIBUTING.md)
## Support Strata
Strata is free and MIT-licensed. I build it and test it on whatever hardware I can get. If it runs well on your PC, a coffee helps
me keep testing new cards and fixing what you report. No pressure. Using it and reporting what you find helps just as much.
**Help keep local AI open to everyone.** A GitHub sponsorship or a coffee helps fund development and testing on more hardware.
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a>
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor_on_GitHub-EA4AAA?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="GitHub Sponsors: sponsor Strata" height="48"></a>
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee: support Strata" height="48"></a>
### Loyal supporters
Thank you to everyone who supports Strata.
<!-- supporters:start (one line per public supporter, newest last; copy a line and change the GitHub name in all four places) -->
<a href="https://github.com/ryan4yin"><img src="https://github.com/ryan4yin.png?size=96" width="48" height="48" alt="ryan4yin" title="ryan4yin"></a>
<a href="https://github.com/themactep"><img src="https://github.com/themactep.png?size=96" width="48" height="48" alt="themactep" title="themactep"></a>
<a href="https://github.com/macintog"><img src="https://github.com/macintog.png?size=96" width="48" height="48" alt="macintog" title="macintog"></a>
<a href="https://github.com/bisonbet"><img src="https://github.com/bisonbet.png?size=96" width="48" height="48" alt="bisonbet" title="bisonbet"></a>
<a href="https://github.com/vizv"><img src="https://github.com/vizv.png?size=96" width="48" height="48" alt="vizv" title="vizv"></a>
<!-- supporters:end -->
[MIT license](LICENSE) · [Credits and model licenses](docs/HOW_IT_WORKS.md#credits) · [Privacy](docs/SAFETY.md)
<a id="option-c-let-your-ai-set-it-up"></a>
<sub>For AI tools and automated readers: <a href="AGENTS.md">technical reference</a> · <a href="docs/AI_SETUP.md">setup instructions</a>.</sub>
**Sponsorships, hardware partnerships or collaborations:** [[email protected]](mailto:[email protected]).
Interested in sponsoring Strata? Contact me via [GitHub](https://github.com/Niko1221).
+206
View File
@@ -0,0 +1,206 @@
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="apps/host/public/brand/strata-lockup-dark.svg">
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="340">
</picture>
</p>
<h3 align="center">Uruchom potężną sztuczną inteligencję na własnym komputerze.</h3>
<p align="center">Rozmawiaj, programuj i pracuj z obrazami — lokalnie.</p>
<p align="center">
<a href="#modele">Modele</a> · <a href="#instalacja"><b>Instalacja</b></a> ·
<a href="#sprzęt">Sprzęt</a> · <a href="#narzędzia">Narzędzia</a> ·
<a href="#pomoc">Pomoc</a> · <a href="#wesprzyj-stratę">Wsparcie</a>
</p>
<p align="center">
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?label=download&color=0ab887" alt="Pobierz Stratę"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="Licencja MIT"></a>
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor-EA4AAA?logo=githubsponsors&logoColor=fff" alt="Wesprzyj Stratę na GitHub Sponsors"></a>
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy_me_a_coffee-FFDD00?logo=buymeacoffee&logoColor=000" alt="Wesprzyj Stratę na Buy Me a Coffee"></a>
</p>
<p align="center"><sub><a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português</a> · <b>Polski</b></sub></p>
**Bezpłatna, z otwartym kodem źródłowym i do uruchomienia u siebie.** Używaj Straty na swoim komputerze albo razem z ulubionymi narzędziami do programowania. Konto nie jest potrzebne.
## Modele
Najpierw polecamy **ISTA-DASLab**, potem **Unsloth** i innych dostawców — o ile zgodny model mieści się w pamięci.
<details>
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> &nbsp;<b>Qwen3.8-Flash-Next</b></summary>
| Dostawca | Do wyboru |
| --- | --- |
| **ISTA-DASLab · polecany** | Q2\_0, IQ2\_XS, IQ3\_XXS, IQ3\_S. [Coder: IQ1\_M](docs/MODELS.md#coder) do programowania. |
| **Unsloth** | UD-IQ4\_XS, UD-Q4\_K\_XL, UD-Q5\_K\_XL, UD-Q6\_K\_XL, Q8\_0 |
| **Gyro · agentionai** | [Gyro-S, Gyro-M](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF). Zużywa mniej pamięci, ale działa wolniej. |
| **Inne opcje** | [Swift 1.5](docs/MODELS.md#swift-15), [NVFP4](docs/NVFP4.md), [OrcaRouter](docs/ORCA.md) |
[Porównaj rozmiary, wymagania pamięciowe i zgodność](docs/MODELS.md).
</details>
<details>
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> &nbsp;<b>Qwen3.6-35B-A3B</b></summary>
**Unsloth:** UD-Q4_K_XL, UD-Q6_K_XL, Q8_0. To obecnie udokumentowane pakiety dla tego modelu.
[Przewodnik po modelu i zgodność](docs/QWEN36.md).
</details>
<details>
<summary><img src="docs/media/models/deepseek.svg" width="22" alt="" align="middle"> &nbsp;<b>DeepSeekV4.1-Flash-1017</b></summary>
W **aplikacji na komputer** i w **Terminalu**. To, co nie mieści się w pamięci, jest czytane z dysku SSD, więc potrzebujesz
dużo wolnego miejsca na dysku.
| Zestaw (ISTA-DAS) | Do pobrania | Potrzebne wolne miejsce |
| --- | ---: | ---: |
| 1.75 bpw | 239 GB | około 245 GB |
| 2.00 bpw | 257 GB | około 265 GB |
| 2.25 bpw | 274 GB | około 280 GB |
Działa na kartach NVIDIA RTX serii 30, 40 i 50 (Windows i Linux), AMD Radeon AI PRO R9700 (Linux) oraz AMD Ryzen AI Max — Strix Halo
(Windows). Instalacja pobiera pliki i silnik DeepSeek, a potem uruchamia model.
```sh
strata launch --family deepseek41 --model ist-das-2.00bpw --plan
```
Zmierzone na zestawie 2.00 bpw w Strata 0.2.5 (Strix Halo: 0.2.7). „Wyjście” to szybkość odpowiedzi, „prompt” — jak szybko Strata czyta twój tekst.
| Sprzęt | Wyjście | Prompt |
| --- | ---: | ---: |
| RTX 5070 12 GB, 64 GB RAM | 4–6 tokenów/s | około 110 tokenów/s (4K) |
| RTX 3060 12 GB, 46 GB RAM | 9–11 tokenów/s | około 155 tokenów/s (4K) |
| 1x Radeon AI PRO R9700, 251 GB RAM | 21–25 tokenów/s | 320–360 tokenów/s (8K) |
| 4x Radeon AI PRO R9700 | 74–77 tokenów/s | 400–450 tokenów/s (8K) |
| Ryzen AI Max+ 395 (Strix Halo), 128 GB pamięci współdzielonej | 19–21 tokenów/s (kod), 11 (czat) | 87 tokenów/s (4K), 125 tokenów/s (8K) |
[Pamięć, dysk i szczegóły](docs/DEEPSEEK41.md) · [Informacje o wydaniach](https://github.com/Niko1221/Strata/releases)
</details>
<a id="the-other-sizes"></a>
Nie wiesz, co się zmieści? [Wybierz model do swojego komputera](docs/MODELS.md#pick-by-memory).
## Instalacja
<a id="option-a-the-strata-app"></a>
### Aplikacja na komputer
**[Pobierz Stratę dla Windows, Linuxa lub Maca →](https://github.com/Niko1221/Strata/releases/latest)**
Zainstaluj aplikację, wybierz model i zacznij rozmawiać. Dla każdego systemu jest jeden instalator. Przy pierwszym
uruchomieniu aplikacja pobiera silnik dla twojej karty graficznej. [Pomoc z instalatorem](docs/INSTALL.md#the-app).
<sub>Pliki: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
<p align="center">
<img src="docs/media/app-chat.png" width="900" alt="Aplikacja Strata na komputer: czat i podgląd kodu na żywo obok siebie">
<br><sub>Czat, pliki i podgląd na żywo w jednym oknie.</sub>
</p>
<a id="option-b-the-terminal-one-line"></a>
### Aplikacja Terminal
Wolisz terminal? Zainstaluj Stratę, a potem uruchom **`strata launch`**.
<details open>
<summary><b>Polecenia instalacji</b></summary>
**Linux / Mac**
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
```
**Windows PowerShell**
```powershell
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
```
W Windows otwórz potem nowe okno terminala. Następnie:
```sh
strata launch
```
Postępuj zgodnie z podpowiedziami, aby pobrać i uruchomić model. [Przewodnik po Terminalu](docs/TERMINAL_LAUNCH.md).
</details>
<p align="center">
<img src="docs/media/terminal-monitor.png" width="900" alt="Aplikacja Terminal Straty pokazująca aktywność modelu, szybkość, pamięć i ostatnie zapytania">
<br><sub>Obserwuj model i sprzęt w trakcie pracy.</sub>
</p>
<a id="build-from-source"></a>
[Budowanie ze źródeł](docs/SOURCE_INSTALL.md) · [Docker](docs/DOCKER.md) · [Pełny przewodnik instalacji](docs/INSTALL.md)
<a id="what-you-need"></a>
## Sprzęt
| Obsługiwany sprzęt | Przewodnik |
| --- | --- |
| **NVIDIA GeForce RTX serii 20 / 30 / 40 / 50** | [Konfiguracja NVIDIA](docs/INSTALL.md#what-you-need) |
| **AMD Radeon RX 7700 / 7800 / 7900, RX 9060 / 9070, AI PRO R9700** | [Konfiguracja AMD](docs/AMD_HIP.md) |
| **AMD Radeon RX 6800 / 6900 · eksperymentalnie** | [Konfiguracja RX 6000](docs/AMD_HIP.md#rdna2-gfx1030) |
| **Intel Arc · Linux** | [Konfiguracja Intel](docs/INTEL_ARC.md) |
| **Apple Silicon · M1 lub nowszy** | [Konfiguracja Mac](docs/MAC.md) |
| **AMD Ryzen AI Max · Strix Halo** | [Konfiguracja Strix Halo](docs/STRIX_HALO.md) |
<a id="using-it"></a>
## Narzędzia
**Claude Code · Codex CLI · OpenCode · oh-my-pi · pi.dev · Hermes Agent · OpenClaw** i inne — przez API zgodne z OpenAI i Anthropic.
Uruchom **`strata connect`**, aby zobaczyć instrukcję konfiguracji dla swojego narzędzia. [Przewodnik po połączeniach](docs/TERMINAL_LAUNCH.md#connect-your-apps-strata-connect).
<a id="guides"></a>
<a id="how-fast-is-it"></a>
<a id="how-does-it-work"></a>
<a id="something-went-wrong"></a>
## Pomoc
[Wszystkie przewodniki](docs) · [Rozwiązywanie problemów](docs/TROUBLESHOOTING.md) · [Zgłoś błąd](https://github.com/Niko1221/Strata/issues/new/choose) ·
[Testy wydajności](docs/COMMUNITY_BENCHMARKS.md) · [Jak to działa](docs/HOW_IT_WORKS.md) · [Współtworzenie](CONTRIBUTING.md)
## Wesprzyj Stratę
**Pomóż zachować lokalną sztuczną inteligencję otwartą dla wszystkich.** Sponsoring na GitHubie lub kawa pomaga finansować rozwój i testy na większej liczbie sprzętu.
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor_on_GitHub-EA4AAA?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="GitHub Sponsors: wesprzyj Stratę" height="48"></a>
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee: wesprzyj Stratę" height="48"></a>
### Wierni wspierający
Dziękujemy wszystkim, którzy wspierają Stratę.
<!-- supporters:start (one line per public supporter, newest last; copy a line and change the GitHub name in all four places) -->
<a href="https://github.com/ryan4yin"><img src="https://github.com/ryan4yin.png?size=96" width="48" height="48" alt="ryan4yin" title="ryan4yin"></a>
<a href="https://github.com/themactep"><img src="https://github.com/themactep.png?size=96" width="48" height="48" alt="themactep" title="themactep"></a>
<a href="https://github.com/macintog"><img src="https://github.com/macintog.png?size=96" width="48" height="48" alt="macintog" title="macintog"></a>
<a href="https://github.com/bisonbet"><img src="https://github.com/bisonbet.png?size=96" width="48" height="48" alt="bisonbet" title="bisonbet"></a>
<a href="https://github.com/vizv"><img src="https://github.com/vizv.png?size=96" width="48" height="48" alt="vizv" title="vizv"></a>
<!-- supporters:end -->
[Licencja MIT](LICENSE) · [Podziękowania i licencje modeli](docs/HOW_IT_WORKS.md#credits) · [Prywatność](docs/SAFETY.md)
<a id="option-c-let-your-ai-set-it-up"></a>
<sub>Dla narzędzi AI i automatycznych czytników: <a href="AGENTS.md">dokumentacja techniczna</a> · <a href="docs/AI_SETUP.md">instrukcja instalacji</a>.</sub>
**Sponsoring, partnerstwa sprzętowe i współpraca:** [[email protected]](mailto:[email protected]).
+135 -342
View File
@@ -1,409 +1,202 @@
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="apps/host/public/brand/strata-lockup-dark.svg">
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="380">
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="340">
</picture>
</p>
<p align="center"><a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <a href="README.es.md">Español</a> · <b>Português</b></p>
<h3 align="center">Rode um modelo de ponta no seu próprio PC gamer</h3>
<p align="center">Suporte a NVIDIA, AMD, Intel · Windows, Linux, Mac · gratuito e de código aberto</p>
<h3 align="center">IA poderosa no seu próprio computador.</h3>
<p align="center">Converse, programe e trabalhe com imagens, localmente.</p>
<p align="center">
<a href="https://github.com/Niko1221/Strata/releases"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Latest release"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT license"></a>
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="GitHub stars"></a>
<a href="#modelos">Modelos</a> · <a href="#instalação"><b>Instalação</b></a> ·
<a href="#hardware">Hardware</a> · <a href="#ferramentas">Ferramentas</a> ·
<a href="#ajuda">Ajuda</a> · <a href="#apoie-o-strata">Apoiar</a>
</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>Um jardim de pagodes em voxels, gerado com um único prompt, rodando numa RTX 5070 com o Strata (IQ3_S, contexto de 128K) ·
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">vídeo completo (49 s)</a></sub></p>
<p align="center">
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?label=download&color=0ab887" alt="Baixar o Strata"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="Licença MIT"></a>
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor-EA4AAA?logo=githubsponsors&logoColor=fff" alt="Apoie o Strata no GitHub Sponsors"></a>
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy_me_a_coffee-FFDD00?logo=buymeacoffee&logoColor=000" alt="Apoie o Strata no Buy Me a Coffee"></a>
</p>
O Strata roda o **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**, um modelo de 125 bilhões de parâmetros que
normalmente precisa de um servidor, num PC comum. Ele conversa, escreve código, lê imagens e funciona com os seus aplicativos e
agentes de programação. Suas conversas e arquivos ficam no seu PC.
<p align="center"><sub><a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <a href="README.es.md">Español</a> · <b>Português</b> · <a href="README.pl.md">Polski</a></sub></p>
**Primeira vez aqui?** Veja [o que você precisa](#o-que-você-precisa) e depois [instale o app](#opção-a-o-app-strata).
**Gratuito, de código aberto e seu para executar.** Use o Strata no desktop ou com suas ferramentas de programação favoritas. Não precisa de conta.
- **Roda num PC gamer.** 32 GB somando memória RAM e memória da placa de vídeo já bastam para começar.
- **Funciona com as suas ferramentas.** Ele fala as APIs da OpenAI e da Anthropic, então Claude Code, Codex CLI, OpenCode e outros agentes se conectam a ele.
- **Lê imagens.** Já vêm ativadas.
- **Gratuito.** Licença MIT, sem conta, sem nuvem.
- **Privado.** Sem telemetria. O que o Strata baixa, quando usa a internet e como confere os downloads está em [docs/SAFETY.md](docs/SAFETY.md).
## Modelos
Versão 0.2.3 · mecanismo 0.1.48
Recomendamos **primeiro a ISTA-DASLab**, depois a **Unsloth** e outros provedores, quando um modelo compatível cabe na memória.
## O que você precisa
<details>
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> &nbsp;<b>Qwen3.8-Flash-Next</b></summary>
**Você precisa de pelo menos 32 GB somando a memória RAM e a memória da placa de vídeo (VRAM).** Num Mac ou num Ryzen AI Max, isso é a
memória unificada. Mais memória permite um modelo maior ou mais rápido.
- Um PC de 32 GB com uma placa de 12 a 16 GB roda o Coder.
- 64 GB de RAM rodam todos os tamanhos.
| Seu computador | Detalhes |
| Provedor | Opções |
| --- | --- |
| **NVIDIA** GeForce RTX série 20, 30, 40, 50 (12 GB ou mais) | Instale o app ou o Terminal. |
| **AMD** Radeon RX 6800 e mais novas, Radeon AI PRO R9700 (12 GB ou mais) | [Placas AMD](docs/AMD_HIP.md) |
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md), [mecanismo Intel](docs/INTEL.md) |
| **Mac com Apple Silicon** (M1 ou mais novo, macOS 15 ou mais novo) | [Mac](docs/MAC.md) |
| **AMD Ryzen AI Max (Strix Halo)** e outros sistemas de memória unificada | [Strix Halo](docs/STRIX_HALO.md) |
| Placas e processadores mais antigos | [GPUs antigas](docs/OLDER_GPUS.md), [CPUs antigas](docs/INSTALL.md#older-cpus-experimental) |
| **ISTA-DASLab · recomendado** | Q2\_0, IQ2\_XS, IQ3\_XXS, IQ3\_S. [Coder: IQ1\_M](docs/MODELS.md#coder) para programar. |
| **Unsloth** | UD-IQ4\_XS, UD-Q4\_K\_XL, UD-Q5\_K\_XL, UD-Q6\_K\_XL, Q8\_0 |
| **Gyro · agentionai** | [Gyro-S, Gyro-M](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF). Usa menos memória, mas é mais lento. |
| **Outras opções** | [Swift 1.5](docs/MODELS.md#swift-15), [NVFP4](docs/NVFP4.md), [OrcaRouter](docs/ORCA.md) |
Você também precisa de:
[Compare tamanhos, necessidades de memória e compatibilidade](docs/MODELS.md).
- cerca de 80 GB de espaço livre no disco (um SSD deixa a primeira inicialização bem mais rápida)
- Windows 10 / 11, Linux ou macOS
- um driver de vídeo atualizado
</details>
Duas ou três placas da mesma marca podem dividir um modelo ([multi-GPU](docs/MULTI_GPU.md)). O Strata nunca divide um modelo entre
placas NVIDIA e AMD. Lista completa: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
<details>
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> &nbsp;<b>Qwen3.6-35B-A3B</b></summary>
**Unsloth:** UD-Q4_K_XL, UD-Q6_K_XL, Q8_0. Esses são os pacotes documentados atualmente para este modelo.
[Guia do modelo e compatibilidade](docs/QWEN36.md).
</details>
<details>
<summary><img src="docs/media/models/deepseek.svg" width="22" alt="" align="middle"> &nbsp;<b>DeepSeekV4.1-Flash-1017</b></summary>
No **aplicativo de desktop** e no **Terminal**. O que não cabe na memória é lido do seu SSD, por isso é preciso muito espaço livre em disco.
| Conjunto (ISTA-DAS) | Download | Espaço livre necessário |
| --- | ---: | ---: |
| 1.75 bpw | 239 GB | cerca de 245 GB |
| 2.00 bpw | 257 GB | cerca de 265 GB |
| 2.25 bpw | 274 GB | cerca de 280 GB |
Roda em NVIDIA RTX 30, 40 e 50 (Windows e Linux) e em AMD Radeon AI PRO R9700 (Linux). A instalação baixa os arquivos e o motor do DeepSeek e depois inicia o modelo.
```sh
strata launch --family deepseek41 --model ist-das-2.00bpw --plan
```
Medido com o conjunto de 2.00 bpw no Strata 0.2.5. “Saída” é a velocidade da resposta; “Prompt” é a rapidez com que o Strata lê o seu texto.
| Hardware | Saída | Prompt |
| --- | ---: | ---: |
| RTX 5070 12 GB, 64 GB de RAM | 4 a 6 tokens/s | cerca de 110 tokens/s (4K) |
| RTX 3060 12 GB, 46 GB de RAM | 9 a 11 tokens/s | cerca de 155 tokens/s (4K) |
| 1x Radeon AI PRO R9700, 251 GB de RAM | 21 a 25 tokens/s | 320 a 360 tokens/s (8K) |
| 4x Radeon AI PRO R9700 | 74 a 77 tokens/s | 400 a 450 tokens/s (8K) |
[Memória, disco e detalhes](docs/DEEPSEEK41.md) · [Notas da versão](https://github.com/Niko1221/Strata/releases)
</details>
<a id="the-other-sizes"></a>
Não sabe o que cabe? [Escolha um modelo para o seu computador](docs/MODELS.md#pick-by-memory).
## Instalação
Escolha uma opção. Todas iniciam o mesmo mecanismo e usam os mesmos modelos.
<a id="option-a-the-strata-app"></a>
### Opção A: o app Strata
### Aplicativo de desktop
O jeito mais fácil. O app já traz o mecanismo: instale, escolha um modelo e converse. Baixe o arquivo para o seu sistema e a sua
placa de vídeo na [página de releases](https://github.com/Niko1221/Strata/releases/latest). Não sabe qual? Veja a explicação
logo abaixo da tabela.
**[Baixar o Strata para Windows, Linux ou Mac →](https://github.com/Niko1221/Strata/releases/latest)**
| Sistema | Sua placa de vídeo | Arquivo para baixar |
| --- | --- | --- |
| **Windows 10 / 11** | NVIDIA RTX, driver 580 ou mais novo | [`Strata-0.2.3-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda13.exe) |
| | NVIDIA, placa mais antiga ou driver anterior ao 580 | [`Strata-0.2.3-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda12.exe) |
| | AMD Radeon série RX 9000 ou Radeon AI PRO R9700 | [`Strata-0.2.3-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip.exe) |
| | AMD Radeon série RX 7000 ou Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip-rdna3.exe) |
| **Linux** | NVIDIA RTX, driver 580 ou mais novo | [`Strata-0.2.3-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda13.AppImage) |
| | NVIDIA, placa mais antiga ou driver anterior ao 580 | [`Strata-0.2.3-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda12.AppImage) |
| | AMD Radeon série RX 9000 ou Radeon AI PRO R9700 | [`Strata-0.2.3-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx9070.AppImage) |
| | AMD Radeon série RX 7000 | [`Strata-0.2.3-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx7900.AppImage) |
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-strixhalo.AppImage) |
| **Mac** (Apple Silicon, macOS 15 ou mais novo) | integrada | [`Strata-0.2.3-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-mac.dmg) |
Instale o aplicativo, escolha um modelo e comece a conversar. Há um instalador por sistema. Na primeira vez que abre, o aplicativo baixa o motor da sua placa de vídeo. [Ajuda com o instalador](docs/INSTALL.md#the-app).
**Se você estiver em dúvida:** para NVIDIA, use `cuda13` se o seu driver for a versão 580 ou mais nova, e `cuda12` nos outros casos. O `cuda12`
serve para placas e drivers mais antigos. Os arquivos `hip` e `amd` são para placas AMD. Outras placas AMD (RX 6000) ainda não têm app pronto: veja [Placas AMD](docs/AMD_HIP.md).
<sub>Arquivos: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
Depois:
<p align="center">
<img src="docs/media/app-chat.png" width="900" alt="Aplicativo de desktop do Strata com um chat e uma prévia do código ao vivo lado a lado">
<br><sub>Chat, arquivos e prévia ao vivo em uma só janela.</sub>
</p>
- **Windows:** execute o `.exe`. Ele instala só para o seu usuário e não pede direitos de administrador. Se o Windows disser "O Windows
protegeu o computador", escolha **Mais informações** e depois **Executar assim mesmo** (o instalador ainda não tem assinatura de código).
- **Linux:** torne o arquivo executável (`chmod +x`) e inicie. É preciso ter o FUSE 2: `sudo apt install libfuse2` (Ubuntu 22.04)
ou `libfuse2t64` (Ubuntu 24.04 e mais novos).
- **Mac:** abra o `.dmg` e arraste o Strata para Aplicativos.
<a id="option-b-the-terminal-one-line"></a>
Na primeira tela, o app lê o seu PC e recomenda um modelo que caiba nele. Clique em instalar. O modelo é um download grande
(cerca de 40 a 70 GB) e continua de onde parou se for interrompido. Fechar a janela encerra o Strata e o mecanismo.
### Aplicativo de Terminal
### Opção B: o Terminal (uma linha)
Prefere o terminal? Instale o Strata e execute **`strata launch`**.
O Terminal é o comando `strata`, para scripts, servidores e agentes de IA.
<details open>
<summary><b>Comandos de instalação</b></summary>
Linux e Mac:
**Linux / Mac**
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
```
Windows (PowerShell):
**Windows PowerShell**
```powershell
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
```
O script confere o seu PC, baixa o Terminal, verifica o SHA-256 e descompacta na sua pasta pessoal. Não precisa de direitos de
administrador. No Windows, abra uma nova janela do terminal depois. Então inicie o Strata:
No Windows, abra um novo terminal depois. Em seguida:
```sh
strata launch
```
Ele faz uma pergunta por vez: modelo, contexto, imagens, quem pode se conectar. Depois baixa o mecanismo para a sua placa de vídeo
(assinada e testada antes do uso) e o modelo, e começa a servir. Em scripts e agentes de IA, use flags:
```sh
strata launch --model q2_0 --yes --no-tui
```
Para ler o script de instalação antes de executá-lo, baixe-o primeiro com `curl -fsSL <address> -o install.sh`, leia e então rode
`sh install.sh`. Todas as telas e flags: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
### Opção C: deixe a sua IA configurar
Cole isto no Claude Code, Cursor, Codex, GitHub Copilot ou outro assistente de programação:
```text
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
```
Ele confere a sua placa de vídeo, a RAM e o disco, escolhe o modelo que cabe, instala e inicia, e diz como conectar os seus
aplicativos. Ferramentas de IA também podem instalar, iniciar e parar o Strata pelo [servidor MCP](docs/MCP_SERVER.md) dele.
Prefere compilar por conta própria? Veja [Compilar a partir do código-fonte](#compilar-a-partir-do-código-fonte).
## Modelos
O app e o `strata launch` recomendam um modelo para a sua memória. É um único modelo em vários tamanhos, mais ou menos comprimidos.
Os tamanhos menores são mais rápidos. Os maiores são um pouco mais inteligentes.
### Pouca memória? Experimente o Gyro
O [Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) (da agentionai) é a menor forma de rodar este modelo.
| Tamanho | Memória necessária (RAM + VRAM) | Download | O que é |
| --- | ---: | ---: | --- |
| **Gyro-S** | 27,6 GB | 58,5 GB | Para PCs onde nenhum outro tamanho cabe. A qualidade é menor que a do IQ2_XS. |
| **Gyro-M** | 35 GB | 92,0 GB | Qualidade próxima à do IQ3_XXS, com cerca de 20% menos memória da GPU. Ele escreve as respostas 16% mais devagar que o Q2_0 numa RTX 3060 de 12 GB e 21% mais devagar numa R9700. |
O Gyro roda em placas NVIDIA e AMD (AMD é experimental) em placas Intel Arc no Linux (testado numa Arc A750) e, desde a 0.2.2, em Macs com Apple Silicon (Gyro-S num Mac mini de 32 GB: 9 tokens/s, com os especialistas lidos do SSD). Detalhes e medições:
[docs/GYRO.md](docs/GYRO.md). Inicie com `strata launch --model gyro-s`.
### Os outros tamanhos
| Sua memória (RAM + VRAM) | Escolha | Observações |
| --- | --- | --- |
| **32 GB** | **Gyro-S**, ou o **[Coder](docs/MODELS.md#coder)** para código | O Coder precisa de 32 GB de RAM e mantém metade dos experts, então é mais fraco fora de código |
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39,2 GB), ou **Gyro-M** (35 GB) | O Swift pensa por menos tempo antes de responder. O Gyro-M chega perto do IQ3_XXS em qualidade, mas escreve as respostas 16 a 21% mais devagar que o Q2_0 |
| **48 GB** | **IQ3_XXS** (47,0 GB) | Mais lento, melhor |
| **64 GB** | **IQ3_S** (54,8 GB), o original ou o Swift 1.5 | O mais lento, o melhor: empata com o modelo completo nos testes publicados |
| **96 GB ou mais** | **UD-IQ4_XS** da Unsloth (cerca de 72 GB de RAM) até **Q8_0** (cerca de 136 GB), de 4 a 8 bits | Mais perto do modelo completo. Com menos RAM, o Strata lê parte deles do SSD, o que é bem mais lento |
Com menos RAM do que a linha indica, ainda pode funcionar: o Strata muda sozinho para o
[modo de pouca RAM](docs/MODELS.md#a-big-graphics-card-and-little-ram). Espere mais lentidão, e muito mais com uma placa de vídeo pequena.
Mais opções:
- **[Swift 1.5](docs/MODELS.md#swift-15)** é um ajuste fino que pensa por menos tempo antes de responder, com qualidade e RAM parecidas.
- **[NVFP4](docs/NVFP4.md)** é a versão de 4 bits da NVIDIA para placas NVIDIA. Precisa de cerca de 80 GB de RAM para manter todos os experts na RAM.
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** é outro modelo, menor, que lê imagens e pode atender vários pedidos ao mesmo tempo.
- **[Uncensored IQ3_XXS da OrcaRouter](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** é configurado manualmente.
Todos os tamanhos, downloads e o que cabe em cada configuração: [docs/MODELS.md](docs/MODELS.md).
### DeepSeek V4.1-Flash (no Terminal, a partir da 0.2.4)
[DeepSeek V4.1-Flash](https://huggingface.co/ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF) é outro modelo, bem maior. A ISTA-DASLab fez os três conjuntos abaixo. O Strata os executa em um PC com uma placa de vídeo de 12 GB e de 46 a 64 GB de RAM, e lê o resto do seu SSD. Ele roda em placas NVIDIA RTX das séries 30 a 50 e na Radeon AI PRO R9700. Usa um pacote de motor próprio, `strata-dsv41`, que o `strata launch` baixa quando você escolhe o DeepSeek. O app de desktop ainda não o oferece.
| Conjunto | Download | Qualidade em relação ao modelo original | Responde a |
| --- | ---: | --- | --- |
| **IQ1_M** (1,75 bits) | 239 GB | KL 0,217, perplexidade 3,80 (+21%); código +8,9%, tarefas de agente +10,2%; mais fraco em prosa comum em inglês. Segundo o cartão do modelo da ISTA-DASLab, mantém 98,7% da média de tarefas do modelo base (AIME25, GPQA-Diamond, LiveCodeBench v6). | ainda não medida |
| **IQ2_XXS** (2.00 bits) | 257 GB | KL 0,170, perplexidade 3,63 (+15,5%); código +8,6%, tarefas de agente +5,3% | 6 a 9 tokens/s em uma RTX 3060, cerca de 5 em uma RTX 5070 |
| **IQ2_XS** (2.25 bits) | 274 GB | KL 0,107, perplexidade 3,37 (+7,3%); código +3,2%, tarefas de agente +2,2% | cerca de 11 tokens/s em uma Radeon AI PRO R9700 |
| **IQ4_NL** (tabela Engram, não é um terceiro modelo) | 111 GB, já incluídos nos três downloads acima | A tabela de consulta Engram de 4 bits que o modelo lê da RAM ou do SSD. Os três conjuntos usam este único arquivo (`DeepSeek-V4.1-Flash-Engram-IQ4_NL.gguf`). | n/a |
4x Radeon AI PRO R9700: cerca de 31 tokens/s, 55 a 69 com DSpark.
Inicie com `strata launch --model ist-das-2.00bpw` (ou `ist-das-1.75bpw`, `ist-das-2.25bpw`). Detalhes, velocidades por placa e a instalação manual: [docs/DEEPSEEK41.md](docs/DEEPSEEK41.md).
## Qual é a velocidade?
Com base numa placa de vídeo de 12 GB (RTX 5070, Ryzen 5 7600, 64 GB de RAM). Veja resultados de outras placas:
[velocidade de cada modelo](docs/MODELS.md#how-fast-is-each-size), [resultados da comunidade](docs/COMMUNITY_BENCHMARKS.md).
| Modelo | As respostas aparecem a | Lê o seu prompt a |
| --- | --- | --- |
| **Q2_0** | `████████████` 94 tokens/s | `████████████` 2.650 tokens/s |
| **IQ2_XS** | `██████████░░` 79 tokens/s | `█████████░░░` 2.090 tokens/s |
| **IQ3_XXS** | `████████░░░░` 62 tokens/s | `████████░░░░` 1.750 tokens/s |
| **IQ3_S** | `███████░░░░░` 53 tokens/s | `███████░░░░░` 1.620 tokens/s |
| **Coder** | `███████░░░░░` 55 tokens/s | `██████████░░` 2.180 tokens/s |
Um token equivale a cerca de ¾ de palavra, então 60 tokens por segundo é mais rápido do que você lê. "Lê o seu prompt" é a velocidade com que o Strata
absorve um documento longo, um código ou um histórico de conversa.
Medido numa placa AMD de 16 GB (RX 9070 XT, Ryzen 9 3900X, 47 GB de RAM):
| Modelo | As respostas aparecem a | Lê o seu prompt a |
| --- | --- | --- |
| **Q2_0** | `████████░░░░` 60 tokens/s | `█████░░░░░░░` 1.160 tokens/s |
| **IQ2_XS** | `███████░░░░░` 52 tokens/s | `█████░░░░░░░` 1.110 tokens/s |
| **Coder** | `██████░░░░░░` 44 tokens/s | `██████░░░░░░` 1.420 tokens/s |
As barras usam a mesma escala nas duas tabelas. Uma placa com mais VRAM é mais rápida: uma RTX 3090 (24 GB) deve escrever cerca de 100 a 140
tokens por segundo. A versão do mecanismo por trás de cada número está em [DETAILS.md](docs/DETAILS.md#speed-measured).
## Como usar
### No app
O app tem um **Chat**, um **Monitor** do modelo e da sua GPU, CPU e RAM, uma página de **Modelos** e as **Configurações**. O chat
pode trabalhar numa pasta do seu PC. No modo **Manual**, ele pergunta antes de alterar um arquivo ou executar um comando. Digite
`/loop 10m <o que fazer>` para repetir uma mensagem a cada 10 minutos enquanto o chat estiver aberto.
O chat pode pesquisar na web com o DuckDuckGo (ou o Bing), sem conta. Você pode adicionar uma chave de busca em **Configurações, Ferramentas**.
A busca é o único recurso que envia algo para fora do seu PC: os termos da pesquisa.
Para rodar dois modelos, ative **Executar vários modelos** e dê a cada modelo a sua própria placa da mesma marca.
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="The Strata app: a chat where the agent built a small voxel pagoda garden, with the page open in the preview on the right"><br>
<sub>Um chat no app Strata: o agente escreveu um pequeno jardim de pagode em voxels em um único arquivo HTML, e a página abre na pré-visualização ao lado da conversa</sub></p>
### No Terminal
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
<sub><code>strata monitor</code> enquanto o modelo responde: velocidade, GPU, memória e as solicitações recentes</sub></p>
```sh
strata launch # telas guiadas: modelo, contexto, imagens, acesso e depois servir
strata launch --model q2_0 --yes --no-tui # sem perguntas: serve, mostra o endereço e volta
strata launch --plan --model q2_0 # mostra o que aconteceria; nada muda
strata launch --status # o que está sendo servido agora
strata launch --stop # para o serviço
strata monitor --url http://127.0.0.1:8080 # acompanha velocidade, GPU, memória e requisições
strata update --check # há um mecanismo novo? (strata update instala)
strata doctor # confere os arquivos do mecanismo instalada
```
`strata --help` lista tudo. Iniciar e servir nunca baixam nada. Só a instalação, o `strata launch` (quando você
escolhe um modelo) e o `strata update` baixam. Para acessar o Strata do celular ou de outro PC, use
`strata launch --access lan`. Ele cria uma chave de API e não inicia numa rede sem uma. Nenhum navegador abre, a menos que
você adicione `--open`. Guia: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md). Cliente completo: [docs/TERMINAL.md](docs/TERMINAL.md).
### Com agentes de IA
Um agente instala o Strata com o comando de uma linha da Opção B e executa
`strata launch --model NAME --yes --no-tui`. Os passos, as flags e os códigos de saída estão em [docs/AI_SETUP.md](docs/AI_SETUP.md),
e as regras do repositório estão em [AGENTS.md](AGENTS.md).
### Conecte os seus outros aplicativos
| Tipo de aplicativo | URL base |
| --- | --- |
| Compatível com OpenAI (a maioria dos apps e agentes) | `http://127.0.0.1:8080/v1` (qualquer chave de API, qualquer nome de modelo) |
| API da Anthropic (Claude Code e outros) | `http://127.0.0.1:8080`, sem `/v1`. Esses aplicativos adicionam `/v1/messages` sozinhos. Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
| OpenAI Responses API (Codex CLI) | `http://127.0.0.1:8080/v1/responses` ([configuração](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
`strata connect` mostra as configurações exatas para OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent e OpenClaw.
`strata connect opencode --write` grava o arquivo depois de perguntar e mantém um backup.
### Imagens
As imagens vêm ativadas nos modelos que sabem lê-las. Anexe ou cole uma imagem no chat, ou envie pelo seu aplicativo.
O leitor de imagens roda no processador quando a sua placa de vídeo tem menos de 24 GB (alguns segundos por imagem, sem tirar nada
da memória da placa) e roda na placa de vídeo a partir de 24 GB. A regra é a mesma para NVIDIA e AMD. Desative as imagens
por modelo, ou escolha o leitor com `--vision off`, `--vision gpu` ou `--vision cpu`.
Os números por trás da regra: numa Radeon AI PRO R9700, o leitor da placa levou 57 ms para uma imagem de 260 tokens e o do
processador levou 3,1 s. Em placas de 16 GB (Tesla P100 e RTX A4000, Linux), o leitor da placa custou de 3 a 5% da velocidade do texto,
porque ocupa cerca de 1,4 GB que o modelo usaria de outra forma. No Mac, a GPU faz a leitura, seja qual for a memória (0,45 s para uma imagem de 260 tokens em um M4 de 32 GB, cerca de 1,2 GB da memória compartilhada);
o processador leva cerca de 50 s por imagem, então só lê com `--vision cpu`.
### Bom saber
- Escolha o nível de raciocínio (**off, low, medium ou high**) no menu do chat ou no "reasoning effort" do seu aplicativo. Off é o mais rápido.
- Por padrão, o Strata responde a um pedido por vez. Para vários ao mesmo tempo, use `--parallel N` no `strata launch` ou
`"parallel": 2` na configuração ([BATCHING.md](docs/BATCHING.md)). Numa placa de 12 GB, isso deixa cada resposta mais lenta.
- O Strata lê a primeira mensagem de uma conversa por inteiro, cerca de 1 minuto a cada 30.000 tokens. As mensagens seguintes começam em segundos.
- Onde ficam as suas conversas e a API completa: [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored), [docs/DETAILS.md](docs/DETAILS.md#using-it).
## Compilar a partir do código-fonte
<details>
<summary>Windows, Linux e Mac a partir do repositório</summary>
[Baixe o Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) e descompacte, ou use `git clone`.
**Windows:** dê dois cliques em `START-HERE.bat`. **Linux:** execute `./setup.sh` na pasta do Strata.
**Mac (Apple Silicon, macOS 15 ou mais novo):** instale uma vez as Command Line Tools da Apple com `xcode-select --install`
(o Xcode em si não é necessário). Abra o Terminal na pasta do Strata e execute `./setup.sh`. Ele compila o mecanismo do Mac no seu
Mac (cerca de 7 minutos), faz as mesmas perguntas, baixa o modelo e inicia. Da próxima vez, `./setup.sh` inicia
na hora. Qual tamanho cabe no seu Mac: [docs/MAC.md](docs/MAC.md).
O instalador encontra a sua placa e configura o mecanismo certo. Ele pergunta qual modelo e tamanho, quanto contexto (quanto texto
o modelo mantém em mente) e se deve ler imagens. Pressione Enter para aceitar a resposta recomendada. Depois ele baixa o
modelo (cerca de 70 GB) e inicia. Se o download parar, execute de novo e ele continua. O navegador abre a página do Strata em
`http://127.0.0.1:8080`.
> Enquanto o modelo inicia, o seu PC pode ficar lento ou parar de responder por 1 a 3 minutos (mais tempo na primeira vez). O Strata carrega
> de 35 a 55 GB na sua RAM e trava parte dela para a placa de vídeo. Espere e não feche a janela.
Da próxima vez, execute `START-HERE.bat` (ou `./setup.sh`) de novo. Ele inicia na hora e não baixa nada duas vezes. Feche a janela
dele para parar o modelo. `UPDATE.bat` (`./update.sh`) atualiza o Strata sem iniciá-lo. Atualização, Docker, várias placas,
onde ficam os arquivos e todas as opções: [docs/INSTALL.md](docs/INSTALL.md).
Para medir o seu PC, execute `BENCH.bat` (`./bench.sh`). Leva de 5 a 10 minutos e gera um relatório que você pode compartilhar
([como](docs/COMMUNITY_BENCHMARKS.md#run-the-benchmark)).
**Opcional: mais velocidade numa placa quando o modelo inteiro cabe na RAM.** Duas configurações opcionais do RubenTorney (#1904, #1905).
Elas ajudam quando você usa uma placa de vídeo e a sua RAM comporta todos os experts, por exemplo Q2_0 ou IQ3_XXS num PC de 64 GB. Abra
`strata-<model>.json` na pasta de dados do Strata ([onde fica](docs/INSTALL.md#where-things-are-stored)) e adicione:
- `"--adapt-async", "1"` à lista `"args"`. O cache de experts passa a ser atualizado numa thread auxiliar, em vez de pausar a
resposta. Medido numa RTX 5070 (Windows, Q2_0): +4 a 5% na geração, +7 a 10% em código e documentos longos. As respostas podem diferir
um pouco do padrão e continuam iguais de uma execução para outra.
- `"STRATA_PIN_BY_NEED": "1"` no bloco `"env"` (Windows). O Windows só consegue travar cerca de metade da RAM para a placa
de vídeo, e isso trava primeiro os experts de que a placa precisa. Medido na RTX 5070: +4,4% na leitura do prompt.
```json
"args": [ ..., "--adapt-async", "1" ],
"env": { "STRATA_PIN_BY_NEED": "1" }
```
Reinicie o Strata depois da alteração. Para desfazer, remova as duas entradas.
Siga as instruções para baixar e iniciar um modelo. [Guia do Terminal](docs/TERMINAL_LAUNCH.md).
</details>
## Como funciona?
<p align="center">
<img src="docs/media/terminal-monitor.png" width="900" alt="Aplicativo de Terminal do Strata mostrando a atividade do modelo, a velocidade, a memória e as solicitações recentes">
<br><sub>Acompanhe seu modelo e seu hardware enquanto ele roda.</sub>
</p>
Modelos como este costumam rodar em servidores com centenas de gigabytes de memória de vídeo. A sua placa de vídeo tem
12 a 24 GB. O Strata faz o modelo caber dividindo o trabalho entre o PC inteiro. Imagine uma biblioteca.
<a id="build-from-source"></a>
- O modelo é uma equipe de 24.576 pequenos especialistas ("experts"), como 24.576 livros de consulta. Cada palavra precisa de apenas 10 deles.
- **A sua placa de vídeo é a mesa.** Ela é pequena e rápida. Os cerca de 4.000 livros mais usados ficam abertos nela.
- **A sua RAM são as estantes da sala de leitura.** Os 24.576 livros estão lá. Quando uma palavra precisa de um livro que não está na
mesa, o seu processador lê da estante na hora, enquanto a placa continua trabalhando.
- **O seu SSD é o arquivo.** Ele guarda uma tabela de consulta de 29 GB. O modelo lê algumas linhas dela por palavra, então ela não
precisa ficar na RAM.
[Compilar a partir do código-fonte](docs/SOURCE_INSTALL.md) · [Docker](docs/DOCKER.md) · [Guia de instalação completo](docs/INSTALL.md)
<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>
<a id="what-you-need"></a>
Um pequeno ajudante também adivinha as próximas palavras. O modelo grande confere todas de uma vez e fica com as certas. Você recebe
a mesma resposta, de 1,6 a 1,8 vez mais rápido. Textos longos são lidos em blocos grandes, de até 8.192 tokens por vez, a mais de 1.000 tokens
por segundo.
## Hardware
<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>
| Hardware compatível | Guia |
| --- | --- |
| **NVIDIA GeForce RTX 20 / 30 / 40 / 50** | [Instalação para NVIDIA](docs/INSTALL.md#what-you-need) |
| **AMD Radeon RX 7700 / 7800 / 7900, RX 9060 / 9070, AI PRO R9700** | [Instalação para AMD](docs/AMD_HIP.md) |
| **AMD Radeon RX 6800 / 6900 · experimental** | [Instalação para RX 6000](docs/AMD_HIP.md#rdna2-gfx1030) |
| **Intel Arc · Linux** | [Instalação para Intel](docs/INTEL_ARC.md) |
| **Apple Silicon · M1 ou mais novo** | [Instalação para Mac](docs/MAC.md) |
| **AMD Ryzen AI Max · Strix Halo** | [Instalação para Strix Halo](docs/STRIX_HALO.md) |
A explicação mais longa: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Cada parte e seus números:
[os detalhes](docs/DETAILS.md#how-it-works) e o [artigo](docs/paper/Strata-Paper.pdf).
<a id="using-it"></a>
## Algo deu errado?
## Ferramentas
- **Meu PC travou na primeira vez que o Strata iniciou.** Ele carrega o modelo na RAM. Espere e não feche a janela. Ainda
travado depois de 10 minutos? Reinicie o PC, feche outros programas e tente de novo, ou escolha um tamanho menor.
- **Parou durante o download ou a instalação.** Abra o app ou o `strata launch` de novo, ou execute `START-HERE.bat` (ou
`./setup.sh`) outra vez. Ele continua de onde parou.
- **Está muito lento, a luz do disco não para de piscar, ou aparece "o mecanismo parou inesperadamente".** O seu PC não tem
RAM livre suficiente. Feche outros programas (os navegadores usam muita), ou escolha um tamanho menor, como Q2_0 ou IQ2_XS.
- **Diz que não há mecanismo para este PC.** No app, abra **Configurações, Mecanismo** e clique em **Atualizar mecanismo**. No
Terminal, execute `strata update`. O `strata doctor` mostra quais arquivos do mecanismo estão instalados.
- **Diz que a porta 8080 já está em uso.** O Strata já está rodando. Procure a janela dele.
**Claude Code · Codex CLI · OpenCode · oh-my-pi · pi.dev · Hermes Agent · OpenClaw**, e outras, pelas APIs compatíveis com OpenAI e Anthropic.
Mais problemas e soluções: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Ainda sem resolver? Abra uma
[issue](https://github.com/Niko1221/Strata/issues). No app, anexe o arquivo de **Configurações, Mecanismo, Diagnóstico, Ver
registros, Salvar logs**. No Terminal, cole a saída de `strata doctor`. Se estiver rodando pelo código-fonte, anexe o `strata-<model>.log` da pasta do
Strata. Para um problema de segurança, informe em particular: [SECURITY.md](SECURITY.md).
Execute **`strata connect`** para ver as instruções da sua ferramenta. [Guia de conexão](docs/TERMINAL_LAUNCH.md#connect-your-apps-strata-connect).
## Créditos e licença
<a id="guides"></a>
<a id="how-fast-is-it"></a>
<a id="how-does-it-work"></a>
<a id="something-went-wrong"></a>
O modelo é o [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next), da equipe Qwen. Ele foi comprimido por
[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5), agentionai (Gyro)
e Unsloth. O Strata usa partes do [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Todos os créditos:
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). O Strata é de código aberto sob a [Licença MIT](LICENSE). Algumas partes e
todos os modelos têm licenças próprias ([quais](docs/HOW_IT_WORKS.md#license)).
## Ajuda
## Contribuindo
Encontrou um bug, ou quer enviar uma alteração ou um benchmark? Leia primeiro o [CONTRIBUTING.md](CONTRIBUTING.md). Ele diz o que
incluir para que outras pessoas possam conferir o seu trabalho.
[Todos os guias](docs) · [Solução de problemas](docs/TROUBLESHOOTING.md) · [Relatar um bug](https://github.com/Niko1221/Strata/issues/new/choose) ·
[Benchmarks](docs/COMMUNITY_BENCHMARKS.md) · [Como funciona](docs/HOW_IT_WORKS.md) · [Contribuir](CONTRIBUTING.md)
## Apoie o Strata
O Strata é gratuito e tem licença MIT. Eu o desenvolvo e testo em todo hardware que consigo arranjar. Se ele roda bem no seu PC, um café me ajuda
a continuar testando placas novas e corrigindo o que você relatar. Sem pressão. Usar e relatar o que encontrar ajuda tanto quanto.
**Ajude a manter a IA local aberta para todos.** Um patrocínio no GitHub ou um café ajuda a financiar o desenvolvimento e os testes em mais hardware.
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a>
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor_on_GitHub-EA4AAA?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="GitHub Sponsors: apoie o Strata" height="48"></a>
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee: apoie o Strata" height="48"></a>
Quer patrocinar o Strata? Fale comigo pelo [GitHub](https://github.com/Niko1221).
### Apoiadores fiéis
Obrigado a todas as pessoas que apoiam o Strata.
<!-- supporters:start (one line per public supporter, newest last; copy a line and change the GitHub name in all four places) -->
<a href="https://github.com/ryan4yin"><img src="https://github.com/ryan4yin.png?size=96" width="48" height="48" alt="ryan4yin" title="ryan4yin"></a>
<a href="https://github.com/themactep"><img src="https://github.com/themactep.png?size=96" width="48" height="48" alt="themactep" title="themactep"></a>
<a href="https://github.com/macintog"><img src="https://github.com/macintog.png?size=96" width="48" height="48" alt="macintog" title="macintog"></a>
<a href="https://github.com/bisonbet"><img src="https://github.com/bisonbet.png?size=96" width="48" height="48" alt="bisonbet" title="bisonbet"></a>
<a href="https://github.com/vizv"><img src="https://github.com/vizv.png?size=96" width="48" height="48" alt="vizv" title="vizv"></a>
<!-- supporters:end -->
[Licença MIT](LICENSE) · [Créditos e licenças dos modelos](docs/HOW_IT_WORKS.md#credits) · [Privacidade](docs/SAFETY.md)
<a id="option-c-let-your-ai-set-it-up"></a>
<sub>Para ferramentas de IA e leitores automáticos: <a href="AGENTS.md">referência técnica</a> · <a href="docs/AI_SETUP.md">instruções de instalação</a>.</sub>
**Patrocínios, parcerias de hardware ou colaborações:** [[email protected]](mailto:[email protected]).
+135 -329
View File
@@ -1,396 +1,202 @@
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="apps/host/public/brand/strata-lockup-dark.svg">
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="380">
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="340">
</picture>
</p>
<p align="center"><a href="README.md">English</a> · <b>简体中文</b> · <a href="README.ja.md">日本語</a> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português</a></p>
<h3 align="center">在你自己的游戏电脑上运行最先进的模型</h3>
<p align="center">支持 NVIDIA、AMD、Intel · Windows、Linux、Mac · 免费开源</p>
<h3 align="center">在你自己的电脑上运行强大的 AI。</h3>
<p align="center">本地聊天、写代码、处理图片。</p>
<p align="center">
<a href="https://github.com/Niko1221/Strata/releases"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Latest release"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT license"></a>
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="GitHub stars"></a>
<a href="#模型">模型</a> · <a href="#安装"><b>安装</b></a> ·
<a href="#硬件">硬件</a> · <a href="#工具">工具</a> ·
<a href="#帮助">帮助</a> · <a href="#支持-strata">支持</a>
</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>一座体素宝塔花园,由一条提示词一次生成,在装有 RTX 5070 的电脑上用 Strata 运行(IQ3_S,128K 上下文) ·
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">完整视频(49 秒)</a></sub></p>
<p align="center">
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?label=download&color=0ab887" alt="下载 Strata"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT 许可证"></a>
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor-EA4AAA?logo=githubsponsors&logoColor=fff" alt="在 GitHub Sponsors 上支持 Strata"></a>
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy_me_a_coffee-FFDD00?logo=buymeacoffee&logoColor=000" alt="在 Buy Me a Coffee 上支持 Strata"></a>
</p>
Strata 运行 **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**。这是一个 1250 亿参数的模型,
通常需要服务器才能跑,现在用普通电脑就行。它能聊天、写代码、看图片,也能配合你的应用和编程智能体使用。
你的聊天记录和文件都留在你自己的电脑上。
<p align="center"><sub><a href="README.md">English</a> · <b>简体中文</b> · <a href="README.ja.md">日本語</a> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português</a> · <a href="README.pl.md">Polski</a></sub></p>
**第一次用?** 先看[硬件要求](#硬件要求),再[安装应用](#方式-astrata-应用)。
**免费、开源,由你自己运行。** 在桌面上使用 Strata,或搭配你喜欢的编程工具。无需账号。
- **游戏电脑就能跑。** 内存和显存加起来有 32 GB 就可以开始。
- **能接入你的工具。** 支持 OpenAI 和 Anthropic 的 API,Claude Code、Codex CLI、OpenCode 等智能体都能连上。
- **能看图片。** 默认开启。
- **免费。** MIT 许可证,不用注册账号,不走云端。
- **隐私。** 没有遥测。Strata 下载什么、什么时候联网、如何校验下载内容,都写在 [docs/SAFETY.md](docs/SAFETY.md)。
## 模型
版本 0.2.3 · 引擎 0.1.48
我们**首先推荐 ISTA-DASLab**,其次是 **Unsloth** 和其他提供方,前提是有内存放得下的兼容模型。
## 硬件要求
<details>
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> &nbsp;<b>Qwen3.8-Flash-Next</b></summary>
**内存和显存(VRAM)加起来至少要有 32 GB。** Mac 和 Ryzen AI Max 上指的是统一内存。内存越大,能跑的模型越大,
或者速度越快。
- 32 GB 内存加一张 12-16 GB 显存的显卡,可以跑 Coder。
- 64 GB 内存可以跑所有尺寸。
| 你的电脑 | 说明 |
| 提供方 | 可选项 |
| --- | --- |
| **NVIDIA** GeForce RTX 20、30、40、50 系列(12 GB 及以上) | 安装应用或终端版即可。 |
| **AMD** Radeon RX 6800 及更新型号、Radeon AI PRO R9700(12 GB 及以上) | [AMD 显卡](docs/AMD_HIP.md) |
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md)、[Intel 引擎](docs/INTEL.md) |
| **Apple 芯片 Mac**(M1 及更新,macOS 15 及以上) | [Mac](docs/MAC.md) |
| **AMD Ryzen AI Max(Strix Halo)** 及其他统一内存系统 | [Strix Halo](docs/STRIX_HALO.md) |
| 较旧的显卡和处理器 | [旧显卡](docs/OLDER_GPUS.md)、[旧处理器](docs/INSTALL.md#older-cpus-experimental) |
| **ISTA-DASLab · 推荐** | Q2\_0, IQ2\_XS, IQ3\_XXS, IQ3\_S。[Coder:IQ1\_M](docs/MODELS.md#coder),适合写代码。 |
| **Unsloth** | UD-IQ4\_XS, UD-Q4\_K\_XL, UD-Q5\_K\_XL, UD-Q6\_K\_XL, Q8\_0 |
| **Gyro · agentionai** | [Gyro-S, Gyro-M](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF)。占用内存更少,但速度更慢。 |
| **其他选项** | [Swift 1.5](docs/MODELS.md#swift-15), [NVFP4](docs/NVFP4.md), [OrcaRouter](docs/ORCA.md) |
另外还需要:
[比较大小、内存需求和兼容性](docs/MODELS.md)。
- 约 80 GB 的可用磁盘空间(用固态硬盘,首次启动会快很多)
- Windows 10 / 11、Linux 或 macOS
- 较新的显卡驱动
</details>
同一品牌的两三张显卡可以共同运行一个模型([多显卡](docs/MULTI_GPU.md))。Strata 不会把一个模型拆到 NVIDIA 和 AMD
显卡上。完整列表见 [docs/INSTALL.md](docs/INSTALL.md#what-you-need)。
<details>
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> &nbsp;<b>Qwen3.6-35B-A3B</b></summary>
**Unsloth:** UD-Q4_K_XL, UD-Q6_K_XL, Q8_0。这些是目前文档中收录的该模型的版本。
[模型指南和兼容性](docs/QWEN36.md)。
</details>
<details>
<summary><img src="docs/media/models/deepseek.svg" width="22" alt="" align="middle"> &nbsp;<b>DeepSeekV4.1-Flash-1017</b></summary>
可在**桌面应用**和**终端**中使用。内存放不下的部分会从 SSD 读取,所以需要大量空闲磁盘空间。
| 版本(ISTA-DAS) | 下载大小 | 所需空闲磁盘 |
| --- | ---: | ---: |
| 1.75 bpw | 239 GB | 约245 GB |
| 2.00 bpw | 257 GB | 约265 GB |
| 2.25 bpw | 274 GB | 约280 GB |
支持 NVIDIA RTX 30、40 和 50 系列(Windows 和 Linux),以及 AMD Radeon AI PRO R9700(Linux)。安装程序会下载模型文件和 DeepSeek 引擎,然后启动模型。
```sh
strata launch --family deepseek41 --model ist-das-2.00bpw --plan
```
以下为 Strata 0.2.5 上 2.00 bpw 版本的实测结果。“输出”是回答的速度,“提示词”是 Strata 读取你输入内容的速度。
| 硬件 | 输出 | 提示词 |
| --- | ---: | ---: |
| RTX 5070 12 GB, 64 GB 内存 | 4 到 6 tokens/s | 约 110 tokens/s(4K) |
| RTX 3060 12 GB, 46 GB 内存 | 9 到 11 tokens/s | 约 155 tokens/s(4K) |
| 1x Radeon AI PRO R9700, 251 GB 内存 | 21 到 25 tokens/s | 320 到 360 tokens/s (8K) |
| 4x Radeon AI PRO R9700 | 74 到 77 tokens/s | 400 到 450 tokens/s (8K) |
[内存、磁盘和详细信息](docs/DEEPSEEK41.md) · [发行说明](https://github.com/Niko1221/Strata/releases)
</details>
<a id="the-other-sizes"></a>
不确定哪个放得下?[为你的电脑选择模型](docs/MODELS.md#pick-by-memory)。
## 安装
三种方式任选一种。它们启动的是同一个引擎,用的是同样的模型。
<a id="option-a-the-strata-app"></a>
### 方式 A:Strata 应用
### 桌面应用
最简单的方式。应用内置引擎:安装、选模型、开始聊天。请根据你的系统和显卡,从
[发布页](https://github.com/Niko1221/Strata/releases/latest)下载对应的文件。不确定选哪个?看表格下面那一段。
**[下载适用于 Windows、Linux 或 Mac 的 Strata →](https://github.com/Niko1221/Strata/releases/latest)**
| 系统 | 你的显卡 | 要下载的文件 |
| --- | --- | --- |
| **Windows 10 / 11** | NVIDIA RTX,驱动 580 或更新 | [`Strata-0.2.3-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda13.exe) |
| | NVIDIA,较旧的显卡,或驱动低于 580 | [`Strata-0.2.3-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda12.exe) |
| | AMD Radeon RX 9000 系列或 Radeon AI PRO R9700 | [`Strata-0.2.3-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip.exe) |
| | AMD Radeon RX 7000 系列或 Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip-rdna3.exe) |
| **Linux** | NVIDIA RTX,驱动 580 或更新 | [`Strata-0.2.3-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda13.AppImage) |
| | NVIDIA,较旧的显卡,或驱动低于 580 | [`Strata-0.2.3-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda12.AppImage) |
| | AMD Radeon RX 9000 系列或 Radeon AI PRO R9700 | [`Strata-0.2.3-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx9070.AppImage) |
| | AMD Radeon RX 7000 系列 | [`Strata-0.2.3-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx7900.AppImage) |
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-strixhalo.AppImage) |
| **Mac**(Apple 芯片,macOS 15 及以上) | 内置 | [`Strata-0.2.3-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-mac.dmg) |
安装应用,选择模型,就可以开始聊天。每个系统只有一个安装程序。首次启动时,应用会下载适合你显卡的引擎。[安装程序帮助](docs/INSTALL.md#the-app)。
**不确定时:** NVIDIA 显卡的驱动版本在 580 及以上就选 `cuda13`,否则选 `cuda12`。`cuda12` 适用于较旧的显卡和驱动。
`hip` 和 `amd` 的文件是给 AMD 显卡用的。其他 AMD 显卡(RX 6000)目前还没有现成的应用,
请看 [AMD 显卡](docs/AMD_HIP.md)。
<sub>文件: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
然后:
<p align="center">
<img src="docs/media/app-chat.png" width="900" alt="Strata 桌面应用,聊天和实时代码预览并排显示">
<br><sub>聊天、文件和实时预览都在一个窗口里。</sub>
</p>
- **Windows:** 运行 `.exe`。它只为当前用户安装,不需要管理员权限。如果 Windows 提示“Windows 已保护你的电脑”,
点击**更多信息**,再点**仍要运行**(安装程序还没有代码签名)。
- **Linux:** 给文件加上可执行权限(`chmod +x`),然后启动。需要 FUSE 2:`sudo apt install libfuse2`(Ubuntu 22.04),
或 `libfuse2t64`(Ubuntu 24.04 及更新)。
- **Mac:** 打开 `.dmg`,把 Strata 拖到“应用程序”。
<a id="option-b-the-terminal-one-line"></a>
第一个界面里,应用会检测你的电脑,并推荐合适的模型。点击安装即可。模型很大,要下载约 40-70 GB,中断后会从断点继续。
关闭窗口会同时停止 Strata 和引擎。
### 终端应用
### 方式 B:终端(一行命令)
更喜欢终端?安装 Strata,然后运行 **`strata launch`**。
终端版就是 `strata` 命令,适合脚本、服务器和 AI 智能体。
<details open>
<summary><b>安装命令</b></summary>
Linux 和 Mac:
**Linux / Mac**
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
```
Windows(PowerShell):
**Windows PowerShell**
```powershell
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
```
脚本会检查你的电脑,下载终端版,校验 SHA-256,然后解压到你的用户目录。不需要管理员权限。在 Windows 上,装完后请
打开一个新的终端窗口。然后启动 Strata:
在 Windows 上,安装后请打开一个新的终端。然后运行:
```sh
strata launch
```
它会一次问一个问题:模型、上下文、图片、允许谁连接。接着为你的显卡下载引擎(已签名,使用前会先试启动),再下载
模型,并开始提供服务。在脚本和 AI 智能体里,请使用参数:
```sh
strata launch --model q2_0 --yes --no-tui
```
如果想在运行前先看看安装脚本,先用 `curl -fsSL <address> -o install.sh` 下载,读完后再用 `sh install.sh` 运行。
所有界面和参数见 [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md)。
### 方式 C:让你的 AI 来安装
把下面这段话粘贴到 Claude Code、Cursor、Codex、GitHub Copilot 或其他编程助手里:
```text
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
```
它会检查你的显卡、内存和磁盘,选出合适的模型,完成安装和启动,并告诉你怎么连接你的应用。AI 工具也可以通过 Strata 的
[MCP 服务器](docs/MCP_SERVER.md)安装、启动和停止 Strata。
想自己动手编译?请看[从源码构建](#从源码构建)。
## 模型
应用和 `strata launch` 会按你的内存推荐模型。它是同一个模型的几种尺寸,压缩程度不同。尺寸小的更快,尺寸大的稍微更聪明一点。
### 内存不够?试试 Gyro
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF)(作者 agentionai)是运行这个模型占用最小的方式。
| 尺寸 | 所需内存(RAM + VRAM) | 下载大小 | 说明 |
| --- | ---: | ---: | --- |
| **Gyro-S** | 27.6 GB | 58.5 GB | 给其他尺寸都放不下的电脑。质量低于 IQ2_XS。 |
| **Gyro-M** | 35 GB | 92.0 GB | 质量约相当于 IQ3_XXS,显存占用少约 20%。输出回答比 Q2_0 慢,12 GB 的 RTX 3060 上慢 16%,R9700 上慢 21%。 |
Gyro 可在 NVIDIA 和 AMD 显卡(AMD 为实验性支持)以及 Linux 上的 Intel Arc 显卡(已在 Arc A750 上测试)上运行,自 0.2.2 起也可在 Apple Silicon 的 Mac 上运行(32 GB 的 Mac mini 上 Gyro-S 为 9 token/秒,专家权重从 SSD 读取)。详情和测试数据:
[docs/GYRO.md](docs/GYRO.md)。用 `strata launch --model gyro-s` 启动。
### 其他尺寸
| 你的内存(RAM + VRAM) | 选择 | 备注 |
| --- | --- | --- |
| **32 GB** | **Gyro-S**,写代码可选 **[Coder](docs/MODELS.md#coder)** | Coder 需要 32 GB 内存,只保留一半的专家,所以代码以外的能力较弱 |
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS(39.2 GB),或 **Gyro-M**(35 GB) | Swift 回答前思考的时间更短。Gyro-M 的质量接近 IQ3_XXS,但输出回答比 Q2_0 慢 16-21% |
| **48 GB** | **IQ3_XXS**(47.0 GB) | 更慢,更好 |
| **64 GB** | **IQ3_S**(54.8 GB),原版或 Swift 1.5 | 最慢,最好:在公开测试中与完整模型持平 |
| **96 GB 及以上** | Unsloth 的 **UD-IQ4_XS**(约 72 GB 内存)到 **Q8_0**(约 136 GB),4 到 8 位 | 更接近完整模型。内存不足时,Strata 会从固态硬盘读取一部分,速度会慢很多 |
内存比这一行写的少,也可能能用:Strata 会自动切换到[低内存模式](docs/MODELS.md#a-big-graphics-card-and-little-ram)。
速度会变慢,显卡较小时会慢得多。
更多选择:
- **[Swift 1.5](docs/MODELS.md#swift-15)** 是微调版本,回答前思考的时间更短,质量和内存占用大致相同。
- **[NVFP4](docs/NVFP4.md)** 是 NVIDIA 为 NVIDIA 显卡提供的 4 位版本。要把所有专家放进内存,需要约 80 GB 内存。
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** 是另一个更小的模型,能看图片,还能同时回答多个请求。
- **[OrcaRouter 的 Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** 需要手动设置。
所有尺寸、下载大小,以及各种配置能跑什么:[docs/MODELS.md](docs/MODELS.md)。
### DeepSeek V4.1-Flash(终端版,0.2.4 起)
[DeepSeek V4.1-Flash](https://huggingface.co/ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF) 是另一个大得多的模型。下面三个版本由 ISTA-DASLab 制作。Strata 在一台有一块 12 GB 显卡和 46 到 64 GB 内存的电脑上运行它们,其余部分从固态硬盘读取。它可在 NVIDIA RTX 30 到 50 系列显卡和 Radeon AI PRO R9700 上运行。它使用自己的引擎包 `strata-dsv41`,你选择 DeepSeek 时由 `strata launch` 下载。桌面应用暂时不提供它。
| 版本 | 下载大小 | 与原模型相比的质量 | 生成速度 |
| --- | ---: | --- | --- |
| **IQ1_M**(1.75 位) | 239 GB | KL 0.217,困惑度 3.80(+21%);代码 +8.9%,智能体任务 +10.2%;普通英文散文表现较弱。据 ISTA-DASLab 的模型卡,它保留了基础模型任务平均分的 98.7%(AIME25、GPQA-Diamond、LiveCodeBench v6)。 | 尚未测量 |
| **IQ2_XXS** (2.00 位) | 257 GB | KL 0.170,困惑度 3.63(+15.5%);代码 +8.6%,智能体任务 +5.3% | RTX 3060 上每秒 6 到 9 个 token,RTX 5070 上约 5 个 |
| **IQ2_XS** (2.25 位) | 274 GB | KL 0.107,困惑度 3.37(+7.3%);代码 +3.2%,智能体任务 +2.2% | 一块 Radeon AI PRO R9700 上每秒约 11 个 token |
| **IQ4_NL**(Engram 表,不是第三个模型) | 111 GB,已包含在上面三个下载中 | 模型从内存或固态硬盘读取的 4 位 Engram 查找表。三个版本共用这一个文件(`DeepSeek-V4.1-Flash-Engram-IQ4_NL.gguf`)。 | 不适用 |
4 块 Radeon AI PRO R9700:每秒约 31 个 token,使用 DSpark 时为 55 到 69。
用 `strata launch --model ist-das-2.00bpw`(或 `ist-das-1.75bpw`、`ist-das-2.25bpw`)启动。详情、各显卡的速度和手动安装见 [docs/DEEPSEEK41.md](docs/DEEPSEEK41.md)。
## 速度如何?
以下数据基于 12 GB 显卡(RTX 5070、Ryzen 5 7600、64 GB 内存)。其他显卡的结果见:
[各模型的速度](docs/MODELS.md#how-fast-is-each-size)、[社区测试结果](docs/COMMUNITY_BENCHMARKS.md)。
| 模型 | 回答输出速度 | 读取提示词速度 |
| --- | --- | --- |
| **Q2_0** | `████████████` 94 tokens/s | `████████████` 2,650 tokens/s |
| **IQ2_XS** | `██████████░░` 79 tokens/s | `█████████░░░` 2,090 tokens/s |
| **IQ3_XXS** | `████████░░░░` 62 tokens/s | `████████░░░░` 1,750 tokens/s |
| **IQ3_S** | `███████░░░░░` 53 tokens/s | `███████░░░░░` 1,620 tokens/s |
| **Coder** | `███████░░░░░` 55 tokens/s | `██████████░░` 2,180 tokens/s |
一个 token 约等于 ¾ 个英文单词,所以每秒 60 个 token 比你阅读的速度还快。“读取提示词”指 Strata 吸收长文档、代码或聊天历史的速度。
在 16 GB 的 AMD 显卡上测得的结果(RX 9070 XT、Ryzen 9 3900X、47 GB 内存):
| 模型 | 回答输出速度 | 读取提示词速度 |
| --- | --- | --- |
| **Q2_0** | `████████░░░░` 60 tokens/s | `█████░░░░░░░` 1,160 tokens/s |
| **IQ2_XS** | `███████░░░░░` 52 tokens/s | `█████░░░░░░░` 1,110 tokens/s |
| **Coder** | `██████░░░░░░` 44 tokens/s | `██████░░░░░░` 1,420 tokens/s |
两张表的进度条使用同一个比例。显存更大的显卡更快:RTX 3090(24 GB)每秒大约能输出 100-140 个 token。每个数字对应的引擎版本见
[DETAILS.md](docs/DETAILS.md#speed-measured)。
## 使用方法
### 在应用里
应用包含**聊天**、显示模型以及 GPU、CPU 和内存状态的**监控**、**模型**页面和**设置**。聊天可以在你电脑上的某个文件夹里工作。
在**手动**模式下,修改文件或运行命令之前它会先询问你。输入 `/loop 10m <要做的事>`,在聊天保持打开期间每 10 分钟重复发送一次消息。
聊天可以用 DuckDuckGo(或 Bing)搜索网页,不需要账号。你也可以在**设置,工具**里添加搜索密钥。搜索是唯一会把内容发出电脑的功能,
发出的就是搜索词。
要同时运行两个模型,请打开**运行多个模型**,并为每个模型分配同一品牌的单独一张显卡。
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="The Strata app: a chat where the agent built a small voxel pagoda garden, with the page open in the preview on the right"><br>
<sub>Strata 应用中的一次对话:智能体用一个 HTML 文件写出了一个小型体素宝塔花园,页面在对话旁的预览中打开</sub></p>
### 在终端里
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
<sub>模型回复时的 <code>strata monitor</code>:速度、GPU、内存和最近的请求</sub></p>
```sh
strata launch # 引导界面:模型、上下文、图片、访问方式,然后开始服务
strata launch --model q2_0 --yes --no-tui # 不提问:开始服务,打印地址,然后返回
strata launch --plan --model q2_0 # 显示将要执行的操作;不做任何更改
strata launch --status # 当前正在提供什么服务
strata launch --stop # 停止服务
strata monitor --url http://127.0.0.1:8080 # 查看速度、GPU、内存和请求
strata update --check # 有新引擎吗?(strata update 会安装它)
strata doctor # 检查已安装的引擎文件
```
`strata --help` 会列出所有命令。启动和提供服务时不会下载任何东西。只有安装、`strata launch`(在你选择模型时)和
`strata update` 会下载。要从手机或另一台电脑访问 Strata,请使用 `strata launch --access lan`。它会生成一个 API 密钥,
没有密钥就不会在网络上启动。除非你加上 `--open`,否则不会打开浏览器。指南:[docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md)。
完整的客户端说明:[docs/TERMINAL.md](docs/TERMINAL.md)。
### 配合 AI 智能体
智能体用方式 B 中的一行命令安装 Strata,然后运行 `strata launch --model NAME --yes --no-tui`。步骤、参数和退出码见
[docs/AI_SETUP.md](docs/AI_SETUP.md),仓库规则见 [AGENTS.md](AGENTS.md)。
### 连接你的其他应用
| 应用类型 | 基础 URL |
| --- | --- |
| 兼容 OpenAI(大多数应用和智能体) | `http://127.0.0.1:8080/v1`(API 密钥和模型名称随便填) |
| Anthropic API(Claude Code 等) | `http://127.0.0.1:8080`,不带 `/v1`。这些应用会自己加上 `/v1/messages`。Claude Code:`ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
| OpenAI Responses API(Codex CLI) | `http://127.0.0.1:8080/v1/responses`([设置方法](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
`strata connect` 会打印 OpenCode、Codex CLI、Claude Code、oh-my-pi、pi、Hermes Agent 和 OpenClaw 的具体设置。
`strata connect opencode --write` 会在询问后写入配置文件,并保留备份。
### 图片
能看图片的模型默认开启图片功能。在聊天里附加或粘贴图片,或者从你的应用发送图片。显卡的显存小于 24 GB 时,读图由处理器完成
(每张图片几秒钟,不占用显卡的显存);显存达到 24 GB 时,由显卡完成。NVIDIA 和 AMD 的规则相同。你可以按模型关闭图片功能,
也可以用 `--vision off`、`--vision gpu` 或 `--vision cpu` 选择读图方式。
这条规则背后的数据:在 Radeon AI PRO R9700 上,显卡读一张 260 token 的图片用了 57 ms,处理器用了 3.1 秒。在 16 GB 的显卡
(Tesla P100 和 RTX A4000,Linux)上,用显卡读图会让文字速度下降 3-5%,因为它占用约 1.4 GB 显存,否则这部分会被模型使用。
在 Mac 上,无论内存多大,都由 GPU 读图(32 GB 的 M4 上读一张 260 token 的图片用 0.45 秒,占用约 1.2 GB 共享内存);处理器读一张约需 50 秒,所以只有指定 `--vision cpu` 时才用。
### 使用须知
- 在聊天菜单或你的应用的“reasoning effort”里选择思考级别(**off、low、medium 或 high**)。off 最快。
- Strata 默认一次回答一个请求。要同时处理多个,请在 `strata launch` 中使用 `--parallel N`,或在配置里写
`"parallel": 2`([BATCHING.md](docs/BATCHING.md))。在 12 GB 显卡上,这会让每个回答变慢。
- Strata 会完整读取聊天的第一条消息,每 30,000 个 token 约需 1 分钟。之后的消息几秒钟就开始。
- 聊天记录存放的位置和完整的 API:[docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored)、[docs/DETAILS.md](docs/DETAILS.md#using-it)。
## 从源码构建
<details>
<summary>从仓库构建 Windows、Linux 和 Mac 版本</summary>
[下载 Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip)并解压,或者用 `git clone` 获取。
**Windows:** 双击 `START-HERE.bat`。**Linux:** 在 Strata 文件夹里运行 `./setup.sh`。
**Mac(Apple 芯片,macOS 15 及以上):** 先用 `xcode-select --install` 安装一次 Apple 的命令行工具(不需要完整的 Xcode)。
在 Strata 文件夹里打开终端,运行 `./setup.sh`。它会在你的 Mac 上构建 Mac 引擎(约 7 分钟),问同样的问题,下载模型并启动。
下次运行 `./setup.sh` 会直接启动。你的 Mac 适合哪个尺寸:[docs/MAC.md](docs/MAC.md)。
安装程序会找到你的显卡,并设置合适的引擎。它会问你用哪个模型和尺寸、上下文多长(模型能记住多少文字),以及是否要读取图片。
直接按回车就是推荐的选择。然后它会下载模型(约 70 GB)并启动。如果下载中断,再运行一次就会继续。浏览器会打开
`http://127.0.0.1:8080` 上的 Strata 页面。
> 模型启动期间,你的电脑可能会变慢,或者在 1-3 分钟内没有响应(第一次最久)。Strata 会把 35-55 GB 加载到内存里,并锁定其中一部分
> 给显卡使用。请等待,不要关闭窗口。
下次再运行 `START-HERE.bat`(或 `./setup.sh`)会立即启动,不会重复下载。关闭它的窗口就能停止模型。`UPDATE.bat`
(`./update.sh`)只更新 Strata,不启动。更新、Docker、多显卡、文件存放位置和所有选项:[docs/INSTALL.md](docs/INSTALL.md)。
要测试你的电脑,运行 `BENCH.bat`(`./bench.sh`)。需要 5-10 分钟,并生成一份可以分享的报告
([方法](docs/COMMUNITY_BENCHMARKS.md#run-the-benchmark))。
**可选:整个模型都放得进内存时,让单卡更快。** 这是 RubenTorney 提供的两个需要手动开启的设置(#1904、#1905)。
当你只用一张显卡,并且内存装得下所有专家时,它们会有帮助,例如在 64 GB 的电脑上跑 Q2_0 或 IQ3_XXS。打开 Strata 数据文件夹里的
`strata-<model>.json`([位置](docs/INSTALL.md#where-things-are-stored)),然后添加:
- 在 `"args"` 列表中加入 `"--adapt-async", "1"`。这样专家缓存会在辅助线程上更新,不会让回答暂停。在 RTX 5070
(Windows,Q2_0)上测得:解码快 4-5%,代码和长文档快 7-10%。回答可能与默认设置略有不同,但每次运行的结果保持一致。
- 在 `"env"` 块中加入 `"STRATA_PIN_BY_NEED": "1"`(Windows)。Windows 只能为显卡锁定大约一半的内存,
这个设置会优先锁定显卡最先需要的专家。在 RTX 5070 上测得:读取提示词快 4.4%。
```json
"args": [ ..., "--adapt-async", "1" ],
"env": { "STRATA_PIN_BY_NEED": "1" }
```
修改后请重启 Strata。要撤销,删除这两项即可。
按提示下载并启动模型。[终端指南](docs/TERMINAL_LAUNCH.md)。
</details>
## 工作原理
<p align="center">
<img src="docs/media/terminal-monitor.png" width="900" alt="Strata 终端应用,显示模型活动、速度、内存和最近的请求">
<br><sub>模型运行时,随时查看模型和硬件状态。</sub>
</p>
这类模型通常运行在有几百 GB 显存的服务器上。你的显卡只有 12-24 GB。Strata 把工作分给整台电脑,让模型装得下。
可以把它想成一座图书馆。
<a id="build-from-source"></a>
- 模型是一个由 24,576 位小专家(“experts”)组成的团队,好比 24,576 本参考书。每个词只需要其中 10 本。
- **你的显卡是书桌。** 它小而快。最常用的大约 4,000 本书摊开放在上面。
- **你的内存是阅览室的书架。** 24,576 本书都在这里。某个词需要一本不在书桌上的书时,处理器会立刻从书架上取来读,显卡则继续工作。
- **你的固态硬盘是档案室。** 里面存着一张 29 GB 的查找表。模型每个词只读其中几行,所以不用常驻内存。
[从源码构建](docs/SOURCE_INSTALL.md) · [Docker](docs/DOCKER.md) · [完整安装指南](docs/INSTALL.md)
<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>
<a id="what-you-need"></a>
另外有一个小助手会猜接下来的几个词。大模型一次检查所有猜测,保留正确的部分。得到的答案不变,但快了 1.6-1.8 倍。
长文本按大块读取,一次最多 8,192 个 token,速度超过每秒 1,000 个 token。
## 硬件
<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>
| 支持的硬件 | 指南 |
| --- | --- |
| **NVIDIA GeForce RTX 20 / 30 / 40 / 50 系列** | [NVIDIA 安装](docs/INSTALL.md#what-you-need) |
| **AMD Radeon RX 7700 / 7800 / 7900、RX 9060 / 9070、AI PRO R9700** | [AMD 安装](docs/AMD_HIP.md) |
| **AMD Radeon RX 6800 / 6900 · 实验性** | [RX 6000 安装](docs/AMD_HIP.md#rdna2-gfx1030) |
| **Intel Arc · Linux** | [Intel 安装](docs/INTEL_ARC.md) |
| **Apple Silicon · M1 或更新** | [Mac 安装](docs/MAC.md) |
| **AMD Ryzen AI Max · Strix Halo** | [Strix Halo 安装](docs/STRIX_HALO.md) |
更详细的解释:[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md)。每个部分和它的数据:
[详细说明](docs/DETAILS.md#how-it-works)和[论文](docs/paper/Strata-Paper.pdf)。
<a id="using-it"></a>
## 出问题了?
## 工具
- **第一次启动 Strata 时我的电脑卡住了。** 它在把模型加载到内存。请等待,不要关闭窗口。10 分钟后还是卡住?重启电脑,
关闭其他程序后再试,或者选一个更小的尺寸。
- **下载或安装时中断了。** 重新启动应用或 `strata launch`,或者再运行一次 `START-HERE.bat`(或 `./setup.sh`)。
它会从中断的地方继续。
- **非常慢、硬盘灯一直闪,或者提示“引擎意外停止”。** 你的电脑可用内存不够。关闭其他程序(浏览器很占内存),
或者选一个更小的尺寸,比如 Q2_0 或 IQ2_XS。
- **提示这台电脑没有可用的引擎。** 在应用里打开**设置,引擎**,点击**更新引擎**。在终端里运行 `strata update`。
`strata doctor` 会显示已安装哪些引擎文件。
- **提示 8080 端口已被占用。** Strata 已经在运行了。找一下它的窗口。
**Claude Code · Codex CLI · OpenCode · oh-my-pi · pi.dev · Hermes Agent · OpenClaw**,也可以通过兼容 OpenAI 和 Anthropic 的 API 使用更多工具。
更多问题和解决办法:[docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)。还是解决不了?请提交
[issue](https://github.com/Niko1221/Strata/issues)。在应用里,附上**设置,引擎,诊断,查看日志,保存日志**得到的文件。
在终端里,粘贴 `strata doctor` 的输出。从源码运行时,附上 Strata 文件夹里的 `strata-<model>.log`。
如果是安全问题,请私下报告:[SECURITY.md](SECURITY.md)。
运行 **`strata connect`**,查看你所用工具的设置方法。[连接指南](docs/TERMINAL_LAUNCH.md#connect-your-apps-strata-connect)。
## 致谢与许可证
<a id="guides"></a>
<a id="how-fast-is-it"></a>
<a id="how-does-it-work"></a>
<a id="something-went-wrong"></a>
模型是 Qwen 团队的 [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)。压缩工作由
[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF)、UkisAI(Swift 1.5)、agentionai(Gyro)
和 Unsloth 完成。Strata 使用了 [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp) 的部分代码。全部致谢:
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits)。Strata 以 [MIT 许可证](LICENSE)开源。少数部分和所有模型有各自的许可证
([具体是哪些](docs/HOW_IT_WORKS.md#license))。
## 帮助
## 参与贡献
发现了 bug,或者想提交修改或基准测试结果?请先阅读 [CONTRIBUTING.md](CONTRIBUTING.md)。其中说明了需要附上哪些内容,
方便其他人核实你的工作。
[全部指南](docs) · [故障排查](docs/TROUBLESHOOTING.md) · [报告问题](https://github.com/Niko1221/Strata/issues/new/choose) ·
[基准测试](docs/COMMUNITY_BENCHMARKS.md) · [工作原理](docs/HOW_IT_WORKS.md) · [参与贡献](CONTRIBUTING.md)
## 支持 Strata
Strata 免费,采用 MIT 许可证。我在能拿到的各种硬件上开发和测试它。如果它在你的电脑上跑得不错,请我喝杯咖啡,能帮我继续测试新显卡、
修复你们反馈的问题。不用有压力。使用它并反馈你的发现,同样很有帮助。
**帮助本地 AI 保持开放。** GitHub 赞助或一杯咖啡,都能帮助我们在更多硬件上开发和测试。
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a>
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor_on_GitHub-EA4AAA?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="GitHub Sponsors:支持 Strata" height="48"></a>
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee:支持 Strata" height="48"></a>
有兴趣赞助 Strata?请通过 [GitHub](https://github.com/Niko1221) 联系我。
### 忠实支持者
感谢每一位支持 Strata 的人。
<!-- supporters:start (one line per public supporter, newest last; copy a line and change the GitHub name in all four places) -->
<a href="https://github.com/ryan4yin"><img src="https://github.com/ryan4yin.png?size=96" width="48" height="48" alt="ryan4yin" title="ryan4yin"></a>
<a href="https://github.com/themactep"><img src="https://github.com/themactep.png?size=96" width="48" height="48" alt="themactep" title="themactep"></a>
<a href="https://github.com/macintog"><img src="https://github.com/macintog.png?size=96" width="48" height="48" alt="macintog" title="macintog"></a>
<a href="https://github.com/bisonbet"><img src="https://github.com/bisonbet.png?size=96" width="48" height="48" alt="bisonbet" title="bisonbet"></a>
<a href="https://github.com/vizv"><img src="https://github.com/vizv.png?size=96" width="48" height="48" alt="vizv" title="vizv"></a>
<!-- supporters:end -->
[MIT 许可证](LICENSE) · [致谢和模型许可证](docs/HOW_IT_WORKS.md#credits) · [隐私](docs/SAFETY.md)
<a id="option-c-let-your-ai-set-it-up"></a>
<sub>面向 AI 工具和自动阅读器: <a href="AGENTS.md">技术参考</a> · <a href="docs/AI_SETUP.md">安装说明</a></sub>
**赞助、硬件合作或其他合作:** [[email protected]](mailto:[email protected]).
+4 -3
View File
@@ -23,8 +23,9 @@ this PC only. The details and every setting are in [docs/DETAILS.md](docs/DETAIL
- **Where it listens.** `127.0.0.1` by default. `--host 0.0.0.0` (or `"host"` in `strata-<model>.json`) opens it to
your network, and the server then warns when no API key is set.
- **API key.** `"api_key"` in the run config (or `STRATA_API_KEY`) is required on `/v1/*` and on every endpoint
that shows the model's state, requests or answers (`/status`, `/metrics`, `/settings`, `/mcp`, `/props`, `/slots`,
`/api/requests`, `/config`) and on every `POST`. It is compared in constant time. Set one before you open the
that shows the model's state, requests or answers (`/status`, `/metrics`, `/metrics/prometheus`, `/settings`,
`/mcp`, `/props`, `/slots`, `/api/requests`, `/config`) and on every `POST`. It is compared in constant time.
Set one before you open the
server to your network or put a tunnel in front of it.
- **Host check (DNS rebinding, 0.1.38).** Without an API key the server answers only requests whose `Host` is a name
it knows (`localhost`, an IP address, the address it listens on and, when it listens beyond this PC, this PC's
@@ -74,7 +75,7 @@ the model. The section above is about the engine's HTTP server. This is what the
The Terminal installs with one line:
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
```
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "strata-desktop",
"productName": "Strata",
"version": "0.2.3",
"version": "0.2.12",
"private": true,
"description": "Strata Desktop - local AI, tuned to one model.",
"scripts": {
+228
View File
@@ -0,0 +1,228 @@
#!/usr/bin/env python3
"""Is every static-PIE program inside an AppImage still the program it was staged as? (#4337)
Why: the bundled ripgrep (`harness/node_modules/@vscode/ripgrep-linux-x64/bin/rg`) is a statically linked PIE. Such a program relocates itself at
start-up by reading its own `.dynamic` section through a fixed offset in its code. linuxdeploy, which Tauri runs over the AppDir, treats every ELF file
as a library to fix up and rewrites its RPATH; for a static PIE that moves `.dynamic` to a new segment and leaves the old place filled with a filler
byte, so the start-up code reads garbage. Reporters on #4337 see the same crash offset in every run (`rg + 0x3ffcf1`) and filler bytes (0x5A) where
`.rela.dyn` and `.dynamic` were. A static PIE whose `.dynamic` is filled does not start: tools/test_appimage_elf.py shows that on a real one.
apps/desktop/scripts/repack-appimage.sh puts the staged bytes back; this file is the check that says whether it did.
appimage_elf.py scan <folder> every static PIE under <folder> must have a sound .dynamic (exit 1 if not)
appimage_elf.py compare <staged-folder> <folder> the same files, byte for byte, plus what differs in their ELF headers
What is checked: only static PIE programs. A shared library (.so, Python's and Node's native modules, Pillow, numpy, sharp) is not one, however it is
named: it has no entry point, or a DT_SONAME and no DF_1_PIE flag, and its RPATH/RUNPATH is normal. A static PIE is an ET_DYN with no interpreter, an entry
point, and DF_1_PIE in DT_FLAGS_1; one whose .dynamic cannot be read at all (overwritten) is reported as damaged, since the overwrite takes the flags too.
Standard library only; reads ELF64 little endian (x86-64 and arm64), anything else is ignored.
"""
import os
import struct
import sys
ET_DYN = 3
PT_LOAD, PT_DYNAMIC, PT_INTERP = 1, 2, 3
DT_NULL, DT_NEEDED, DT_RELA, DT_RELASZ, DT_RELAENT, DT_SONAME, DT_RPATH, DT_RUNPATH = 0, 1, 7, 8, 9, 14, 15, 29
DT_FLAGS_1, DF_1_PIE = 0x6FFFFFFB, 0x08000000
FILLERS = (0x5A, 0x58) # 'Z' (what the reports found) and 'X' (what patchelf documents)
RUN = 128 # that many equal filler bytes in a row is not code or data
def _known_tag(tag):
"""The dynamic tags the ELF specification and the GNU and processor ranges define: anything else is garbage."""
return 0 <= tag <= 0x22 or 0x6000000D <= tag <= 0x6FFFFFFF or 0x70000000 <= tag <= 0x7FFFFFFF
def read_elf(data):
"""The facts about an ELF64 little-endian file, or None when it is not one."""
if len(data) < 64 or data[:4] != b"\x7fELF" or data[4] != 2 or data[5] != 1:
return None
e_type = struct.unpack_from("<H", data, 16)[0]
e_entry = struct.unpack_from("<Q", data, 0x18)[0]
phoff = struct.unpack_from("<Q", data, 0x20)[0]
phentsize, phnum = struct.unpack_from("<HH", data, 0x36)
if phentsize < 56 or phoff + phentsize * phnum > len(data):
return {"type": e_type, "entry": e_entry, "phdrs": [], "broken": "the program header table is outside the file"}
phdrs = []
for i in range(phnum):
p_type, flags, off, vaddr, _paddr, filesz, memsz, align = struct.unpack_from("<IIQQQQQQ", data, phoff + i * phentsize)
phdrs.append({"type": p_type, "flags": flags, "off": off, "vaddr": vaddr, "filesz": filesz, "memsz": memsz, "align": align})
return {"type": e_type, "entry": e_entry, "phdrs": phdrs, "broken": None}
def is_pie_candidate(info):
"""An ET_DYN with no interpreter, a dynamic segment and an entry point: what a static PIE looks like from outside the dynamic section.
A shared library normally has no entry point (e_entry 0) and is not a candidate; the few that do (the C library itself, the dynamic loader)
have a readable dynamic section and are told apart by it (static_pie_kind). The test uses nothing the damage we look for can erase."""
return bool(info) and info["type"] == ET_DYN and not info["broken"] and info.get("entry", 0) != 0 and not any(p["type"] == PT_INTERP for p in info["phdrs"]) and any(p["type"] == PT_DYNAMIC for p in info["phdrs"])
def static_pie_kind(data, info):
"""'static-pie' for a sound static PIE (DT_FLAGS_1 has DF_1_PIE, no DT_SONAME), 'damaged' for a candidate whose dynamic section cannot be read
(a static PIE with an overwritten .dynamic loses its flags with it, so unreadable is treated as damaged), None for everything else:
shared libraries, which carry a DT_SONAME and no DF_1_PIE, and programs that are not static PIEs."""
if not is_pie_candidate(info):
return None
entries, why = dynamic_entries(data, info)
if why or any(not _known_tag(t) for t, _ in entries):
return "damaged"
tags = {t: v for t, v in entries}
if DT_SONAME in tags:
return None
return "static-pie" if tags.get(DT_FLAGS_1, 0) & DF_1_PIE else None
def is_static_pie(info):
"""Kept for callers: a candidate. check_static_pie decides with static_pie_kind."""
return is_pie_candidate(info)
def _mapped(info, vaddr, size):
"""The file offset of [vaddr, vaddr+size) when one PT_LOAD maps all of it from the file, else None."""
for p in info["phdrs"]:
if p["type"] == PT_LOAD and p["vaddr"] <= vaddr and vaddr + size <= p["vaddr"] + p["filesz"]:
return p["off"] + (vaddr - p["vaddr"])
return None
def dynamic_entries(data, info):
"""(tag, value) pairs of the PT_DYNAMIC segment up to DT_NULL, and the problem if it does not end properly."""
seg = next(p for p in info["phdrs"] if p["type"] == PT_DYNAMIC)
if seg["off"] + seg["filesz"] > len(data):
return [], "the dynamic segment is outside the file"
out = []
for at in range(seg["off"], seg["off"] + seg["filesz"] - 15, 16):
tag, val = struct.unpack_from("<qQ", data, at)
if tag == DT_NULL:
return out, None
out.append((tag, val))
return out, "the dynamic segment does not end with DT_NULL"
def _longest_filler(data, start, end):
best = run = 0
prev = None
for b in data[start:end]:
run = run + 1 if b == prev and b in FILLERS else (1 if b in FILLERS else 0)
prev = b
best = max(best, run)
return best
def check_static_pie(data):
"""The problems of one file (a list of sentences); empty for a sound static PIE and for anything that is not one."""
info = read_elf(data)
if static_pie_kind(data, info) is None:
return []
problems = []
seg = next(p for p in info["phdrs"] if p["type"] == PT_DYNAMIC)
if _mapped(info, seg["vaddr"], max(seg["filesz"], 1)) is None:
problems.append("the dynamic segment is not inside any loaded segment")
entries, why = dynamic_entries(data, info)
if why:
problems.append(why)
bad = [t for t, _ in entries if not _known_tag(t)]
if bad:
problems.append(f"{len(bad)} dynamic entries have tags that do not exist (first {bad[0]:#x}): the section is overwritten")
if any(t in (DT_RPATH, DT_RUNPATH) for t, _ in entries):
problems.append("a static PIE has an RPATH or RUNPATH: something rewrote it after it was linked")
values = dict(entries)
if DT_RELA in values:
size = values.get(DT_RELASZ, 0)
at = _mapped(info, values[DT_RELA], max(size, 1))
if size == 0 or at is None:
problems.append("the relocation table named by DT_RELA is empty or outside the loaded segments")
elif _longest_filler(data, at, at + size) >= min(RUN, size):
problems.append("the relocation table is filled with one byte")
for p in info["phdrs"]:
if p["type"] == PT_LOAD and _longest_filler(data, p["off"], p["off"] + p["filesz"]) >= RUN:
problems.append(f"{RUN} or more filler bytes (0x5A or 0x58) in a row inside a loaded segment")
break
return problems
def elf_differences(staged, packaged):
"""What differs between two builds of one program in the parts that matter for start-up: for the report."""
a, b = read_elf(staged), read_elf(packaged)
if not a or not b:
return [] if staged == packaged else ["one of the two is not an ELF64 file"]
out = []
ta, tb = [p["type"] for p in a["phdrs"]], [p["type"] for p in b["phdrs"]]
if ta != tb:
out.append(f"program headers {len(ta)} -> {len(tb)} ({ta.count(PT_LOAD)} -> {tb.count(PT_LOAD)} loaded segments)")
if is_pie_candidate(a) and is_pie_candidate(b):
da, _ = dynamic_entries(staged, a)
db, _ = dynamic_entries(packaged, b)
if da != db:
out.append(f"dynamic section: {len(da)} -> {len(db)} entries" + ("; an RPATH/RUNPATH was added" if any(t in (DT_RPATH, DT_RUNPATH) for t, _ in db) and not any(t in (DT_RPATH, DT_RUNPATH) for t, _ in da) else ""))
return out
def _files(folder):
for base, dirs, names in os.walk(folder):
dirs.sort()
for name in sorted(names):
path = os.path.join(base, name)
if os.path.islink(path) or not os.path.isfile(path):
continue
yield path
def scan(folder):
"""{relative path: problems} for every static PIE under `folder` that has any."""
found = {}
for path in _files(folder):
try:
with open(path, "rb") as f:
head = f.read(4)
if head != b"\x7fELF":
continue
data = head + f.read()
except OSError:
continue
problems = check_static_pie(data)
if problems:
found[os.path.relpath(path, folder)] = problems
return found
def compare(staged_folder, folder):
"""{relative path: sentences} for every staged ELF file that is missing or differs in `folder`."""
found = {}
for path in _files(staged_folder):
rel = os.path.relpath(path, staged_folder)
with open(path, "rb") as f:
want = f.read()
if want[:4] != b"\x7fELF":
continue
other = os.path.join(folder, rel)
if not os.path.isfile(other):
found[rel] = ["missing"]
continue
with open(other, "rb") as f:
have = f.read()
if have != want:
found[rel] = ["bytes differ"] + elf_differences(want, have)
return found
def main(argv):
if len(argv) >= 3 and argv[1] == "scan":
bad = scan(argv[2])
for rel, problems in bad.items():
print(f"BROKEN {rel}: " + "; ".join(problems))
print(f"{len(bad)} static PIE file(s) with a damaged dynamic section")
return 1 if bad else 0
if len(argv) >= 4 and argv[1] == "compare":
bad = compare(argv[2], argv[3])
for rel, why in bad.items():
print(f"DIFFERS {rel}: " + "; ".join(why))
print(f"{len(bad)} ELF file(s) differ from what was staged")
return 1 if bad else 0
print(__doc__)
return 2
if __name__ == "__main__":
sys.exit(main(sys.argv))
+52
View File
@@ -0,0 +1,52 @@
/** Strata's output-budget correction for the staged pi-ai copy. */
import { createHash } from 'node:crypto';
import { lstatSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
export const HARNESS_BUDGET_PATCH = Object.freeze({
id: 'strata-pi-ai-context-safety-v2',
package: '@earendil-works/pi-ai',
version: '1.1.0',
file: 'node_modules/@earendil-works/pi-ai/dist/api/simple-options.js',
beforeSha256: '9b3cc0ae6daa86fdc050bc0cb0f3f986401e64ccddbde6757183c9f8ad411be2',
});
const sha256 = bytes => createHash('sha256').update(bytes).digest('hex');
const oldClamp = 'const available = model.contextWindow - estimateContextTokens(context).tokens - CONTEXT_SAFETY_TOKENS;';
// Small windows also need room after tool results; a fixed margin must not
// consume all remaining estimated space. Keep the old 64K+ calculation.
const newClamp = `const remaining = model.contextWindow - estimateContextTokens(context).tokens;
const safety = model.contextWindow >= 65536 ? CONTEXT_SAFETY_TOKENS
: Math.min(CONTEXT_SAFETY_TOKENS, Math.floor(model.contextWindow / 16), Math.max(0, Math.floor(remaining / 2)));
const available = remaining - safety;`;
/**
* Patch only a flattened, disposable staged Harness. Returns the BUNDLE.json
* provenance entry; the caller records it when writing the bundle manifest.
* A repeat application or any unknown package/source fails before writing.
*/
export function patchStagedHarnessBudget(stagedRoot) {
const fail = message => { throw new Error(`Harness output-budget patch: ${message}`); };
const root = realpathSync(resolve(stagedRoot));
const packageDir = join(root, 'node_modules', '@earendil-works', 'pi-ai');
const target = join(root, HARNESS_BUDGET_PATCH.file);
let manifest, before;
try {
manifest = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8'));
// Flattening must have removed links; never write through a shared source file.
if (realpathSync(target) !== target || !lstatSync(target).isFile() || lstatSync(target).nlink !== 1)
fail('expected an unlinked staged module');
before = readFileSync(target);
} catch (error) {
fail(`cannot read a standalone staged pi-ai dependency (${error.message})`);
}
if (manifest?.name !== HARNESS_BUDGET_PATCH.package || manifest?.version !== HARNESS_BUDGET_PATCH.version)
fail('expected @earendil-works/pi-ai 1.1.0; review the patch before staging a different dependency');
if (sha256(before) !== HARNESS_BUDGET_PATCH.beforeSha256)
fail('unexpected simple-options.js SHA-256; review the dependency source before staging');
const source = before.toString('utf8');
if (source.split(oldClamp).length !== 2) fail('expected exactly one known context clamp');
const after = Buffer.from(source.replace(oldClamp, newClamp));
writeFileSync(target, after);
return { ...HARNESS_BUDGET_PATCH, afterSha256: sha256(after) };
}
+29 -2
View File
@@ -9,7 +9,9 @@
# installer's own integrity promise ("what we staged is what runs") is broken. This script unpacks the AppImage, copies the staged
# folders (control, harness, node, engine-bundle) over the AppDir byte for byte, and packs it again with the same runtime.
#
# --check does not write anything: it lists every engine-bundle file whose bytes differ from the manifest in the AppImage.
# --check does not write anything: it lists every file of control, harness, node and engine-bundle whose bytes differ from what was staged, then
# looks at every static PIE in the AppImage (appimage_elf.py: the bundled ripgrep is one; linuxdeploy's RPATH rewrite leaves such a program with a
# filled .dynamic and it crashes at start, #4337) and runs the bundled `rg` once. A single "0 file(s) differ" and "ok" is what a release needs.
set -euo pipefail
IN="$(realpath "${1:?in.AppImage}")"; OUT="${2:-}"
@@ -33,13 +35,36 @@ LIB="$(find "$WORK/AppDir/usr/lib" -maxdepth 2 -type d -name engine-bundle -prin
[ -n "$LIB" ] || LIB="$(find "$WORK/AppDir/usr/lib" -maxdepth 2 -type d -name control -printf '%h\n' | head -1)"
[ -n "$LIB" ] || { echo "no Strata resource folder in the AppImage" >&2; exit 1; }
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Every static PIE under the AppDir must still have a sound .dynamic, and the bundled ripgrep must start (x86-64 Linux only: it is run).
elf_and_rg_check() {
local bad=0
if command -v python3 >/dev/null 2>&1; then
python3 "$HERE/appimage_elf.py" scan "$LIB" || bad=1
else
echo "python3 is not here: the static PIE check was skipped" >&2
fi
local rg
rg="$(find "$LIB/harness" -path '*ripgrep-linux-x64/bin/rg' -type f 2>/dev/null | head -1)"
if [ -n "$rg" ] && [ "$(uname -s)" = Linux ] && [ "$(uname -m)" = x86_64 ]; then
local t; t="$(mktemp -d -t strata-rg.XXXXXX)"
echo 'needle in a haystack' > "$t/h.txt"
if "$rg" --version >/dev/null 2>&1 && [ "$("$rg" --no-config -c needle "$t/h.txt" 2>/dev/null)" = 1 ]; then echo "bundled rg runs: ok"
else echo "the bundled rg does not run: $rg" >&2; bad=1; fi
rm -rf "$t"
fi
return $bad
}
if [ "$CHECK" = 1 ]; then
bad=0
while IFS= read -r f; do
rel="${f#$RES/}"
if ! cmp -s "$f" "$LIB/$rel"; then echo "differs: $rel"; bad=$((bad + 1)); fi
done < <(find "$RES/engine-bundle" "$RES/node" -type f ! -name .gitignore)
done < <(for d in control harness node engine-bundle; do [ -d "$RES/$d" ] && find "$RES/$d" -type f ! -name .gitignore; done)
echo "$bad file(s) differ from what was staged"
elf_and_rg_check || bad=$((bad + 1))
[ "$bad" = 0 ]
exit
fi
@@ -49,6 +74,8 @@ for d in control harness node engine-bundle; do
[ -d "$RES/$d" ] || continue
rsync -aL --checksum --delete --exclude .gitignore "$RES/$d/" "$LIB/$d/"
done
# What was put back must be sound before it is packed: a damaged static PIE (the bundled ripgrep) stops the pack here, not on a person's computer.
elf_and_rg_check || { echo "the restored AppDir failed the static PIE / ripgrep check; not packing" >&2; exit 1; }
# The launcher records the environment the AppImage was started from, before its own hooks add the AppImage's libraries and GTK/GIO
# settings (LD_LIBRARY_PATH, GTK_*, XDG_DATA_DIRS ...). The host gives it back to every program that is not Strata's own (zenity,
# kdialog, xdg-open, the assistant's shell): apps/host/server/external-env.mjs. Without it a system program loads the AppImage's libssl
+5
View File
@@ -19,6 +19,7 @@ import { join, relative, resolve, sep } from 'node:path';
import { createHash } from 'node:crypto';
import { flattenPnpm } from './flatten-pnpm.mjs';
import { isOtherOsBinaryFolder, missingHarnessImports } from './harness-prune.mjs';
import { patchStagedHarnessBudget } from './harness-budget.mjs';
const args = process.argv.slice(2);
const opt = (name, fallback) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : fallback; };
@@ -144,6 +145,9 @@ function prune(dir) {
}
prune(out);
// Strata's provenance-recorded output-budget correction applies only to this copy.
const patches = [patchStagedHarnessBudget(out)];
// --- licence: the MIT text and the third-party notices travel with the files ---------------------------------------------
for (const name of ['LICENSE', 'THIRD_PARTY_NOTICES.md']) {
if (existsSync(join(src, name))) cpSync(join(src, name), join(out, name));
@@ -156,6 +160,7 @@ const total = size(out);
writeFileSync(join(out, 'BUNDLE.json'), JSON.stringify({
component: 'deepseek-harness', version: cli.version, commit, license: 'MIT', platform, node: process.version,
bytes: total.bytes, files: total.files, entry: 'apps/cli/lib/bin.js', builtAt: new Date().toISOString(),
patches,
note: 'Pruned production install (pnpm deploy --prod). No package manager runs on the user\'s PC.',
}, null, 2));
+4 -4
View File
@@ -616,10 +616,10 @@ await scenario('stop', { installed: true, env: { FAKE_TOKEN_DELAY: '0.1', FAKE_R
await scenario('latest', {}, async ({ base, browser, shot }) => {
const page = await newPage(browser, base);
const now = Date.now();
await page.evaluate(now => {
await page.evaluate(async now => {
const messages = [];
for (let i = 0; i < 40; i++) { messages.push({ id: 'u' + i, role: 'user', content: 'question ' + i, at: now }, { id: 'a' + i, role: 'assistant', content: 'answer number ' + i + ' with some text to fill the line and more of it.', at: now, tokens: 20, inputTokens: 5 }); }
localStorage.setItem('strata.sessions.v1', JSON.stringify([{ id: 's_latest', title: 'long', updatedAt: now, createdAt: now, messages }]));
await fetch('/api/chats/s_latest', { method: 'PUT', headers: { 'x-strata-client': 'ui' }, body: JSON.stringify({ id: 's_latest', title: 'long', updatedAt: now, createdAt: now, messages }) });
localStorage.setItem('strata.active-session.v1', 's_latest');
}, now);
await page.reload({ waitUntil: 'domcontentloaded' });
@@ -690,12 +690,12 @@ if (only.has('restart')) {
await run(async (context, { base }) => {
const page = context.pages()[0] ?? await context.newPage();
await page.goto(base, { waitUntil: 'domcontentloaded' });
await page.evaluate(() => { localStorage.setItem('strata.prefs.v2', JSON.stringify({ theme: 'dark', contentFontSize: 19, language: 'de' })); localStorage.setItem('strata.sessions.v1', JSON.stringify([{ id: 'chat-1', title: 'My chat' }])); });
await page.evaluate(() => { localStorage.setItem('strata.prefs.v2', JSON.stringify({ theme: 'dark', contentFontSize: 19, language: 'de' })); return fetch('/api/chats/chat-1', { method: 'PUT', headers: { 'x-strata-client': 'ui' }, body: JSON.stringify({ id: 'chat-1', title: 'My chat', messages: [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }) }); });
});
await run(async (context, { base }) => {
const page = context.pages()[0] ?? await context.newPage();
await page.goto(base, { waitUntil: 'domcontentloaded' });
const saved = await page.evaluate(() => ({ prefs: JSON.parse(localStorage.getItem('strata.prefs.v2') || '{}'), chats: localStorage.getItem('strata.sessions.v1'), theme: document.documentElement.dataset.theme }));
const saved = await page.evaluate(async () => ({ prefs: JSON.parse(localStorage.getItem('strata.prefs.v2') || '{}'), chats: JSON.stringify(await (await fetch('/api/chats', { headers: { 'x-strata-client': 'ui' } })).json()), theme: document.documentElement.dataset.theme }));
check(saved.prefs.contentFontSize === 19 && saved.prefs.language === 'de', 'text size and language survive a restart on the same port', JSON.stringify(saved.prefs));
check(saved.theme === 'dark', 'the theme survives too', saved.theme);
check(/My chat/.test(saved.chats ?? ''), 'and the chat list', saved.chats);
+157 -1
View File
@@ -349,6 +349,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "byteorder-lite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "bytes"
version = "1.12.1"
@@ -1755,6 +1761,19 @@ dependencies = [
"icu_properties",
]
[[package]]
name = "image"
version = "0.25.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
dependencies = [
"bytemuck",
"byteorder-lite",
"moxcms",
"num-traits",
"png 0.18.1",
]
[[package]]
name = "indexmap"
version = "1.9.3"
@@ -2097,6 +2116,20 @@ version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "mac-notification-sys"
version = "0.6.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd604973958ddcc11b561193c0fb96ba146506ef2f231ef2e7c35fd2cbc9beca"
dependencies = [
"cc",
"log",
"objc2",
"objc2-foundation",
"time",
"uuid",
]
[[package]]
name = "markup5ever"
version = "0.39.0"
@@ -2166,6 +2199,16 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "moxcms"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
dependencies = [
"num-traits",
"pxfm",
]
[[package]]
name = "muda"
version = "0.20.0"
@@ -2229,6 +2272,20 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
[[package]]
name = "notify-rust"
version = "4.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4587364a9a0074333429b3df75a30a205340c56a536ca3eb6ca0e59b87bbf8af"
dependencies = [
"futures-lite",
"log",
"mac-notification-sys",
"serde",
"tauri-winrt-notification",
"zbus",
]
[[package]]
name = "num-conv"
version = "0.2.2"
@@ -2757,6 +2814,15 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "precomputed-hash"
version = "0.1.1"
@@ -2825,6 +2891,12 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "pxfm"
version = "0.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
[[package]]
name = "quick-xml"
version = "0.42.0"
@@ -2855,6 +2927,35 @@ version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
dependencies = [
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "raw-window-handle"
version = "0.6.2"
@@ -3524,17 +3625,20 @@ checksum = "ff7589a1859b09232522bb9edf076ca7fcf14ded96685d3fa629db73023ffbb7"
[[package]]
name = "strata-desktop"
version = "0.2.3"
version = "0.2.12"
dependencies = [
"gtk",
"serde",
"serde_json",
"tauri",
"tauri-build",
"tauri-plugin-notification",
"tauri-plugin-shell",
"tauri-plugin-single-instance",
"tauri-plugin-updater",
"webkit2gtk",
"webview2-com",
"windows-core",
]
[[package]]
@@ -3755,6 +3859,7 @@ dependencies = [
"gtk",
"heck 0.5.0",
"http",
"image",
"jni 0.21.1",
"libc",
"log",
@@ -3869,6 +3974,26 @@ dependencies = [
"walkdir",
]
[[package]]
name = "tauri-plugin-notification"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b939d7b19c8af1381a72ef8474b3aba2b56c1ddac2c23f0c8e96abbcf86b198b"
dependencies = [
"log",
"notify-rust",
"rand",
"serde",
"serde_json",
"serde_repr",
"tauri",
"tauri-plugin",
"thiserror 2.0.21",
"time",
"url",
"zbus",
]
[[package]]
name = "tauri-plugin-shell"
version = "2.5.0"
@@ -4042,6 +4167,17 @@ dependencies = [
"toml 1.1.8+spec-1.1.0",
]
[[package]]
name = "tauri-winrt-notification"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f37a6c354fd28fc9e322ed9bd47e3959576dad28c9d58ea1cf888cce1c7ccb36"
dependencies = [
"thiserror 2.0.21",
"windows",
"windows-version",
]
[[package]]
name = "tempfile"
version = "3.27.0"
@@ -5332,6 +5468,26 @@ dependencies = [
"serde",
]
[[package]]
name = "zerocopy"
version = "0.8.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "zerofrom"
version = "0.1.8"
+9 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "strata-desktop"
version = "0.2.3"
version = "0.2.12"
description = "Strata Desktop — local AI, tuned to one model."
edition = "2021"
rust-version = "1.77"
@@ -11,12 +11,19 @@ license = "MIT"
gtk = "0.18"
webkit2gtk = "2.0"
# Windows: the shell listens for the page's renderer failing (WebView2 ProcessFailed, see hook_renderer_failures in main.rs); these are the versions wry itself uses.
[target.'cfg(windows)'.dependencies]
webview2-com = "0.39"
windows-core = "0.62"
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri = { version = "2", features = ["tray-icon", "image-png"] }
tauri-plugin-shell = "2"
# Desktop notifications for an approval that waits (#3477); the tray icon above is Tauri's own.
tauri-plugin-notification = "2"
tauri-plugin-updater = "2"
tauri-plugin-single-instance = "2"
serde = { version = "1", features = ["derive"] }
@@ -1 +1 @@
{"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener","allow-supports-multiple-windows"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-exit":{"identifier":"allow-exit","description":"Enables the exit command without any pre-configured scope.","commands":{"allow":["exit"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-supports-multiple-windows":{"identifier":"allow-supports-multiple-windows","description":"Enables the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":["supports_multiple_windows"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-exit":{"identifier":"deny-exit","description":"Denies the exit command without any pre-configured scope.","commands":{"allow":[],"deny":["exit"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"DenLine truncated
{"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener","allow-supports-multiple-windows"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-exit":{"identifier":"allow-exit","description":"Enables the exit command without any pre-configured scope.","commands":{"allow":["exit"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-supports-multiple-windows":{"identifier":"allow-supports-multiple-windows","description":"Enables the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":["supports_multiple_windows"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-exit":{"identifier":"deny-exit","description":"Denies the exit command without any pre-configured scope.","commands":{"allow":[],"deny":["exit"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"DenLine truncated
@@ -2426,6 +2426,216 @@
"const": "core:window:deny-unminimize",
"markdownDescription": "Denies the unminimize command without any pre-configured scope."
},
{
"description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`",
"type": "string",
"const": "notification:default",
"markdownDescription": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`"
},
{
"description": "Enables the batch command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-batch",
"markdownDescription": "Enables the batch command without any pre-configured scope."
},
{
"description": "Enables the cancel command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-cancel",
"markdownDescription": "Enables the cancel command without any pre-configured scope."
},
{
"description": "Enables the check_permissions command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-check-permissions",
"markdownDescription": "Enables the check_permissions command without any pre-configured scope."
},
{
"description": "Enables the create_channel command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-create-channel",
"markdownDescription": "Enables the create_channel command without any pre-configured scope."
},
{
"description": "Enables the delete_channel command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-delete-channel",
"markdownDescription": "Enables the delete_channel command without any pre-configured scope."
},
{
"description": "Enables the get_active command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-get-active",
"markdownDescription": "Enables the get_active command without any pre-configured scope."
},
{
"description": "Enables the get_pending command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-get-pending",
"markdownDescription": "Enables the get_pending command without any pre-configured scope."
},
{
"description": "Enables the is_permission_granted command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-is-permission-granted",
"markdownDescription": "Enables the is_permission_granted command without any pre-configured scope."
},
{
"description": "Enables the list_channels command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-list-channels",
"markdownDescription": "Enables the list_channels command without any pre-configured scope."
},
{
"description": "Enables the notify command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-notify",
"markdownDescription": "Enables the notify command without any pre-configured scope."
},
{
"description": "Enables the permission_state command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-permission-state",
"markdownDescription": "Enables the permission_state command without any pre-configured scope."
},
{
"description": "Enables the register_action_types command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-register-action-types",
"markdownDescription": "Enables the register_action_types command without any pre-configured scope."
},
{
"description": "Enables the register_listener command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-register-listener",
"markdownDescription": "Enables the register_listener command without any pre-configured scope."
},
{
"description": "Enables the remove_active command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-remove-active",
"markdownDescription": "Enables the remove_active command without any pre-configured scope."
},
{
"description": "Enables the remove_listener command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-remove-listener",
"markdownDescription": "Enables the remove_listener command without any pre-configured scope."
},
{
"description": "Enables the request_permission command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-request-permission",
"markdownDescription": "Enables the request_permission command without any pre-configured scope."
},
{
"description": "Enables the show command without any pre-configured scope.",
"type": "string",
"const": "notification:allow-show",
"markdownDescription": "Enables the show command without any pre-configured scope."
},
{
"description": "Denies the batch command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-batch",
"markdownDescription": "Denies the batch command without any pre-configured scope."
},
{
"description": "Denies the cancel command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-cancel",
"markdownDescription": "Denies the cancel command without any pre-configured scope."
},
{
"description": "Denies the check_permissions command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-check-permissions",
"markdownDescription": "Denies the check_permissions command without any pre-configured scope."
},
{
"description": "Denies the create_channel command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-create-channel",
"markdownDescription": "Denies the create_channel command without any pre-configured scope."
},
{
"description": "Denies the delete_channel command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-delete-channel",
"markdownDescription": "Denies the delete_channel command without any pre-configured scope."
},
{
"description": "Denies the get_active command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-get-active",
"markdownDescription": "Denies the get_active command without any pre-configured scope."
},
{
"description": "Denies the get_pending command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-get-pending",
"markdownDescription": "Denies the get_pending command without any pre-configured scope."
},
{
"description": "Denies the is_permission_granted command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-is-permission-granted",
"markdownDescription": "Denies the is_permission_granted command without any pre-configured scope."
},
{
"description": "Denies the list_channels command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-list-channels",
"markdownDescription": "Denies the list_channels command without any pre-configured scope."
},
{
"description": "Denies the notify command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-notify",
"markdownDescription": "Denies the notify command without any pre-configured scope."
},
{
"description": "Denies the permission_state command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-permission-state",
"markdownDescription": "Denies the permission_state command without any pre-configured scope."
},
{
"description": "Denies the register_action_types command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-register-action-types",
"markdownDescription": "Denies the register_action_types command without any pre-configured scope."
},
{
"description": "Denies the register_listener command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-register-listener",
"markdownDescription": "Denies the register_listener command without any pre-configured scope."
},
{
"description": "Denies the remove_active command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-remove-active",
"markdownDescription": "Denies the remove_active command without any pre-configured scope."
},
{
"description": "Denies the remove_listener command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-remove-listener",
"markdownDescription": "Denies the remove_listener command without any pre-configured scope."
},
{
"description": "Denies the request_permission command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-request-permission",
"markdownDescription": "Denies the request_permission command without any pre-configured scope."
},
{
"description": "Denies the show command without any pre-configured scope.",
"type": "string",
"const": "notification:deny-show",
"markdownDescription": "Denies the show command without any pre-configured scope."
},
{
"description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`",
"type": "string",
@@ -2491,6 +2701,60 @@
"type": "string",
"const": "shell:deny-stdin-write",
"markdownDescription": "Denies the stdin_write command without any pre-configured scope."
},
{
"description": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`",
"type": "string",
"const": "updater:default",
"markdownDescription": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`"
},
{
"description": "Enables the check command without any pre-configured scope.",
"type": "string",
"const": "updater:allow-check",
"markdownDescription": "Enables the check command without any pre-configured scope."
},
{
"description": "Enables the download command without any pre-configured scope.",
"type": "string",
"const": "updater:allow-download",
"markdownDescription": "Enables the download command without any pre-configured scope."
},
{
"description": "Enables the download_and_install command without any pre-configured scope.",
"type": "string",
"const": "updater:allow-download-and-install",
"markdownDescription": "Enables the download_and_install command without any pre-configured scope."
},
{
"description": "Enables the install command without any pre-configured scope.",
"type": "string",
"const": "updater:allow-install",
"markdownDescription": "Enables the install command without any pre-configured scope."
},
{
"description": "Denies the check command without any pre-configured scope.",
"type": "string",
"const": "updater:deny-check",
"markdownDescription": "Denies the check command without any pre-configured scope."
},
{
"description": "Denies the download command without any pre-configured scope.",
"type": "string",
"const": "updater:deny-download",
"markdownDescription": "Denies the download command without any pre-configured scope."
},
{
"description": "Denies the download_and_install command without any pre-configured scope.",
"type": "string",
"const": "updater:deny-download-and-install",
"markdownDescription": "Denies the download_and_install command without any pre-configured scope."
},
{
"description": "Denies the install command without any pre-configured scope.",
"type": "string",
"const": "updater:deny-install",
"markdownDescription": "Denies the install command without any pre-configured scope."
}
]
},
+32
View File
@@ -701,6 +701,35 @@ SectionEnd
!macroend
; STRATA-MIGRATE-END
; STRATA-STOP-BEGIN
; Stop what Strata itself started from the install folder (the app, its host, the node runtime, the engine) before files are removed:
; a running copy keeps its files open, so the folder stayed behind after use. Only processes whose image path is inside INSTDIR are
; stopped; this uninstaller's own process and any program elsewhere are left alone.
!macro StrataStopOwnProcesses INSTALLDIR
System::Call 'kernel32::SetEnvironmentVariableW(w "STRATA_INSTDIR", w "${INSTALLDIR}")'
System::Call 'kernel32::GetCurrentProcessId()i.r9'
System::Call 'kernel32::SetEnvironmentVariableW(w "STRATA_SELFPID", w "$9")'
; the installer is a 32-bit program, so plain powershell.exe is the 32-bit one, which cannot read the image path of a 64-bit process: use the 64-bit one
StrCpy $8 "$WINDIR\Sysnative\WindowsPowerShell\v1.0\powershell.exe"
IfFileExists $8 +2 0
StrCpy $8 "powershell.exe"
nsExec::Exec '"$8" -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "$$d = [IO.Path]::GetFullPath($$env:STRATA_INSTDIR).TrimEnd([char]92) + [char]92; $$ids = @(); $$ids += Get-Process -ErrorAction SilentlyContinue | Where-Object { $$_.Path -and [IO.Path]::GetFullPath($$_.Path).StartsWith($$d, [StringComparison]::OrdinalIgnoreCase) } | ForEach-Object { $$_.Id }; try { $$ids += Get-CimInstance Win32_Process -ErrorAction Stop | Where-Object { $$_.ExecutablePath -and [IO.Path]::GetFullPath($$_.ExecutablePath).StartsWith($$d, [StringComparison]::OrdinalIgnoreCase) } | ForEach-Object { $$_.ProcessId } } catch {}; $$ids | Sort-Object -Unique | Where-Object { $$_ -ne [int]$$env:STRATA_SELFPID } | ForEach-Object { Stop-Process -Id $$_ -Force -ErrorAction SilentlyContinue }; Start-Sleep -Milliseconds 1500"'
Pop $9
!macroend
; STRATA-STOP-END
; STRATA-CLEAN-BEGIN
; The app writes into its own folders while it runs (Python bytecode caches under control\, for one), and the file-by-file delete below does not know them,
; so those folders stayed behind with a few dozen files. These four folders hold only what the installer put there and what the app made there;
; user data is never in them (it lives in the data folder), and the install folder itself is still removed with a plain RMDir.
!macro StrataRemoveAppFolders INSTALLDIR
RMDir /r "${INSTALLDIR}\control"
RMDir /r "${INSTALLDIR}\host"
RMDir /r "${INSTALLDIR}\node"
RMDir /r "${INSTALLDIR}\harness"
!macroend
; STRATA-CLEAN-END
Section Install
SetOutPath $INSTDIR
@@ -847,6 +876,7 @@ Section Uninstall
!insertmacro NSIS_HOOK_PREUNINSTALL
!endif
!insertmacro StrataStopOwnProcesses "$INSTDIR"
!insertmacro CheckIfAppIsRunning "$INSTDIR\${MAINBINARYNAME}.exe" "${PRODUCTNAME}"
; Delete the app directory and its content from disk
@@ -879,6 +909,8 @@ Section Uninstall
{{/each}}
!insertmacro StrataRemoveAppFolders "$INSTDIR"
; Delete uninstaller
Delete "$INSTDIR\uninstall.exe"
+2 -2
View File
@@ -188,7 +188,7 @@ fn graphics_environment() -> Vec<String> {
const NAMES: &[&str] = &[
"XDG_SESSION_TYPE", "XDG_CURRENT_DESKTOP", "DESKTOP_SESSION", "DISPLAY", "WAYLAND_DISPLAY", "GDK_BACKEND", "GTK_THEME",
"WEBKIT_DISABLE_DMABUF_RENDERER", "WEBKIT_DISABLE_COMPOSITING_MODE", "WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS", "LIBGL_ALWAYS_SOFTWARE",
"GALLIUM_DRIVER", "__GLX_VENDOR_LIBRARY_NAME", "__NV_PRIME_RENDER_OFFLOAD", "MESA_LOADER_DRIVER_OVERRIDE", "APPIMAGE", "APPDIR",
"FONTCONFIG_FILE", "FONTCONFIG_PATH", "STRATA_FONT_CONFIG", "GALLIUM_DRIVER", "__GLX_VENDOR_LIBRARY_NAME", "__NV_PRIME_RENDER_OFFLOAD", "MESA_LOADER_DRIVER_OVERRIDE", "APPIMAGE", "APPDIR",
];
NAMES.iter().filter_map(|n| std::env::var(n).ok().map(|v| format!(" {n}={v}"))).collect()
}
@@ -584,7 +584,7 @@ mod tests {
let path = write_startup_failure(&dir, "the app stopped unexpectedly: boom (at main.rs:1, thread main)", Some(Path::new("/x/desktop-1.log"))).unwrap();
assert!(path.file_name().unwrap().to_string_lossy().starts_with("desktop-crash-"));
let text = std::fs::read_to_string(&path).unwrap();
for needle in ["boom", "main.rs:1", "/x/desktop-1.log", "0.2.0", "System:", "Meaning:"] {
for needle in ["boom", "main.rs:1", "/x/desktop-1.log", env!("CARGO_PKG_VERSION"), "System:", "Meaning:"] {
assert!(text.contains(needle), "{needle}");
}
let _ = std::fs::remove_dir_all(&dir);
+291
View File
@@ -0,0 +1,291 @@
//! Linux: keep the colour emoji font that the AppImage's WebKitGTK cannot draw away from the app window (#4533).
//!
//! The WebKitGTK inside the AppImage paints a COLRv1 colour font through Skia, and its `colrv1_configure_skpaint` reads past the end of a
//! gradient's colour-stop list. The library is built with libstdc++ range checks on, so the read becomes `abort()` and `WebKitWebProcess`
//! dies: the window goes grey the first time a chat title or a message draws an emoji (Fedora 44 and newer Ubuntu ship
//! `Noto-COLRv1.ttf` as the default colour emoji font; the reporters on #4533 name the same assertion and the same workaround).
//!
//! fontconfig reads one file when `FONTCONFIG_FILE` is set, and that file replaces the normal configuration, so the file written here
//! includes the system configuration first and only then rejects the COLRv1 faces. Emoji are then drawn from whatever else the system
//! has (the outline "Noto Emoji" is preferred); nothing else about the fonts changes.
//!
//! Only the window process is told (main.rs run_window_process, before WebKit starts its own processes). The shell owns the host, the
//! engine and the agent, and they keep the fonts the person chose. A machine with no COLRv1 font is not touched at all.
//!
//! This file is plain std so its decisions are unit-tested on every platform; only the window process on Linux uses it.
//! The idea is that of community PR #4485; the code is written for this tree.
use std::path::{Path, PathBuf};
/// File name patterns of the COLRv1 faces (fontconfig globs, case sensitive, so both spellings). Bitmap (`CBDT`) and outline faces are not
/// listed: they draw fine, and hiding them would leave an empty box where an emoji should be.
pub const COLRV1_GLOBS: &[&str] = &["*COLRv1*", "*colrv1*", "*COLRV1*"];
/// The outline family preferred in place of the colour one.
const OUTLINE_EMOJI: &str = "Noto Emoji";
/// fontconfig's own configuration. `FONTCONFIG_FILE` replaces it, so the written file pulls it back in.
pub const SYSTEM_CONFIG: &str = "/etc/fonts/fonts.conf";
/// `STRATA_FONT_CONFIG=off` turns this off; `STRATA_FONT_CONFIG=/path/to/fonts.conf` uses the person's own file.
const OFF: &str = "off";
/// The name of the file written under the data folder.
const FILE_NAME: &str = "fonts-no-colrv1.conf";
/// Folders fontconfig scans by default, where a colour emoji font is found: the system ones, the person's own, and the XDG data folders.
pub fn font_dirs(env: &dyn Fn(&str) -> Option<String>) -> Vec<PathBuf> {
let mut dirs = vec![PathBuf::from("/usr/share/fonts"), PathBuf::from("/usr/local/share/fonts")];
let home = env("HOME").filter(|h| !h.is_empty()).map(PathBuf::from);
match env("XDG_DATA_HOME").filter(|d| !d.is_empty()) {
Some(data) => dirs.push(PathBuf::from(data).join("fonts")),
None => {
if let Some(h) = &home {
dirs.push(h.join(".local/share/fonts"));
}
}
}
if let Some(h) = &home {
dirs.push(h.join(".fonts"));
}
if let Some(list) = env("XDG_DATA_DIRS") {
for d in list.split(':').filter(|d| !d.is_empty()) {
dirs.push(PathBuf::from(d).join("fonts"));
}
}
dirs.dedup();
dirs
}
/// A COLRv1 font file installed under `dirs`, if any. The scan is shallow and stops at the first one found, so a window that starts on a
/// machine without such a font costs a few directory reads and nothing else.
pub fn colrv1_font_present(dirs: &[PathBuf]) -> Option<PathBuf> {
fn scan(dir: &Path, depth: u8, found: &mut Option<PathBuf>) {
if found.is_some() || depth == 0 {
return;
}
let Ok(entries) = std::fs::read_dir(dir) else { return };
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
scan(&path, depth - 1, found);
}
else if path.file_name().map(|n| n.to_string_lossy().to_ascii_lowercase().contains("colrv1")).unwrap_or(false) {
*found = Some(path);
return;
}
if found.is_some() {
return;
}
}
}
let mut found = None;
for dir in dirs {
scan(dir, 5, &mut found);
if found.is_some() {
break;
}
}
found
}
/// The configuration file's text: the system's own rules, then the rejection of the COLRv1 faces, then the outline family preferred.
pub fn config_text(system_config: &str) -> String {
let mut lines: Vec<String> = vec![
"<?xml version=\"1.0\"?>".into(),
"<!DOCTYPE fontconfig SYSTEM \"urn:fontconfig:fonts.dtd\">".into(),
"<!-- Written by Strata for its own window only. The WebKitGTK inside the AppImage aborts while it draws a COLRv1 colour font,".into(),
" so those faces are hidden from the window; every other font is the system's own setup, included first.".into(),
" Off: STRATA_FONT_CONFIG=off. Your own file: set FONTCONFIG_FILE yourself. -->".into(),
"<fontconfig>".into(),
format!(" <include ignore_missing=\"yes\">{system_config}</include>"),
" <selectfont>".into(),
];
// fontconfig rejects a font only inside <selectfont>
for glob in COLRV1_GLOBS {
lines.push(format!(" <rejectfont><glob>{glob}</glob></rejectfont>"));
}
lines.push(" </selectfont>".into());
lines.push(" <alias binding=\"strong\">".into());
lines.push(" <family>Noto Color Emoji</family>".into());
lines.push(format!(" <prefer><family>{OUTLINE_EMOJI}</family></prefer>"));
lines.push(" </alias>".into());
lines.push("</fontconfig>".into());
lines.join("\n") + "\n"
}
/// Write the file under `dir` and return its path, or None when it cannot be written or there is no system configuration to include
/// (then the window would be left without any font). Rewritten only when the contents differ, so a start-up touches the disk once, and a
/// file left by an older Strata is put right. Written through a temporary file and moved, so a second instance starting at the same moment
/// never hands fontconfig half a file.
pub fn ensure_config(dir: &Path, system_config: &Path) -> Option<PathBuf> {
if !system_config.exists() {
return None;
}
std::fs::create_dir_all(dir).ok()?;
let path = dir.join(FILE_NAME);
let want = config_text(&system_config.to_string_lossy());
if matches!(std::fs::read_to_string(&path), Ok(have) if have == want) {
return Some(path);
}
let tmp = dir.join(format!("{FILE_NAME}.{}.tmp", std::process::id()));
std::fs::write(&tmp, want.as_bytes()).ok()?;
if std::fs::rename(&tmp, &path).is_err() {
let _ = std::fs::remove_file(&tmp);
// another writer got there first: take its file only if it is the one wanted
return (std::fs::read_to_string(&path).ok()? == want).then_some(path);
}
Some(path)
}
/// The file the window process should hand to fontconfig, or None to leave the environment exactly as it is. In this order, so a person's
/// own choice always wins and a machine without the font is untouched:
/// 1. `FONTCONFIG_FILE` already set: theirs.
/// 2. `STRATA_FONT_CONFIG=off`: nothing.
/// 3. `STRATA_FONT_CONFIG=<path>`: that file, when it exists.
/// 4. no COLRv1 font installed: nothing.
pub fn config_for(env: &dyn Fn(&str) -> Option<String>, dir: &Path, dirs: &[PathBuf], system_config: &Path) -> Option<PathBuf> {
if env("FONTCONFIG_FILE").is_some() || env("STRATA_FONT_CONFIG").as_deref() == Some(OFF) {
return None;
}
if let Some(custom) = env("STRATA_FONT_CONFIG") {
let p = PathBuf::from(custom);
return p.is_file().then_some(p);
}
colrv1_font_present(dirs)?;
ensure_config(dir, system_config)
}
#[cfg(test)]
mod tests {
use super::*;
fn env_from(pairs: &[(&str, &str)]) -> impl Fn(&str) -> Option<String> {
let owned: Vec<(String, String)> = pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect();
move |k: &str| owned.iter().find(|(a, _)| a == k).map(|(_, v)| v.clone())
}
fn scratch(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("strata-fontguard-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn the_file_includes_the_system_config_first_and_rejects_only_colrv1_faces_inside_selectfont() {
let text = config_text("/etc/fonts/fonts.conf");
assert!(text.starts_with("<?xml"));
let include = text.find("/etc/fonts/fonts.conf").expect("the system's fonts are kept");
let select = text.find("<selectfont>").expect("a rejection needs <selectfont>");
let select_end = text.find("</selectfont>").expect("closed");
assert!(include < select && select < select_end);
assert_eq!(text.matches("<rejectfont>").count(), COLRV1_GLOBS.len());
for glob in COLRV1_GLOBS {
let at = text.find(&format!("<glob>{glob}</glob>")).unwrap_or_else(|| panic!("{glob} is rejected"));
assert!(at > select && at < select_end, "{glob} is inside <selectfont>");
}
// the bitmap and the outline faces stay
assert!(!text.contains("NotoColorEmoji.ttf") && !text.contains("NotoEmoji-Regular.ttf"));
assert!(text.contains("<prefer><family>Noto Emoji</family></prefer>"));
assert!(!text.contains('{') && !text.contains('}'), "no placeholder left: {text}");
// well formed enough: every opened tag is closed
for tag in ["fontconfig", "selectfont", "alias"] {
assert_eq!(text.matches(&format!("<{tag}")).count(), text.matches(&format!("</{tag}>")).count(), "{tag}");
}
}
#[test]
fn the_globs_match_the_real_file_names_and_not_the_faces_that_draw_fine() {
// fontconfig's glob is `*` anywhere; this is the same rule on the file names that matter
let hit = |name: &str| COLRV1_GLOBS.iter().any(|g| { let core = g.trim_matches('*'); name.contains(core) });
for bad in ["/usr/share/fonts/google-noto-color-emoji-fonts/Noto-COLRv1.ttf", "/usr/share/fonts/truetype/noto/NotoColorEmoji-COLRv1.ttf",
"/home/me/.fonts/Twemoji-colrv1.ttf", "/usr/share/fonts/NotoSansCOLRv1-Regular.ttf"] {
assert!(hit(bad), "{bad}");
}
for good in ["/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf", "/usr/share/fonts/truetype/noto/NotoEmoji-Regular.ttf",
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/truetype/noto/NotoSansArabic-Regular.ttf"] {
assert!(!hit(good), "{good}");
}
}
#[test]
fn a_persons_own_fontconfig_or_an_opt_out_leaves_the_environment_alone() {
let dir = scratch("own");
let system = dir.join("system.conf");
std::fs::write(&system, b"<fontconfig/>").unwrap();
let fonts = dir.join("fonts");
std::fs::create_dir_all(&fonts).unwrap();
std::fs::write(fonts.join("Noto-COLRv1.ttf"), b"x").unwrap();
let dirs = vec![fonts];
assert_eq!(config_for(&env_from(&[("FONTCONFIG_FILE", "/home/me/fonts.conf")]), &dir, &dirs, &system), None);
assert_eq!(config_for(&env_from(&[("STRATA_FONT_CONFIG", "off")]), &dir, &dirs, &system), None);
let theirs = dir.join("theirs.conf");
std::fs::write(&theirs, b"<fontconfig/>").unwrap();
assert_eq!(config_for(&env_from(&[("STRATA_FONT_CONFIG", theirs.to_str().unwrap())]), &dir, &dirs, &system), Some(theirs));
assert_eq!(config_for(&env_from(&[("STRATA_FONT_CONFIG", "/no/such/fonts.conf")]), &dir, &dirs, &system), None);
// no setting at all: the font is there, so the file is written
assert!(config_for(&env_from(&[]), &dir.join("out"), &dirs, &system).is_some());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_machine_without_a_colrv1_font_is_not_touched() {
let dir = scratch("none");
let system = dir.join("system.conf");
std::fs::write(&system, b"<fontconfig/>").unwrap();
let fonts = dir.join("fonts");
std::fs::create_dir_all(fonts.join("noto")).unwrap();
std::fs::write(fonts.join("noto/NotoColorEmoji.ttf"), b"bitmap").unwrap();
std::fs::write(fonts.join("noto/NotoEmoji-Regular.ttf"), b"outline").unwrap();
std::fs::write(fonts.join("noto/NotoSansArabic-Regular.ttf"), b"arabic").unwrap();
assert_eq!(colrv1_font_present(&[fonts.clone()]), None);
assert_eq!(config_for(&env_from(&[]), &dir.join("out"), &[fonts.clone()], &system), None);
assert!(!dir.join("out").exists(), "nothing is written");
std::fs::write(fonts.join("noto/Noto-COLRv1.ttf"), b"colour").unwrap();
assert!(colrv1_font_present(&[fonts]).is_some());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn the_scan_is_shallow_and_a_missing_folder_is_fine() {
let dir = scratch("deep");
let mut deep = dir.clone();
for i in 0..9 { deep = deep.join(format!("d{i}")); }
std::fs::create_dir_all(&deep).unwrap();
std::fs::write(deep.join("Noto-COLRv1.ttf"), b"x").unwrap();
assert_eq!(colrv1_font_present(&[dir.clone(), dir.join("missing")]), None, "nine levels down is not a font folder");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn the_folders_follow_the_xdg_variables() {
let none = font_dirs(&env_from(&[("HOME", "/home/me")]));
assert!(none.contains(&PathBuf::from("/usr/share/fonts")));
assert!(none.contains(&PathBuf::from("/home/me/.local/share/fonts")));
assert!(none.contains(&PathBuf::from("/home/me/.fonts")));
let set = font_dirs(&env_from(&[("HOME", "/home/me"), ("XDG_DATA_HOME", "/data/me"), ("XDG_DATA_DIRS", "/usr/local/share:/var/lib/flatpak/exports/share")]));
assert!(set.contains(&PathBuf::from("/data/me/fonts")));
assert!(!set.contains(&PathBuf::from("/home/me/.local/share/fonts")));
assert!(set.contains(&PathBuf::from("/var/lib/flatpak/exports/share/fonts")));
}
#[test]
fn the_file_is_written_once_an_old_one_is_put_right_and_nothing_is_left_behind() {
let dir = scratch("write");
let system = dir.join("system.conf");
std::fs::write(&system, b"<fontconfig/>").unwrap();
let out = dir.join("out");
let first = ensure_config(&out, &system).expect("written");
assert_eq!(ensure_config(&out, &system).unwrap(), first, "the same path every start");
std::fs::write(&first, "<fontconfig/>").unwrap();
assert_eq!(ensure_config(&out, &system), Some(first.clone()));
assert_eq!(std::fs::read_to_string(&first).unwrap(), config_text(&system.to_string_lossy()));
assert_eq!(std::fs::read_dir(&out).unwrap().count(), 1, "one file, no temporary");
// no system configuration to include: nothing is written (the window would have no font at all)
assert_eq!(ensure_config(&dir.join("out2"), &dir.join("absent.conf")), None);
assert!(!dir.join("out2").exists());
let _ = std::fs::remove_dir_all(&dir);
}
}
+130 -4
View File
@@ -39,6 +39,11 @@ use tauri::{Manager, WebviewUrl, WebviewWindowBuilder};
mod updater;
mod desktoplog;
mod recovery;
mod shell;
// Plain std, built on every platform so its decisions are tested everywhere; only the Linux window process uses it.
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
mod fontguard;
#[cfg(target_os = "linux")]
mod supervisor;
@@ -252,13 +257,24 @@ fn pick_port() -> Result<u16, String> {
/// launch starts with empty storage, as before).
const PREFERRED_PORT: u16 = 47831;
/// Set when the port the person (or the app) wanted was busy: "busy:<wanted>:<used>". The host gets it as STRATA_PORT_NOTICE and tells the
/// person (the chats and settings saved on the page belong to the port, so a different port starts empty) (#3402).
static PORT_NOTICE: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None);
fn pick_stable_port() -> Result<u16, String> {
match TcpListener::bind(("127.0.0.1", PREFERRED_PORT)) {
// The person may have chosen a port in Settings (shell-prefs.json); otherwise the app's usual one.
let wanted = shell::wanted_port(&shell::read_prefs(), PREFERRED_PORT);
*PORT_NOTICE.lock().unwrap() = None;
match TcpListener::bind(("127.0.0.1", wanted)) {
Ok(listener) => {
drop(listener);
Ok(PREFERRED_PORT)
Ok(wanted)
}
Err(_) => {
let used = pick_port()?;
*PORT_NOTICE.lock().unwrap() = Some(shell::busy_notice(wanted, used));
Ok(used)
}
Err(_) => pick_port(),
}
}
@@ -469,6 +485,9 @@ fn host_command(res: &Path, port: u16, shutdown_token: &str) -> Result<Command,
.stderr(Stdio::inherit());
apply_runtime_policy(&mut cmd, res);
hide_console(&mut cmd);
if let Some(notice) = PORT_NOTICE.lock().unwrap().clone() {
cmd.env("STRATA_PORT_NOTICE", notice);
}
// A user with the Strata API worker on a different port sets this; unset means the worker's
// documented loopback default.
if let Ok(url) = std::env::var("STRATA_SERVER_URL") {
@@ -864,7 +883,19 @@ fn main() {
/// Windows and macOS: one process owns the host and the window.
#[cfg(not(target_os = "linux"))]
fn run_embedded() {
tauri::Builder::default()
let builder = tauri::Builder::default();
// macOS: WKWebView ends the page's web content process under memory pressure and leaves a blank window; the page is loaded again (#5079).
#[cfg(target_os = "macos")]
let builder = builder.on_web_content_process_terminate(|webview| {
let port = webview.app_handle().state::<HostPort>().0.lock().ok().and_then(|p| *p);
let Some(port) = port else { return };
let failed = webview.url().ok().map(|u| u.to_string());
let target = webview.clone();
on_renderer_failure(format!("http://127.0.0.1:{port}"), recovery::Failure::RendererExited, "web content process terminated".to_string(), failed, move |url| {
if let Ok(url) = url.parse::<tauri::Url>() { let _ = target.navigate(url); }
});
});
builder
// First, as the plugin requires: a second launch ends here after telling the running app to show itself.
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
if let Some(window) = app.get_webview_window("main") {
@@ -875,6 +906,7 @@ fn run_embedded() {
}))
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_notification::init())
.manage(HostProcess(Mutex::new(None)))
.manage(HostPort(Mutex::new(None)))
.manage(ShutdownToken(Mutex::new(None)))
@@ -901,6 +933,20 @@ fn run_embedded() {
Ok(())
})
.on_window_event(|window, event| {
// "Minimise to tray" (Settings, off by default): closing the main window hides it and keeps Strata running in the tray; Quit in the
// tray menu ends it (#3477). If no tray could be made the window closes as before.
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
if window.label() == "main" {
let prefs = shell::read_prefs();
if prefs.tray {
api.prevent_close();
shell::hide_to_tray(&window.app_handle().clone(), &prefs.language);
if !shell::tray_exists(&window.app_handle().clone()) {
let _ = window.destroy();
}
}
}
}
if let tauri::WindowEvent::Destroyed = event {
stop_host(&window.app_handle().clone());
}
@@ -957,9 +1003,80 @@ fn open_window(app: &tauri::App, port: u16, shell_token: &str) -> Result<(), Box
}
#[cfg(target_os = "linux")]
hook_linux_drops(&window, port, shell_token);
#[cfg(windows)]
hook_renderer_failures(&window, port);
Ok(())
}
/// Milliseconds since the first failure report or window (a clock that only goes forward, for the recovery limit).
#[cfg(not(target_os = "linux"))]
fn uptime_ms() -> u64 {
static START: std::sync::OnceLock<Instant> = std::sync::OnceLock::new();
START.get_or_init(Instant::now).elapsed().as_millis() as u64
}
/// The page's renderer is gone (#5079). Writes it to the run log, and, when the window needs the page again, loads it again through
/// `navigate` after a short wait: the same screen with `?recovery=1` the first time (the page then starts with the sidebar previews
/// closed once), the home screen the second time, and nothing after two in two minutes. The chats are the host's and are not touched.
#[cfg(not(target_os = "linux"))]
fn on_renderer_failure(origin: String, failure: recovery::Failure, detail: String, failed: Option<String>, navigate: impl FnOnce(String) + Send + 'static) {
static GUARD: Mutex<recovery::Guard> = Mutex::new(recovery::Guard::new());
let say = |text: &str| { if let Some(log) = desktoplog::run_log() { log.line("shell", text); } };
say(&format!("the page's {} ({detail}); it was showing {}", failure.name(), failed.as_deref().unwrap_or("an unknown address")));
let plan = match GUARD.lock() {
Ok(mut guard) => recovery::plan(&mut guard, uptime_ms(), &origin, failure, failed.as_deref()),
Err(_) => recovery::Plan::Stop,
};
match plan {
recovery::Plan::Ignore => {}
recovery::Plan::Stop => say("the page's renderer failed again within two minutes of two recoveries; the window is left as it is (Reload in the page loads it again)"),
recovery::Plan::Reload { attempt, url } => {
say(&format!("loading the page again (recovery {attempt}): {url}"));
std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(recovery::RELOAD_DELAY_MS));
navigate(url);
});
}
}
}
/// Windows: WebView2 tells the shell when a process of the page fails; the page's renderer dying is recovered by on_renderer_failure.
#[cfg(windows)]
fn hook_renderer_failures(window: &tauri::WebviewWindow, port: u16) {
use webview2_com::Microsoft::Web::WebView2::Win32::{ICoreWebView2ProcessFailedEventArgs2, COREWEBVIEW2_PROCESS_FAILED_KIND};
use webview2_com::{take_pwstr, ProcessFailedEventHandler};
use windows_core::{Interface, PWSTR};
let target = window.clone();
let origin = format!("http://127.0.0.1:{port}");
let _ = window.with_webview(move |platform| unsafe {
let Ok(core) = platform.controller().CoreWebView2() else { return };
let handler = ProcessFailedEventHandler::create(Box::new(move |sender, args| {
let Some(args) = args else { return Ok(()) };
let mut kind = COREWEBVIEW2_PROCESS_FAILED_KIND::default();
args.ProcessFailedKind(&mut kind)?;
let mut detail = format!("kind {}", kind.0);
if let Ok(more) = args.cast::<ICoreWebView2ProcessFailedEventArgs2>() {
let mut reason = Default::default();
let mut exit_code = 0i32;
if more.Reason(&mut reason).is_ok() { detail = format!("{detail}, reason {}", recovery::reason_name(reason.0)); }
if more.ExitCode(&mut exit_code).is_ok() { detail = format!("{detail}, exit code {exit_code:#x}"); }
}
let failed = sender.and_then(|view| {
let mut source = PWSTR::null();
view.Source(&mut source).ok()?;
Some(take_pwstr(source))
});
let window = target.clone();
on_renderer_failure(origin.clone(), recovery::Failure::from_webview2(kind.0), detail, failed, move |url| {
if let Ok(url) = url.parse::<tauri::Url>() { let _ = window.navigate(url); }
});
Ok(())
}));
let mut token = 0i64;
let _ = core.add_ProcessFailed(&handler, &mut token);
});
}
/// A file:// address from a drag, as a path (percent-escapes decoded); anything else (http, mailto, text) is not a dropped file.
#[cfg_attr(windows, allow(dead_code))]
fn file_uri_to_path(uri: &str) -> Option<String> {
@@ -1113,6 +1230,15 @@ fn run_window_process() -> ! {
eprintln!("[strata] STRATA_WINDOW_TEST_CRASH is set: the window process aborts on purpose.");
std::process::abort();
}
// WebKitGTK aborts while it draws a COLRv1 colour emoji font (fontguard.rs, #4533): this process is told to leave that font out. Only this
// process, and before WebKit starts its own: the host, the engine and the agent keep the fonts the person chose.
{
let env = |k: &str| std::env::var(k).ok();
let dirs = fontguard::font_dirs(&env);
if let Some(conf) = fontguard::config_for(&env, &desktoplog::data_dir().join("fonts"), &dirs, Path::new(fontguard::SYSTEM_CONFIG)) {
std::env::set_var("FONTCONFIG_FILE", conf);
}
}
// The shell is this process's parent: when it is gone (killed, crashed, logged off) the window goes too.
let parent = std::os::unix::process::parent_id();
std::thread::spawn(move || loop {
+260
View File
@@ -0,0 +1,260 @@
//! A page whose renderer dies is brought back, not left on the browser's error page (#5079).
//!
//! WebView2 (Windows) reports a dead renderer through its ProcessFailed event, and WKWebView (macOS) through
//! `webViewWebContentProcessDidTerminate`; until now the shell listened to neither, so the window kept the browser's own "This page has a
//! problem" page, nothing reached the log, and the person's Reload brought back the same chat that had just crashed. Linux does not need it:
//! there the window is its own process and the shell moves to browser mode when it dies (supervisor.rs).
//!
//! This file is only decisions, so it builds and tests everywhere; the platform hooks are in main.rs.
//! - which failures are worth a reload (the page's own renderer; not a GPU process that restarts by itself, not a frame of the page);
//! - how many automatic recoveries are allowed in a window of time (a renderer that dies again at once is not reloaded for ever);
//! - where the reload goes: the same screen first, with the sidebar previews closed once; the home screen the second time.
//! The page reads `?recovery=<n>` in apps/host/src/lib/recovery.ts. The chats are the host's: nothing here touches them.
/// Automatic recoveries allowed inside `WINDOW_MS`; the next failure leaves the window as it is for the person to reload.
pub const MAX_AUTOMATIC: usize = 2;
/// The window of time the limit counts over.
pub const WINDOW_MS: u64 = 120_000;
/// Let the browser start a new renderer before the page is asked for again.
pub const RELOAD_DELAY_MS: u64 = 400;
/// WebView2's COREWEBVIEW2_PROCESS_FAILED_KIND values, by number.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Failure {
BrowserExited,
/// The renderer of the page itself.
RendererExited,
Unresponsive,
/// The renderer of a frame inside the page (a website preview); the page itself is alive.
FrameRendererExited,
Gpu,
Other(i32),
}
impl Failure {
pub fn from_webview2(kind: i32) -> Failure {
match kind {
0 => Failure::BrowserExited,
1 => Failure::RendererExited,
2 => Failure::Unresponsive,
3 => Failure::FrameRendererExited,
6 => Failure::Gpu,
other => Failure::Other(other),
}
}
/// Only the page's own renderer dying leaves a window that has to be loaded again. A GPU process is restarted by the browser, a frame
/// shows its own sad face, an unresponsive page may only be busy, and a dead browser process cannot navigate at all.
pub fn needs_reload(self) -> bool {
self == Failure::RendererExited
}
pub fn name(self) -> String {
match self {
Failure::BrowserExited => "browser process exited".into(),
Failure::RendererExited => "renderer exited".into(),
Failure::Unresponsive => "renderer unresponsive".into(),
Failure::FrameRendererExited => "frame renderer exited".into(),
Failure::Gpu => "gpu process exited".into(),
Failure::Other(n) => format!("process failure kind {n}"),
}
}
}
/// WebView2's COREWEBVIEW2_PROCESS_FAILED_REASON, for the log.
pub fn reason_name(reason: i32) -> &'static str {
match reason {
0 => "unexpected",
1 => "unresponsive",
2 => "terminated",
3 => "crashed",
4 => "launch failed",
5 => "out of memory",
6 => "profile deleted",
_ => "unknown",
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Action {
/// Load the page again; `attempt` is 1 for the first recovery of the window, 2 for the second.
Reload { attempt: usize },
/// Too many in a short time: leave the window alone.
Stop,
}
/// Counts recoveries over a sliding window of time. `now_ms` is any clock that only goes forward (the caller's uptime).
#[derive(Default, Debug)]
pub struct Guard {
times: Vec<u64>,
}
impl Guard {
pub const fn new() -> Guard {
Guard { times: Vec::new() }
}
pub fn on_failure(&mut self, now_ms: u64) -> Action {
self.times.retain(|t| now_ms.saturating_sub(*t) < WINDOW_MS);
if self.times.len() >= MAX_AUTOMATIC {
return Action::Stop;
}
self.times.push(now_ms);
Action::Reload { attempt: self.times.len() }
}
}
/// The address a recovery loads. The first goes back to the screen that died (its `#/...` part) with `?recovery=1`, which makes the page
/// start with the sidebar previews closed once; the second goes to the home screen, in case the screen itself is what crashes.
/// `failed` is the address the window was showing; it is used only when it is on `origin`.
pub fn recovery_url(origin: &str, attempt: usize, failed: Option<&str>) -> String {
let origin = origin.trim_end_matches('/');
let base = format!("{origin}/?recovery={}", attempt.clamp(1, 9));
if attempt > 1 {
return base;
}
let Some(failed) = failed else { return base };
// on this origin: after it comes the end, a path, a query or a fragment (not another digit of a longer port)
let rest = match failed.strip_prefix(origin) {
Some(rest) if rest.is_empty() || rest.starts_with(['/', '?', '#']) => rest,
_ => return base,
};
match rest.find('#') {
Some(at) => {
let hash = &rest[at..];
// only what a page address holds: no spaces, quotes, angle brackets or control characters
let safe = hash.len() < 2048 && hash.starts_with("#/") && hash.chars().all(|c| c.is_ascii_graphic() && !matches!(c, '"' | '<' | '>' | '\\' | '`' | '{' | '}' | '|' | '^'));
if safe { format!("{base}{hash}") } else { base }
}
None => base,
}
}
/// What the shell does about one failure report.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Plan {
/// Not a failure that leaves the window needing a reload: only logged.
Ignore,
/// A reload is due, to `url`.
Reload { attempt: usize, url: String },
/// Too many recent recoveries: the window is left as it is.
Stop,
}
/// One failure report in, the decision out. The platform hooks (main.rs) only collect the facts and carry the decision out.
pub fn plan(guard: &mut Guard, now_ms: u64, origin: &str, failure: Failure, failed: Option<&str>) -> Plan {
if !failure.needs_reload() {
return Plan::Ignore;
}
match guard.on_failure(now_ms) {
Action::Stop => Plan::Stop,
Action::Reload { attempt } => Plan::Reload { attempt, url: recovery_url(origin, attempt, failed) },
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn only_the_pages_own_renderer_is_reloaded() {
assert!(Failure::from_webview2(1).needs_reload());
for kind in [0, 2, 3, 4, 5, 6, 7, 8, 9, 42] {
assert!(!Failure::from_webview2(kind).needs_reload(), "kind {kind}");
}
assert_eq!(Failure::from_webview2(6), Failure::Gpu);
assert_eq!(Failure::from_webview2(1).name(), "renderer exited");
assert_eq!(Failure::from_webview2(42).name(), "process failure kind 42");
}
#[test]
fn reasons_are_named_for_the_log() {
assert_eq!(reason_name(5), "out of memory");
assert_eq!(reason_name(3), "crashed");
assert_eq!(reason_name(99), "unknown");
}
#[test]
fn two_recoveries_in_two_minutes_then_the_window_is_left_alone() {
let mut g = Guard::new();
assert_eq!(g.on_failure(1_000), Action::Reload { attempt: 1 });
assert_eq!(g.on_failure(5_000), Action::Reload { attempt: 2 });
assert_eq!(g.on_failure(6_000), Action::Stop);
assert_eq!(g.on_failure(100_000), Action::Stop);
// the first has left the window: one more is allowed, and it is the second of the window
assert_eq!(g.on_failure(1_000 + WINDOW_MS), Action::Reload { attempt: 2 });
assert_eq!(g.on_failure(1_000 + WINDOW_MS + 1), Action::Stop);
}
#[test]
fn a_renderer_that_dies_later_starts_over_at_the_first_recovery() {
let mut g = Guard::new();
assert_eq!(g.on_failure(0), Action::Reload { attempt: 1 });
assert_eq!(g.on_failure(10), Action::Reload { attempt: 2 });
assert_eq!(g.on_failure(WINDOW_MS + 100), Action::Reload { attempt: 1 });
}
#[test]
fn a_stopped_loop_stays_stopped_until_the_window_has_passed() {
let mut g = Guard::new();
let mut reloads = 0;
// a renderer that dies every second for ten minutes: never more than two reloads in any two-minute window
let mut loaded = Vec::new();
for second in 0..600u64 {
if let Action::Reload { .. } = g.on_failure(second * 1_000) {
reloads += 1;
loaded.push(second);
}
}
assert!(reloads <= 2 * (600 / (WINDOW_MS / 1_000) as usize + 1));
for w in loaded.windows(3) {
assert!(w[2] - w[0] >= WINDOW_MS / 1_000, "three reloads inside one window: {w:?}");
}
}
#[test]
fn the_first_recovery_keeps_the_screen_and_the_second_goes_home() {
let o = "http://127.0.0.1:47831";
assert_eq!(recovery_url(o, 1, Some("http://127.0.0.1:47831/#/chat/s_abc")), "http://127.0.0.1:47831/?recovery=1#/chat/s_abc");
assert_eq!(recovery_url(o, 1, Some("http://127.0.0.1:47831/?recovery=1#/chat/s_abc")), "http://127.0.0.1:47831/?recovery=1#/chat/s_abc");
assert_eq!(recovery_url(o, 2, Some("http://127.0.0.1:47831/#/chat/s_abc")), "http://127.0.0.1:47831/?recovery=2");
assert_eq!(recovery_url(o, 1, None), "http://127.0.0.1:47831/?recovery=1");
assert_eq!(recovery_url("http://127.0.0.1:47831/", 1, None), "http://127.0.0.1:47831/?recovery=1");
}
#[test]
fn an_address_that_is_not_ours_or_not_plain_is_not_carried_over() {
let o = "http://127.0.0.1:47831";
for failed in [
"https://example.com/#/chat/x",
"http://127.0.0.1:47832/#/chat/x",
"http://127.0.0.1:47831/#/chat/x y",
"http://127.0.0.1:47831/#/chat/\"onload=",
"http://127.0.0.1:47831/#/chat/<script>",
"http://127.0.0.1:47831/#nothing",
"http://127.0.0.1:47831/#/chat/\u{e9}",
"chrome-error://chromewebdata/",
] {
assert_eq!(recovery_url(o, 1, Some(failed)), "http://127.0.0.1:47831/?recovery=1", "{failed}");
}
let long = format!("http://127.0.0.1:47831/#/{}", "a".repeat(3000));
assert_eq!(recovery_url(o, 1, Some(&long)), "http://127.0.0.1:47831/?recovery=1");
// a look-alike host is not the origin
assert_eq!(recovery_url(o, 1, Some("http://127.0.0.1:478310/#/chat/x")), "http://127.0.0.1:47831/?recovery=1");
}
#[test]
fn a_failure_report_becomes_one_decision() {
let o = "http://127.0.0.1:47831";
let mut g = Guard::new();
let chat = Some("http://127.0.0.1:47831/#/chat/s_1");
// a GPU process or a frame going down never costs the person their page, and does not use up the allowance
assert_eq!(plan(&mut g, 0, o, Failure::Gpu, chat), Plan::Ignore);
assert_eq!(plan(&mut g, 1, o, Failure::FrameRendererExited, chat), Plan::Ignore);
assert_eq!(plan(&mut g, 2, o, Failure::Unresponsive, chat), Plan::Ignore);
assert_eq!(plan(&mut g, 3, o, Failure::BrowserExited, chat), Plan::Ignore);
assert_eq!(plan(&mut g, 1_000, o, Failure::RendererExited, chat), Plan::Reload { attempt: 1, url: "http://127.0.0.1:47831/?recovery=1#/chat/s_1".into() });
assert_eq!(plan(&mut g, 4_000, o, Failure::RendererExited, chat), Plan::Reload { attempt: 2, url: "http://127.0.0.1:47831/?recovery=2".into() });
assert_eq!(plan(&mut g, 7_000, o, Failure::RendererExited, chat), Plan::Stop);
}
}
+238
View File
@@ -0,0 +1,238 @@
//! The shell's own choices and the two things the host asks of it besides updates (#3402, #3477).
//!
//! `<Strata data folder>/shell-prefs.json` is written by the App Host when the person changes a setting
//! (`apps/host/server/shell-prefs.mjs`) and read here when Strata starts, before any page exists:
//!
//! { "port": 0 | 1024..65535, "tray": false, "language": "en" }
//!
//! * `port`: the port the window's origin uses. 0 is the default, 47831. When the port is busy the shell takes a free one and tells the
//! host (`STRATA_PORT_NOTICE=busy:<wanted>:<used>`), which shows the person a warning: the chats and settings of the page are kept per
//! port, so a different port starts empty.
//! * `tray`: closing the window hides it and keeps Strata running in the tray (Windows, macOS). The tray menu follows `language`.
//! * `/shell/notify` on the loopback bridge shows a desktop notification (an approval is waiting) whether the window is open or hidden.
use std::path::PathBuf;
use serde_json::Value;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Prefs {
pub port: u16,
pub tray: bool,
pub language: String,
}
impl Default for Prefs {
fn default() -> Self {
Prefs { port: 0, tray: false, language: "en".into() }
}
}
pub const LANGUAGES: [&str; 8] = ["en", "zh", "ja", "de", "es", "ru", "pt", "ko"];
pub fn prefs_path() -> PathBuf {
crate::desktoplog::data_dir().join("shell-prefs.json")
}
/// The file's text as choices; anything missing or out of range is the default.
pub fn parse_prefs(text: &str) -> Prefs {
let value: Value = serde_json::from_str(text.trim_start_matches('\u{feff}')).unwrap_or(Value::Null);
let port = value.get("port").and_then(Value::as_u64).filter(|p| (1024..=65535).contains(p)).unwrap_or(0) as u16;
let tray = value.get("tray").and_then(Value::as_bool).unwrap_or(false);
let language = value
.get("language")
.and_then(Value::as_str)
.filter(|l| LANGUAGES.contains(l))
.unwrap_or("en")
.to_string();
Prefs { port, tray, language }
}
pub fn read_prefs() -> Prefs {
std::fs::read_to_string(prefs_path()).map(|t| parse_prefs(&t)).unwrap_or_default()
}
/// The port to ask for: the person's choice, else the app's usual one.
pub fn wanted_port(prefs: &Prefs, usual: u16) -> u16 {
if prefs.port != 0 { prefs.port } else { usual }
}
/// The notice the host shows when the wanted port was taken.
pub fn busy_notice(wanted: u16, used: u16) -> String {
format!("busy:{wanted}:{used}")
}
/// `%41%20b` -> "A b" (a query value from the host). Bad escapes are kept as typed.
pub fn percent_decode(text: &str) -> String {
let bytes = text.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' && i + 2 < bytes.len() {
if let Some(v) = std::str::from_utf8(&bytes[i + 1..i + 3]).ok().and_then(|h| u8::from_str_radix(h, 16).ok()) {
out.push(v);
i += 3;
continue;
}
}
out.push(if bytes[i] == b'+' { b' ' } else { bytes[i] });
i += 1;
}
String::from_utf8_lossy(&out).into_owned()
}
/// The value of `key` in the query of a request path (`/shell/notify?title=..&body=..`).
pub fn query_value(path: &str, key: &str) -> Option<String> {
let query = path.split_once('?')?.1;
query.split('&').find_map(|pair| {
let (k, v) = pair.split_once('=')?;
(k == key).then(|| percent_decode(v))
})
}
/// A short single-line text for a notification: no control characters, bounded.
pub fn clean_text(text: &str, max: usize) -> String {
text.chars().filter(|c| !c.is_control()).take(max).collect::<String>().trim().to_string()
}
/// The tray menu: (open, quit, tooltip), in the app's language.
pub fn tray_labels(language: &str) -> (&'static str, &'static str, &'static str) {
match language {
"zh" => ("打开 Strata", "退出 Strata", "Strata"),
"ja" => ("Strata を開く", "Strata を終了", "Strata"),
"de" => ("Strata öffnen", "Strata beenden", "Strata"),
"es" => ("Abrir Strata", "Salir de Strata", "Strata"),
"ru" => ("Открыть Strata", "Выйти из Strata", "Strata"),
"pt" => ("Abrir o Strata", "Sair do Strata", "Strata"),
"ko" => ("Strata 열기", "Strata 종료", "Strata"),
_ => ("Open Strata", "Quit Strata", "Strata"),
}
}
#[cfg(not(target_os = "linux"))]
mod native {
use tauri::menu::{Menu, MenuItem};
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
use tauri::{AppHandle, Manager};
use tauri_plugin_notification::NotificationExt;
const TRAY_ID: &str = "strata-tray";
fn show_main(app: &AppHandle) {
if let Some(window) = app.get_webview_window("main") {
let _ = window.unminimize();
let _ = window.show();
let _ = window.set_focus();
}
}
/// Hide the window and keep Strata running in the tray (made on the first need, with the menu in the app's language).
pub fn hide_to_tray(app: &AppHandle, language: &str) {
if app.tray_by_id(TRAY_ID).is_none() {
let (open, quit, tip) = super::tray_labels(language);
let built = (|| -> tauri::Result<()> {
let open_item = MenuItem::with_id(app, "open", open, true, None::<&str>)?;
let quit_item = MenuItem::with_id(app, "quit", quit, true, None::<&str>)?;
let menu = Menu::with_items(app, &[&open_item, &quit_item])?;
let mut builder = TrayIconBuilder::with_id(TRAY_ID)
.tooltip(tip)
.menu(&menu)
.show_menu_on_left_click(false)
.on_menu_event(|app, event| match event.id().as_ref() {
"open" => show_main(app),
"quit" => app.exit(0),
_ => {}
})
.on_tray_icon_event(|tray, event| {
if let TrayIconEvent::Click { button: MouseButton::Left, button_state: MouseButtonState::Up, .. } = event {
show_main(tray.app_handle());
}
});
if let Some(icon) = app.default_window_icon() {
builder = builder.icon(icon.clone());
}
builder.build(app)?;
Ok(())
})();
if built.is_err() {
// No tray could be made (a desktop without one): do not hide a window nobody could bring back.
return;
}
}
if let Some(window) = app.get_webview_window("main") {
let _ = window.hide();
}
}
pub fn tray_exists(app: &AppHandle) -> bool {
app.tray_by_id(TRAY_ID).is_some()
}
/// Show a desktop notification; a hidden or minimised window also asks for attention (the taskbar button flashes).
pub fn notify(app: &AppHandle, title: &str, body: &str) -> bool {
if title.is_empty() {
return false;
}
if let Some(window) = app.get_webview_window("main") {
let hidden = !window.is_visible().unwrap_or(true) || window.is_minimized().unwrap_or(false);
if hidden {
let _ = window.request_user_attention(Some(tauri::UserAttentionType::Informational));
}
}
app.notification().builder().title(title).body(body).show().is_ok()
}
}
#[cfg(not(target_os = "linux"))]
pub use native::{hide_to_tray, notify, tray_exists};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_file_gives_choices_and_anything_wrong_is_the_default() {
assert_eq!(parse_prefs(""), Prefs::default());
assert_eq!(parse_prefs("{ not json"), Prefs::default());
let p = parse_prefs(r#"{"port":47900,"tray":true,"language":"de"}"#);
assert_eq!((p.port, p.tray, p.language.as_str()), (47900, true, "de"));
assert_eq!(parse_prefs("\u{feff}{\"port\":47900}").port, 47900);
assert_eq!(parse_prefs(r#"{"port":80}"#).port, 0, "a port below 1024 is not used");
assert_eq!(parse_prefs(r#"{"port":70000}"#).port, 0);
assert_eq!(parse_prefs(r#"{"port":"47831"}"#).port, 0, "a text is not a number");
assert_eq!(parse_prefs(r#"{"tray":"yes","language":"xx"}"#), Prefs::default());
}
#[test]
fn the_wanted_port_is_the_choice_or_the_usual_one() {
assert_eq!(wanted_port(&Prefs::default(), 47831), 47831);
assert_eq!(wanted_port(&Prefs { port: 47900, ..Prefs::default() }, 47831), 47900);
assert_eq!(busy_notice(47831, 51234), "busy:47831:51234");
}
#[test]
fn query_values_are_decoded_and_bounded() {
let path = "/shell/notify?title=Strata%20needs%20your%20answer&body=a%20%26%20b%3F+ok";
assert_eq!(query_value(path, "title").as_deref(), Some("Strata needs your answer"));
assert_eq!(query_value(path, "body").as_deref(), Some("a & b? ok"));
assert_eq!(query_value(path, "missing"), None);
assert_eq!(query_value("/shell/notify", "title"), None);
assert_eq!(percent_decode("100%"), "100%");
assert_eq!(percent_decode("%zz%41"), "%zzA");
assert_eq!(percent_decode("%C3%A4"), "ä");
assert_eq!(clean_text("a\nb\u{7}c", 10), "abc");
assert_eq!(clean_text("abcdef", 3), "abc");
}
#[test]
fn every_language_has_tray_labels() {
for language in LANGUAGES {
let (open, quit, tip) = tray_labels(language);
assert!(!open.is_empty() && !quit.is_empty() && !tip.is_empty(), "{language}");
if language != "en" {
assert_ne!(open, tray_labels("en").0, "{language}");
}
}
assert_eq!(tray_labels("xx"), tray_labels("en"));
}
}
+10
View File
@@ -210,6 +210,16 @@ fn handle(mut stream: TcpStream, app: &AppHandle, token: &str, progress: &Arc<Mu
respond(&mut stream, "202 Accepted", &progress.lock().unwrap().json());
}
("GET", "/app-update/status") => respond(&mut stream, "200 OK", &progress.lock().unwrap().json()),
// The host asks for a desktop notification (an approval is waiting): title and body are in the query, bounded and cleaned here.
("POST", p) if p.starts_with("/shell/notify") => {
let title = crate::shell::clean_text(&crate::shell::query_value(p, "title").unwrap_or_default(), 120);
let body = crate::shell::clean_text(&crate::shell::query_value(p, "body").unwrap_or_default(), 300);
#[cfg(not(target_os = "linux"))]
let shown = crate::shell::notify(app, &title, &body);
#[cfg(target_os = "linux")]
let shown = false;
respond(&mut stream, "200 OK", &serde_json::json!({ "shown": shown }))
}
_ => respond(&mut stream, "404 Not Found", &serde_json::json!({ "error": "unknown route" })),
}
}
+12
View File
@@ -10,3 +10,15 @@ npm run preview:ui
The built UI is included. To rebuild after source edits, run `npm run build`. `preview:ui` uses isolated simulated data; `npm run dev` connects the normal host.
The previous project README is retained in [docs/README-historical.md](docs/README-historical.md). It contains older implementation status and is historical reference.
## Languages
The app speaks English, Chinese, Japanese, German, French, Spanish, Russian, Portuguese and Korean. The English text is the key: `src/locales/en.json`
(with `fix-en.json`, `tune-en.json` and `tools-en.json`) holds every string, and each language has the same four files (`fr.json`, `fix-fr.json`, ...).
- **A new string.** Write it with `tr("...")`, then add it to the English catalog (`node scripts/append-localization.mjs`, or the next `fx` number in
`scripts/make-fix-locales.py`). The language tests fail until every language has it.
- **French.** `python scripts/fr_sync.py missing todo.tsv` lists the English strings that have no French yet, one per line (`id`, a tab, the text).
Translate the second column ("vous", short plain words, one next step; a lone `=` keeps the English), then `python scripts/fr_sync.py apply todo.tsv` merges it
(it also sets the French spacing before `: ; ? !` and the typographic apostrophe) and `python scripts/fr_sync.py check` confirms nothing is missing.
- **Screenshots in a language.** `npm run build`, then `node scripts/language-shots.mjs fr <folder> models settings setup` (hermetic preview host, scratch profile, headless Chrome).
+1 -1
View File
@@ -6,7 +6,7 @@ Implemented 14 October 2026 in the React/Node app under `backups/strata`.
The default is one model at a time. First launch uses the visual `GuidedSetup` funnel: **Welcome → Your model → Prepare → Make it yours → Chat**. Welcome has animated vector artwork and one expert skip at the bottom. Model selection offers friendly version cards, context, RAM/VRAM inventory with an aggregate total, and an existing-folder browser. Unified CPU/GPU memory counts once. Memory verdicts are guidance: **May run slower** does not block selection or Continue. There is no compatibility alternatives panel or picture-reading question at this stage. The preparation screen confirms the download size before **Download model**, then shows engine-reported progress alongside a clickable Chat / Files / Your setup tour. Vision-capable families always prepare their mmproj/encoder files. Preparation does not load the model automatically.
After preparation, users choose **Leave 20% for my work** or **Full performance**, and choose whether to **Read pictures when this model loads** (off initially). The optional longer native calibration measures engine settings for approximately 5–10 minutes. **Tune & start chatting** applies the resource and vision preferences, loads one model, performs a short benchmark, and completes setup only when that model is healthy. Chat opens automatically with starter prompts for a new user; selecting a prompt fills the composer without sending it. Progress, choices and operation IDs survive reloads. Finish later preserves the current stage and keeps completed files available.
After preparation, users choose **Leave room for my work** or **Full performance**, and choose whether to **Read pictures when this model loads** (off initially). The optional longer native calibration measures engine settings for approximately 5–10 minutes. **Tune & start chatting** applies the resource and vision preferences, loads one model, performs a short benchmark, and completes setup only when that model is healthy. Chat opens automatically with starter prompts for a new user; selecting a prompt fills the composer without sending it. Progress, choices and operation IDs survive reloads. Finish later preserves the current stage and keeps completed files available.
Normal Models shows Version, optional picture reading, context, Computer use and Load model. The resource preference can be changed here after onboarding. Image generation is a separate collapsed option. The selected model and context remain visible in the toolbar; saved setups and Manage models are available there. Local GGUF import is a secondary entry in Version. Imported models retain the existing metadata/shard validation and reference their files without copying them.
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Strata — local AI, tuned to one model</title>
<title>Strata</title>
<meta name="description" content="Strata runs one local model, tuned to your hardware." />
<meta name="color-scheme" content="light dark" />
<meta name="theme-color" content="#f9fafb" />
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "strata",
"version": "0.2.3",
"version": "0.2.12",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "strata",
"version": "0.2.3",
"version": "0.2.12",
"dependencies": {
"html-to-image": "^1.11.13",
"katex": "^0.19.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "strata",
"private": true,
"version": "0.2.3",
"version": "0.2.12",
"type": "module",
"description": "Strata \u2014 local AI, tuned to one model. Web shell + engine host.",
"scripts": {
Binary file not shown.
+209
View File
@@ -0,0 +1,209 @@
#!/usr/bin/env node
/**
* AGENT-LOST end to end: a long single-file write through the real agent bridge, the real Harness and a real engine.
*
* STRATA_HARNESS_DIR=<harness folder> node scripts/agent-lost-e2e.mjs [pagoda|long|cut ...]
*
* Scenarios (default: all)
* pagoda the owner's own prompt ("Write me a 100 voxel pagoda in one html file with garden."), thinking Medium, bypass mode
* long a deterministic long write: one file of N numbered lines (E2E_LINES, default 1800, about 22K tokens), thinking Off
* mcp Strata's own tool loop (serve runs an MCP tool itself): a call that finishes, and a call the reply is cut inside
* raw the model server alone: a reply cut inside a tool call, as the client receives it
* cut the same long write with the Harness told a small window (E2E_CUT_WINDOW, default 24000), so the reply budget is
* about 7K tokens and the reply is cut inside the tool call: the turn must say so in words and the agent must carry on
*
* A model server that is already running is used when E2E_BASE_URL is set (nothing is started or stopped). Otherwise this
* starts serve/server.py as its own child with the engine named below and stops it by PID.
*
* Environment: E2E_ENGINE (strata executable), E2E_PACK, E2E_GGUF1, E2E_GGUF2, E2E_MTP, E2E_PROFILE, E2E_LIB (library folder),
* E2E_PORT (18590), E2E_WINDOW (65536), E2E_OUT (result folder), E2E_PYTHON.
*
* It prints one table and writes <E2E_OUT>/result.json with the frames summary of each scenario.
*/
import { spawn } from 'node:child_process';
import { existsSync, mkdtempSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { createAgentBridge } from '../server/agent/agent-bridge.mjs';
const REPO = join(import.meta.dirname, '..', '..', '..');
const env = (name, fallback) => process.env[name] ?? fallback;
const HOME = process.env.HOME ?? '';
const PORT = Number(env('E2E_PORT', '18590'));
const WINDOW = Number(env('E2E_WINDOW', '65536'));
const EXISTING = process.env.E2E_BASE_URL ?? '';
const PYTHON = env('E2E_PYTHON', 'python3');
const OUT = env('E2E_OUT', mkdtempSync(join(tmpdir(), 'strata-agent-lost-')));
const LINES = Number(env('E2E_LINES', '1100'));
const REPEAT = Number(env('E2E_REPEAT', '1'));
const TURN_S = Number(env('E2E_TURN_S', '900'));
const wanted = process.argv.slice(2);
const want = id => wanted.length === 0 || wanted.includes(id);
mkdirSync(OUT, { recursive: true });
if (!process.env.STRATA_HARNESS_DIR) { console.error('STRATA_HARNESS_DIR is not set'); process.exit(2); }
const sleep = ms => new Promise(r => setTimeout(r, ms));
let server = null, bridge = null;
const finish = async (code) => {
try { await bridge?.close(); } catch { /* going away */ }
if (server?.pid) { try { process.kill(-server.pid, 'SIGTERM'); } catch { try { process.kill(server.pid, 'SIGTERM'); } catch { /* gone */ } } }
await sleep(1500);
console.log(`\nresult folder: ${OUT}`);
process.exit(code);
};
process.on('SIGINT', () => finish(130));
process.on('SIGTERM', () => finish(143));
if (!EXISTING) {
const pack = env('E2E_PACK', join(HOME, '026q/pack-iq3s'));
const g1 = env('E2E_GGUF1', join(HOME, 'models/IQ3_S/Qwen3.8-Flash-Next-GSQ-RCO-IQ3_S-00001-of-00002.gguf'));
const g2 = env('E2E_GGUF2', join(HOME, 'models/IQ3_S/Qwen3.8-Flash-Next-GSQ-RCO-IQ3_S-00002-of-00002.gguf'));
const mtp = env('E2E_MTP', join(HOME, 'pr1426/mtp-rt'));
const profile = env('E2E_PROFILE', join(HOME, 'bwm/cnt3/data/expert-profile.bin'));
const exe = env('E2E_ENGINE', join(HOME, 'agentlost/eng/strata'));
const lib = env('E2E_LIB', join(HOME, 'agentlost/eng'));
for (const f of [pack, g1, g2, mtp, exe]) if (!existsSync(f)) { console.error(`missing: ${f}`); process.exit(2); }
const config = join(OUT, 'server.json');
writeFileSync(config, JSON.stringify({
exe, args: ['--pack', pack, '--native', g1, '--ple-gguf', g2, ...(existsSync(profile) ? ['--expert-profile', profile] : []),
'--prefill', 'auto', '--spec-min-p', '0.5', '--mtp', mtp, '--max-context', String(WINDOW), '--kv', 'int8', '--expert-cache', 'auto', '--spec', '4',
...(process.env.E2E_ENGINE_ARGS ? process.env.E2E_ENGINE_ARGS.split(' ').filter(Boolean) : [])],
cwd: REPO, tokenizer: join(pack, 'tokenizer'), model_name: 'qwen3.8-flash-next-iq3_s', log: join(OUT, 'engine.log'), lib_dirs: [lib], port: PORT, gpu: 0,
fit_max_tokens: true,
// Strata's own tool loop (serve/server.py run_with_mcp): one small MCP server, offered only to requests that ask for it (strata_mcp).
mcp_servers: { fake: { command: PYTHON, args: [join(REPO, 'serve', 'mcp_fake_server.py')] } },
}, null, 2));
server = spawn(PYTHON, ['serve/server.py', '--engine', 'strata', '--config', config, '--port', String(PORT), '--no-api-key', '--ready-file', join(OUT, 'ready.json')], {
cwd: REPO, env: { ...process.env, LD_LIBRARY_PATH: `${lib}:${process.env.LD_LIBRARY_PATH ?? ''}`, STRATA_DATA_DIR: join(OUT, 'data'), PYTHONPATH: `${join(REPO, 'tools')}:${REPO}` }, stdio: ['ignore', 'pipe', 'pipe'], detached: true,
});
const sink = [];
server.stdout.on('data', d => sink.push(String(d)));
server.stderr.on('data', d => sink.push(String(d)));
server.on('exit', code => { sink.push(`[server exited ${code}]`); server = null; writeFileSync(join(OUT, 'server.log'), sink.join('')); });
}
const base = EXISTING || `http://127.0.0.1:${PORT}`;
let ready = false;
for (let i = 0; i < 360 && !ready; i++) {
if (!EXISTING && !server) break;
try { ready = (await fetch(`${base}/health`, { signal: AbortSignal.timeout(2000) })).ok; } catch { /* still loading */ }
if (!ready) await sleep(2000);
}
if (!ready) { console.error('the engine did not become ready'); await finish(4); }
const model = (await (await fetch(`${base}/v1/models`)).json()).data[0].id;
console.log(`engine ready: ${model}`);
const rows = [];
const note = (id, ok, detail) => { rows.push({ id, ok, detail }); console.log(`${ok ? 'PASS' : 'FAIL'} ${id} ${detail}`); };
/** The bridge for a window as the host would tell it (the engine's own window stays WINDOW). */
function makeBridge(window, dir) {
mkdirSync(join(OUT, dir), { recursive: true });
return createAgentBridge({
dataDir: join(OUT, dir), env: process.env, startTimeoutMs: 180_000, log: l => console.log(' bridge:', l),
localInstances: () => [{ id: 'e2e', label: 'iq3_s', baseUrl: `${base}/v1`, modelId: model, context: window, slots: 1, key: '' }], connections: () => [],
});
}
bridge = makeBridge(WINDOW, 'agent');
/** One agent turn; every frame is kept in a compact timeline so the live data path can be read afterwards. */
async function turn(name, message, { thinking = 'Medium', mode = 'bypass', chatId = name } = {}) {
const workspace = join(OUT, 'ws-' + name);
mkdirSync(workspace, { recursive: true });
const frames = [];
const t0 = Date.now();
let timedOut = false;
const stopTimer = setTimeout(() => { timedOut = true; void bridge.stop(chatId).catch(() => {}); }, TURN_S * 1000);
await bridge.runTurn({ chatId, message, workspacePath: workspace, mode, thinking, onFrame: f => frames.push({ at: Date.now() - t0, f }) });
clearTimeout(stopTimer);
const seconds = (Date.now() - t0) / 1000;
const byType = {};
for (const { f } of frames) byType[f.type] = (byType[f.type] ?? 0) + 1;
const done = frames.map(x => x.f).filter(f => f.type === 'done').at(-1) ?? null;
const error = frames.map(x => x.f).filter(f => f.type === 'error').at(-1) ?? null;
const prepare = frames.map(x => x.f).filter(f => f.type === 'status' && (f.phase === 'preparing' || f.code === 'preparing-tool'));
const cards = frames.map(x => x.f).filter(f => f.type === 'agent-tool').map(f => f.tool);
const text = frames.map(x => x.f).filter(f => f.type === 'delta').map(f => f.text).join('');
const files = readdirSync(workspace).map(n => ({ name: n, bytes: statSync(join(workspace, n)).size }));
const cutNotes = frames.map(x => x.f).filter(f => f.type === 'delta' && f.noteReason === 'cut').map(f => f.text.trim());
const drafts = frames.map(x => x.f).filter(f => f.type === 'agent-tool-draft').map(f => f.draft);
const writes = drafts.filter(d => d.name === 'write' && d.bytes > 0);
const draftStats = { frames: drafts.length, writeFrames: writes.length, lastWrite: writes.filter(d => !d.done).at(-1) ? { target: writes.filter(d => !d.done).at(-1).target, bytes: writes.filter(d => !d.done).at(-1).bytes, lines: writes.filter(d => !d.done).at(-1).lines } : null, previewSample: writes.filter(d => !d.done).at(-1)?.preview.slice(-80) ?? null };
const writesDone = cards.filter(c => c.name === 'write' && c.state === 'done').length;
const summary = { name, seconds, timedOut, writesDone, cutNotes, draftStats, byType, done, error, files, cards: cards.map(c => ({ name: c.name, state: c.state, target: c.target })), prepareStatuses: prepare.length, firstPrepare: prepare[0] ?? null, lastPrepare: prepare.at(-1) ?? null, text: text.slice(-1200) };
writeFileSync(join(OUT, `turn-${name}.json`), JSON.stringify({ summary, frames: frames.slice(0, 4000) }, null, 1));
return { ...summary, frames, workspace };
}
const longPrompt = `Create the file big.txt in the current folder with the write tool, in ONE call. It must hold exactly ${LINES} lines. Line N is "N: " followed by one short true sentence about a different city of the world (a new city on every line, no repeats, no pattern). Write every line out yourself, do not use a script or a loop, and do not shorten it.`;
if (want('raw')) {
// What the model server itself sends when a reply is cut inside a tool call (no Harness): the stream of tool_calls pieces, then finish_reason "length".
const body = { model, stream: true, max_tokens: Number(env('E2E_RAW_CAP', '700')), temperature: 0.6,
chat_template_kwargs: { enable_thinking: false },
tools: [{ type: 'function', function: { name: 'write', description: 'Write a file.', parameters: { type: 'object', properties: { file_path: { type: 'string' }, content: { type: 'string' } }, required: ['file_path', 'content'] } } }],
messages: [{ role: 'user', content: longPrompt }] };
const res = await fetch(`${base}/v1/chat/completions`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
const text = await res.text();
const events = text.split(String.fromCharCode(10)).filter(l => l.startsWith('data: {')).map(l => JSON.parse(l.slice(6)));
let args = '', name = '', content = '', finish = null, pieces = 0, ids = new Set();
for (const e of events) for (const c of e.choices ?? []) {
for (const tc of c.delta?.tool_calls ?? []) { pieces += 1; if (tc.id) ids.add(tc.id); if (tc.function?.name) name = tc.function.name; args += tc.function?.arguments ?? ''; }
content += c.delta?.content ?? '';
if (c.finish_reason) finish = c.finish_reason;
}
const usage = events.map(e => e.usage).filter(Boolean).at(-1);
writeFileSync(join(OUT, 'raw-sse.txt'), text);
note('raw: a reply cut inside a tool call reaches the client as pieces of the call and finish_reason length', finish === 'length' && name === 'write' && pieces > 5, `finish ${finish}; tool ${name}; ${pieces} call pieces, ${args.length} argument characters, closed JSON ${(() => { try { JSON.parse(args); return true; } catch { return false; } })()}; plain content ${JSON.stringify(content.slice(0, 80))}; ids ${ids.size}; usage ${JSON.stringify(usage)}`);
}
if (want('mcp')) {
// Strata's own tool loop: the server runs the MCP tool itself. A call that finishes is run; a call the reply is cut inside is announced and never run.
const ask = async (content, max_tokens) => {
const res = await fetch(`${base}/v1/chat/completions`, { method: 'POST', headers: { 'content-type': 'application/json' },
body: JSON.stringify({ model, stream: true, strata_mcp: true, max_tokens, temperature: 0.6, chat_template_kwargs: { enable_thinking: false }, messages: [{ role: 'user', content }] }) });
const text = await res.text();
const events = text.split(String.fromCharCode(10)).filter(l => l.startsWith('data: {')).map(l => JSON.parse(l.slice(6)));
const mcp = events.map(e => e.strata_mcp).filter(Boolean);
const finish = events.flatMap(e => e.choices ?? []).map(c => c.finish_reason).filter(Boolean).at(-1);
const said = events.flatMap(e => e.choices ?? []).map(c => c.delta?.content ?? '').join('');
return { mcp, finish, said };
};
const ok = await ask('Use the fake__echo tool to echo the text "hello from the tool", then tell me what it returned.', 600);
note('mcp: a short call is run by the server and answered', ok.mcp.some(m => m.event === 'result' && m.ok) && ok.finish === 'stop', `events ${JSON.stringify(ok.mcp.map(m => m.event))}; finish ${ok.finish}; said ${JSON.stringify(ok.said.slice(-120))}`);
const cut = await ask('Use the fake__echo tool once. Its text must be the sentence "the quick brown fox jumps over the lazy dog" written out 300 times in a row, separated by spaces.', 300);
note('mcp: a call the reply is cut inside is announced and never run (#211), and the stream says length', cut.mcp.some(m => m.event === 'start') && !cut.mcp.some(m => m.event === 'call') && cut.finish === 'length', `events ${JSON.stringify(cut.mcp.map(m => m.event))}; finish ${cut.finish}; said ${JSON.stringify(cut.said.slice(0, 100))}`);
}
if (want('pagoda')) {
const t = await turn('pagoda', 'Write me a 100 voxel pagoda in one html file with garden.');
const html = t.files.find(f => /\.html?$/i.test(f.name));
console.log(` pagoda: ${t.seconds.toFixed(0)} s, frames ${JSON.stringify(t.byType)}, done ${JSON.stringify(t.done && { finishReason: t.done.finishReason, tokensOut: t.done.tokensOut })}, files ${JSON.stringify(t.files)}`);
note('pagoda: a file was written or the turn says why not', !!html || /cut|too long|ran out|limit/i.test(t.text), `file ${html ? html.name + ' ' + html.bytes + ' B' : 'none'}; finish ${t.done?.finishReason}; text tail: ${JSON.stringify(t.text.slice(-240))}`);
}
if (want('long')) for (let rep = 1; rep <= REPEAT; rep++) {
const t = await turn(`long${rep}`, longPrompt, { thinking: 'Off' });
const f = t.files.find(x => x.name === 'big.txt');
const lines = f ? readFileSync(join(t.workspace, 'big.txt'), 'utf8').split(String.fromCharCode(10)).filter(Boolean).length : 0;
console.log(` long${rep}: ${t.seconds.toFixed(0)} s, frames ${JSON.stringify(t.byType)}, done ${JSON.stringify(t.done && { finishReason: t.done.finishReason, tokensOut: t.done.tokensOut })}, draft ${JSON.stringify(t.draftStats)}, cut notes ${JSON.stringify(t.cutNotes)}`);
note(`long${rep}: big.txt written (${LINES} lines asked)`, lines >= LINES * 0.95 || t.writesDone > 0 || t.cutNotes.length > 0 && !!f, `file ${f ? f.bytes + ' B, ' + lines + ' lines' : 'none'}; finish ${t.done?.finishReason}; tokens out ${t.done?.tokensOut}; cards ${JSON.stringify(t.cards.filter(c => c.state !== 'running').map(c => c.name))}; writes done ${t.writesDone}; cut notes ${t.cutNotes.length}; draft frames ${t.draftStats.frames}`);
note(`long${rep}: the turn did not end silently`, !!f || t.cutNotes.length > 0 || t.text.trim().length > 0, `text tail ${JSON.stringify(t.text.slice(-160))}`);
}
if (want('cut')) {
// A small window told to the Harness (E2E_CUT_WINDOW, default 24000; the engine's own stays WINDOW) makes the reply budget about 7K tokens (raised to
// 12K by the fix), so the first reply is cut inside the tool call, as the pagoda's was at 19,660.
await bridge.close();
bridge = makeBridge(Number(env('E2E_CUT_WINDOW', '24000')), 'agent-cut');
const t = await turn('cut', longPrompt, { thinking: 'Off' });
const f = t.files.find(x => x.name === 'big.txt');
const lines = f ? readFileSync(join(t.workspace, 'big.txt'), 'utf8').split(String.fromCharCode(10)).filter(Boolean).length : 0;
console.log(` cut: ${t.seconds.toFixed(0)} s, frames ${JSON.stringify(t.byType)}, done ${JSON.stringify(t.done && { finishReason: t.done.finishReason })}, files ${JSON.stringify(t.files)}, cut notes ${JSON.stringify(t.cutNotes)}, draft ${JSON.stringify(t.draftStats)}`);
note('cut: the chat said the reply was cut, in words', t.cutNotes.length > 0 && t.cutNotes.every(n => /length limit/.test(n)), JSON.stringify(t.cutNotes));
note('cut: the agent went on and the file exists', !!f, `file ${f ? f.bytes + ' B, ' + lines + ' lines' : 'none'}; finish ${t.done?.finishReason}`);
}
writeFileSync(join(OUT, 'result.json'), JSON.stringify(rows, null, 2));
console.log('\n' + rows.map(r => `${r.ok ? 'PASS' : 'FAIL'} ${r.id}`).join('\n'));
await finish(rows.some(r => !r.ok) ? 1 : 0);
+99 -37
View File
@@ -16,10 +16,15 @@ import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const APP = join(ROOT, 'src/styles/app.css');
const STYLE_DIR = join(ROOT, 'src/styles');
const KIT_SRC = join(ROOT, '..', 'ui');
const KIT_COPY = join(ROOT, 'src/styles/kit');
/** The app's own stylesheets, in cascade order: the last one wins. */
const APP_SHEETS = ['app.css', 'liquid.css']
.map((name) => ({ name, css: existsSync(join(STYLE_DIR, name)) ? readFileSync(join(STYLE_DIR, name), 'utf8') : null }))
.filter((sheet) => sheet.css !== null);
const KIT_SCALES = {
radius: new Set(['8px', '14px', '20px', '24px', '9999px', '999px', '50%', '6px', '10px', '12px', '15px', '16px', '2px', '1.5px', 'inherit', '0']),
fontSize: new Set(['11px', '12px', '13px', '14px', '15px', '16px', '17px', '18px', '19px', '22px', '24px', '30px', '32px', '40px', 'inherit']),
@@ -66,9 +71,12 @@ const BLUR_ALLOWED = new Set([
'.menu',
'.telepop',
'.scrim',
".scrim[data-open='true']", // the same scrim, once it is showing
'.dialog-scrim', // the sheet's scrim: it frosts the whole working screen
'.config-savebar', // floating commit controls
'.mobile-bar', // compact navigation controls
'.st-toast--toned',
'.nav', // the navigation layer over the ambient depth layer
]);
/**
@@ -83,6 +91,7 @@ const ELEVATION_ALLOWED = new Set([
'.app[data-nav-open=\'true\'] > .nav', // dismissible navigation sheet
'.st-tab[aria-checked=\'true\']', // selected segmented control
'.nav-new', // primary action affordance
'.nav-new.st-btn--primary', // the same affordance, with its accent recipe
'.st-drawer.sheet',
'.slashmenu',
'.select__pop',
@@ -95,9 +104,12 @@ const ELEVATION_ALLOWED = new Set([
// The jump-to-latest pill sits above a scrolling transcript, so it genuinely
// overlaps moving content — the same test the other entries pass.
'.jumplatest',
'.quote-selection', // the Quote button that floats beside selected words (#4785)
'.st-btn--primary', // the accent fill's own lift
'.st-slider::-webkit-slider-thumb', // a control affordance: the thumb needs to read against its track
'.st-toggle::after', // same: the knob needs to read against its track
'.family-switch button[aria-selected="true"]', // a selected segmented control, like .st-tab
'.native-choice__col .native-resource-choice button[data-selected="true"]', // same
]);
/**
@@ -110,25 +122,61 @@ const CAPSULE_CONTROLS = ['.st-btn', '.st-input', '.st-tab', '.navitem', '.navse
/** Literal colours that are deliberate and documented. */
const DOCUMENTED_LITERALS = ['#05261b', '#12c68e', '#0a7a55'];
/**
* The only selector allowed to paint a gradient: the single ambient depth layer.
*
* The flat-surface rule exists so surfaces read as paper rather than decoration.
* Depth was the one thing it could not express, and the navigation material has
* nothing to sample over a flat fill. A fixed, two-field wash on the page — too
* quiet to read as a gradient at ~1.04:1 — is the smallest thing that makes the
* glass mean anything. It is body-only, it never moves, and both reduced-
* transparency gates remove it. See src/styles/liquid.css §1.
*/
const AMBIENT_SELECTORS = new Set(['body']);
const failures = [];
const notes = [];
const fail = (file, line, message) => failures.push(`${file}:${line} ${message}`);
/* ------------------------------------------------------------ app.css ---- */
/* ------------------------------------------------------------ app sheets -- */
const css = readFileSync(APP, 'utf8');
const lines = css.split('\n');
let selector = '(top level)';
let inKeyframes = false;
/**
* Scan one app stylesheet for the design-system, motion and material rules.
*
* Run over every sheet in the cascade, not only app.css: a rule appended in a
* later file ships just the same, and the "audit palette" block that broke AA on
* the primary button survived precisely because it had never been scanned.
*/
function scanStylesheet(file, css) {
const lines = css.split('\n');
let selector = '(top level)';
let inKeyframes = false;
lines.forEach((raw, i) => {
const line = raw.replace(/\/\*.*?\*\//g, '');
const n = i + 1;
lines.forEach((raw, i) => {
const line = raw.replace(/\/\*.*?\*\//g, '');
const n = i + 1;
const sel = line.match(/^([^@{}\s][^{}]*)\{$/);
// Selectors are usually indented inside a media query or a nested block, so
// the line is trimmed before it is recognised. Without this every indented
// rule was attributed to "(top level)", and the material and elevation
// allowlists — which match on the selector — rejected most of the file.
const sel = line.trim().match(/^([^@{}\s][^{}]*)\{$/);
if (sel) selector = sel[1].trim();
if (/@keyframes/.test(line)) inKeyframes = true;
if (inKeyframes && /^\}/.test(line)) inKeyframes = false;
// A whole rule on one line (`a, b { … }`) never ends with `{`, so it used to
// be attributed to whatever rule ran before it — which is why a shadow on a
// segmented control was reported against `.menu .response-stats`. The rule's
// own selector is used for its declarations.
let selectorForLine = selector;
if (!sel && !inKeyframes) {
const inline = line.trim().match(/^([^@{}][^{}]*?)\s*\{/);
if (inline && /\}/.test(line)) selectorForLine = inline[1].trim();
}
// A one-line `@keyframes name { … }` opens and closes on the same line, so it
// must not switch the tracker on: it never sees a closing brace at column 0,
// and every rule after it was then treated as part of an animation — which
// disabled the one-line selector attribution above.
if (/@keyframes/.test(line) && !/\}/.test(line)) inKeyframes = true;
if (inKeyframes && /^\}/.test(raw)) inKeyframes = false;
for (const [, prop, rawValue] of line.matchAll(/([a-z-]+)\s*:\s*([^;]+);/g)) {
const value = rawValue.trim();
@@ -137,24 +185,24 @@ lines.forEach((raw, i) => {
if (prop === 'border-radius') {
for (const part of value.split(/\s+/)) {
if (!KIT_SCALES.radius.has(part) && !part.startsWith('var(') && !part.includes('%')) {
fail('app.css', n, `off-scale radius "${part}" in ${selector}`);
fail(file, n, `off-scale radius "${part}" in ${selectorForLine}`);
}
}
}
if (prop === 'font-size' && !value.startsWith('var(') && !value.includes('clamp') && !KIT_SCALES.fontSize.has(value)) {
fail('app.css', n, `off-scale font-size "${value}" in ${selector}`);
fail(file, n, `off-scale font-size "${value}" in ${selectorForLine}`);
}
if (prop === 'box-shadow' && !KIT_SCALES.shadow.some((re) => re.test(value))) {
fail('app.css', n, `shadow is not a kit token: "${value}" in ${selector}`);
fail(file, n, `shadow is not a kit token: "${value}" in ${selectorForLine}`);
}
if (/gradient|radial|conic/.test(value) && prop.startsWith('background')) {
fail('app.css', n, `gradient in ${selector} — the system is flat`);
if (/gradient|radial|conic/.test(value) && prop.startsWith('background') && !AMBIENT_SELECTORS.has(selector)) {
fail(file, n, `gradient in ${selectorForLine} — the system is flat`);
}
if (/^(color|background|background-color|border-color)$/.test(prop)) {
const literals = value.match(/#[0-9a-fA-F]{3,8}\b|rgba?\([^)]*\)/g);
if (literals && !value.includes('data:image') && !value.includes('currentColor')
&& !DOCUMENTED_LITERALS.some((d) => value.includes(d))) {
fail('app.css', n, `literal colour "${value}" in ${selector} — use a token`);
fail(file, n, `literal colour "${value}" in ${selectorForLine} — use a token`);
}
}
@@ -163,57 +211,70 @@ lines.forEach((raw, i) => {
// Values must be a --st-material-* token: that is what collapses the blur
// under reduced transparency, so gating cannot be forgotten.
if (prop === 'backdrop-filter' || prop === '-webkit-backdrop-filter') {
if (!/^var\(--st-material-(thin|regular|thick)\)$/.test(value) && !(selector === '.scrim' && value === 'none !important')) {
fail('app.css', n, `blur must use a --st-material-* token (it carries the gating): "${value}"`);
if (!/^var\(--st-material-(thin|regular|thick)\)( !important)?$/.test(value) && !(selector === '.scrim' && value === 'none !important')) {
fail(file, n, `blur must use a --st-material-* token (it carries the gating): "${value}"`);
}
if (!BLUR_ALLOWED.has(selector)) {
fail('app.css', n, `material on "${selector}", which is not an approved surface — it does not overlay moving content`);
if (!BLUR_ALLOWED.has(selectorForLine)) {
fail(file, n, `material on "${selectorForLine}", which is not an approved surface — it does not overlay moving content`);
}
}
if (prop === 'box-shadow' && /var\(--st-sh-(card|bar|pop)\)/.test(value) && !ELEVATION_ALLOWED.has(selector)) {
fail('app.css', n, `elevation shadow on "${selector}" — shadows are reserved for surfaces that overlap content`);
if (prop === 'box-shadow' && /var\(--st-sh-(card|bar|pop)\)/.test(value) && !ELEVATION_ALLOWED.has(selectorForLine)) {
fail(file, n, `elevation shadow on "${selectorForLine}" — shadows are reserved for surfaces that overlap content`);
}
if (prop === 'transition') {
if (/^\d/.test(value) || /(^|[\s,])\d+m?s\b/.test(value)) {
fail('app.css', n, `raw duration in transition — use a --st-t-* token: "${value}"`);
fail(file, n, `raw duration in transition — use a --st-t-* token: "${value}"`);
}
for (const part of value.split(',')) {
const prop0 = part.trim().split(/\s+/)[0];
if (LAYOUT_PROPS.includes(prop0)) {
fail('app.css', n, `transition on layout property "${prop0}" in ${selector}`);
fail(file, n, `transition on layout property "${prop0}" in ${selectorForLine}`);
}
}
}
if (prop === 'transition-property') {
for (const p of value.split(',').map((v) => v.trim())) {
if (LAYOUT_PROPS.includes(p)) fail('app.css', n, `transition on layout property "${p}" in ${selector}`);
if (LAYOUT_PROPS.includes(p)) fail(file, n, `transition on layout property "${p}" in ${selectorForLine}`);
}
}
if (prop === 'animation' || prop === 'animation-name') {
const name = value.match(/^(st-[a-z-]+)/)?.[1];
const infinite = /infinite/.test(value);
if (infinite && name && !INFINITE_OK.has(name)) {
fail('app.css', n, `looping animation "${name}" is not on the allowlist — nothing loops while idle`);
fail(file, n, `looping animation "${name}" is not on the allowlist — nothing loops while idle`);
}
const duration = value.match(/\b(\d+(?:\.\d+)?m?s)\b/)?.[1];
if (duration && !LOOP_DURATION_OK.has(duration)) {
fail('app.css', n, `raw animation duration "${duration}" in ${selector} — use a --st-t-* token`);
fail(file, n, `raw animation duration "${duration}" in ${selectorForLine} — use a --st-t-* token`);
}
}
if (prop === 'animation-duration' && /^\d/.test(value) && !LOOP_DURATION_OK.has(value)) {
fail('app.css', n, `raw animation-duration "${value}" in ${selector}`);
fail(file, n, `raw animation-duration "${value}" in ${selectorForLine}`);
}
}
});
}
});
}
for (const sheet of APP_SHEETS) scanStylesheet(sheet.name, sheet.css);
const css = APP_SHEETS.find((sheet) => sheet.name === 'app.css')?.css ?? '';
const allAppCss = APP_SHEETS.map((sheet) => sheet.css).join('\n');
/* ------------------------------------------------- kit sync + motion scale -- */
for (const file of ['tokens.css', 'components.css']) {
const a = join(KIT_SRC, file);
const b = join(KIT_COPY, file);
if (!existsSync(a) || !existsSync(b)) {
fail('kit', 0, `${file} missing on one side`);
// Some checkouts of this branch do not carry the design-kit source (apps/ui),
// only the synced copy. There is nothing to compare against then, so the check
// reports that instead of failing every run with a condition no one can fix.
if (!existsSync(a)) {
notes.push(`kit copy: ${file} has no source in this checkout — drift check skipped`);
continue;
}
if (!existsSync(b)) {
fail('kit', 0, `${file} missing from src/styles/kit — run npm run sync:kit`);
continue;
}
// The sync rewrites @font-face URLs to absolute; everything else must match.
@@ -223,10 +284,11 @@ for (const file of ['tokens.css', 'components.css']) {
}
}
// Which selectors have been given the capsule radius.
// Which selectors have been given the capsule radius. Checked across the whole
// cascade: a control is a capsule wherever the rule that says so lives.
const capsuled = new Set();
{
const rules = css.replace(/\/\*[\s\S]*?\*\//g, '');
const rules = allAppCss.replace(/\/\*[\s\S]*?\*\//g, '');
for (const [, sel, body] of rules.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
if (!/border-radius:\s*var\(--st-r-pill\)/.test(body)) continue;
for (const part of sel.split(',')) capsuled.add(part.trim());
@@ -263,7 +325,7 @@ const referenced = new Set();
for (const file of [...readdirSync(join(ROOT, 'src'), { recursive: true })].filter((f) => /\.tsx$/.test(String(f)))) {
const full = join(ROOT, 'src', String(file));
if (!statSync(full).isFile()) continue;
for (const m of readFileSync(full, 'utf8').matchAll(/name="([a-z-]+)"/g)) referenced.add(m[1]);
for (const m of readFileSync(full, 'utf8').matchAll(/<(?:Icon|IconBtn)\s[^>]*?\bname="([a-z-]+)"/g)) referenced.add(m[1]);
}
for (const name of referenced) {
if (!symbols.has(name)) fail('src', 0, `icon "${name}" is referenced but not in the sprite — run npm run sync:kit`);
+54
View File
@@ -0,0 +1,54 @@
/**
* Where the Chromium binary the probes drive actually lives.
*
* The probes launched a fixed macOS cache path, so on Windows and Linux they
* printed "No cached Chromium found" and every runtime gate (contrast, measure,
* luminance, motion) was skipped rather than run. Resolution is now a single
* place with the same order everywhere:
*
* 1. STRATA_CHROME — an explicit override, for a CI image or a second Chrome
* 2. the cached Playwright build (macOS, the original platforms)
* 3. an installed browser: Chrome, Edge, Chromium
*
* `requireChromium` exits with the harness message the scripts already used, so
* a machine with no browser at all still fails loudly instead of silently
* passing a gate.
*/
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { homedir } from 'node:os';
const PLAYWRIGHT_MAC = [
join(homedir(), 'Library/Caches/ms-playwright/chromium-1234/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing'),
join(homedir(), 'Library/Caches/ms-playwright/chromium_headless_shell-1234/chrome-headless-shell-mac-arm64/chrome-headless-shell'),
];
const INSTALLED = [
// Windows
'C:/Program Files/Google/Chrome/Application/chrome.exe',
'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe',
'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe',
'C:/Program Files/Microsoft/Edge/Application/msedge.exe',
// macOS
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
// Linux
'/usr/bin/google-chrome',
'/usr/bin/google-chrome-stable',
'/usr/bin/chromium',
'/usr/bin/chromium-browser',
];
export function findChromium() {
const candidates = [process.env.STRATA_CHROME, ...PLAYWRIGHT_MAC, ...INSTALLED].filter(Boolean);
return candidates.find((path) => existsSync(path)) ?? null;
}
export function requireChromium() {
const path = findChromium();
if (!path) {
console.error('No Chromium found. Set STRATA_CHROME, or install Chrome, Edge or Chromium.');
process.exit(2);
}
return path;
}
+26
View File
@@ -0,0 +1,26 @@
// Throwaway: serves the demo host unchanged, except POST /api/chat/stream, which streams a long scripted reply (NDJSON frames like the real host).
import http from 'node:http';
const UP = Number(process.env.UP ?? 18791), PORT = Number(process.env.PORT ?? 18790);
const TOK = Number(process.env.REPLY_TOKENS ?? 12000), REASON = Number(process.env.REASON_TOKENS ?? 3000), RATE = Number(process.env.RATE ?? 400);
const sleep = ms => new Promise(r => setTimeout(r, ms));
const para = i => `### Section ${i}\n\nThis is paragraph ${i} with **bold**, *italic*, \`inline code\` and a link [x](https://example.com/${i}). The measured value is about ${i} units and the results agree with the earlier runs, which is what we expected from the model.
More prose follows here so that a paragraph looks like real writing and not like a stress pattern.\n\n- item one of ${i}\n- item two of ${i}\n\n\`\`\`python\ndef f_${i}(a, b):\n total = a + b * ${i}\n return total\n\`\`\`\n\n| a | b |\n|---|---|\n| ${i} | ${i * 2} |\n\n`;
http.createServer(async (req, res) => {
if (req.method === 'POST' && req.url.startsWith('/api/chat/stream')) {
let raw = ''; for await (const c of req) raw += c;
res.writeHead(200, { 'content-type': 'application/x-ndjson' });
const w = o => res.write(JSON.stringify(o) + '\n');
w({ type: 'start', model: 'Mock', contextMax: 131072, requestId: 'r' + Date.now() });
let sent = 0, i = 0;
const per = Math.max(1, Math.round(RATE / 50));
while (sent < REASON) { w({ type: 'reasoning', text: 'thinking about step ' + (i++) + ' of the problem. '.repeat(1) }); sent += 9; if (sent % per < 9) await sleep(20); }
sent = 0; i = 0;
while (sent < TOK) { const p = para(i++); for (let k = 0; k < p.length; k += 24) { w({ type: 'delta', text: p.slice(k, k + 24) }); sent += 6; if ((sent / 6) % Math.max(1, Math.round(per / 2)) === 0) await sleep(10); } w({ type: 'stats', tokensOut: sent, tps: 90 }); }
w({ type: 'done', tokensIn: 100, tokensOut: sent, tps: 90, durationMs: 1000, usageSource: 'engine' });
return res.end();
}
const up = http.request({ host: '127.0.0.1', port: UP, path: req.url, method: req.method, headers: req.headers }, r => { res.writeHead(r.statusCode, r.headers); r.pipe(res); });
up.on('error', () => { res.writeHead(502); res.end(); });
req.pipe(up);
}).listen(PORT, '127.0.0.1', () => console.log('proxy on', PORT));
+57
View File
@@ -0,0 +1,57 @@
// Measures the chat page's heap, DOM size and main-thread time per streamed reply. Needs a throwaway host (STRATA_REAL_ENGINE=0, port 18791), scripts/chat-growth-proxy.mjs (port 18790) and Chrome.
import { chromium } from 'playwright-core';
import { deflateSync } from 'node:zlib';
import { randomBytes } from 'node:crypto';
import { execSync } from 'node:child_process';
const BASE = process.env.BASE ?? 'http://127.0.0.1:18790';
const TURNS = Number(process.env.TURNS ?? 6), PICS = Number(process.env.PICS ?? 3), MON = process.env.MON !== '0';
const crc = (() => { const t = new Int32Array(256).map((_, n) => { let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; return c; }); return b => { let c = -1; for (const x of b) c = t[(c ^ x) & 255] ^ (c >>> 8); return (c ^ -1) >>> 0; }; })();
function png(w, h) {
const chunk = (type, data) => { const len = Buffer.alloc(4); len.writeUInt32BE(data.length); const body = Buffer.concat([Buffer.from(type), data]); const c = Buffer.alloc(4); c.writeUInt32BE(crc(body)); return Buffer.concat([len, body, c]); };
const raw = Buffer.alloc((w * 3 + 1) * h); for (let y = 0; y < h; y++) { raw[y * (w * 3 + 1)] = 0; randomBytes(w * 3).copy(raw, y * (w * 3 + 1) + 1); }
const ihdr = Buffer.alloc(13); ihdr.writeUInt32BE(w, 0); ihdr.writeUInt32BE(h, 4); ihdr[8] = 8; ihdr[9] = 2;
return Buffer.concat([Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), chunk('IHDR', ihdr), chunk('IDAT', deflateSync(raw, { level: 1 })), chunk('IEND', Buffer.alloc(0))]);
}
const browser = await chromium.launch({ executablePath: 'C:/Program Files/Google/Chrome/Application/chrome.exe', headless: true, args: ['--enable-precise-memory-info', '--js-flags=--expose-gc'] });
const ctx = await browser.newContext({ viewport: { width: 1400, height: 900 } });
const page = await ctx.newPage();
const cdp = await ctx.newCDPSession(page);
await cdp.send('Performance.enable');
await fetch(BASE + '/api/model-providers', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ kind: 'custom', provider: null, name: 'Mock model', baseUrl: 'http://127.0.0.1:18898/v1', protocol: 'openai-chat', apiKey: '', models: [{ id: 'mock-model', name: 'Mock model' }] }) });
await page.goto(BASE + '/'); await page.waitForTimeout(1500);
const skip = page.getByText('Go straight to the app'); await skip.waitFor({ timeout: 10000 }).catch(() => {}); if (await skip.count()) { await skip.click(); await page.waitForTimeout(800); }
async function metrics(tag) {
await cdp.send('HeapProfiler.collectGarbage').catch(() => {});
const m = Object.fromEntries((await cdp.send('Performance.getMetrics')).metrics.map(x => [x.name, x.value]));
const dom = await cdp.send('Memory.getDOMCounters').catch(() => ({}));
const sess = await page.evaluate(async () => { try { const h = { 'x-strata-client': 'ui' }; const l = await (await fetch('/api/chats', { headers: h })).json(); return l.chats.reduce((n, c) => n + (c.bytes ?? 0), 0); } catch { return -1; } });
console.log(`${tag.padEnd(22)} heap=${(m.JSHeapUsedSize / 1048576).toFixed(0)}MB total=${(m.JSHeapTotalSize / 1048576).toFixed(0)}MB nodes=${dom.nodes ?? m.Nodes} listeners=${dom.jsEventListeners ?? '-'} docs=${m.Documents} ls=${(sess / 1e6).toFixed(2)}M`);
return m.JSHeapUsedSize;
}
await page.locator('.chat-model-picker').first().click(); await page.locator('.model-picker-api').first().click(); await page.waitForTimeout(500);
await metrics('start');
const ta = page.locator('.composer textarea');
for (let t = 1; t <= TURNS; t++) {
if (t <= PICS) {
const b64 = png(900, 900).toString('base64');
await page.evaluate(b64 => { const bytes = Uint8Array.from(atob(b64), c => c.charCodeAt(0)); const d = new DataTransfer(); d.items.add(new File([bytes], 'image.png', { type: 'image/png' })); document.querySelector('.composer textarea').dispatchEvent(new ClipboardEvent('paste', { clipboardData: d, bubbles: true, cancelable: true })); }, b64);
await page.waitForTimeout(600);
}
await ta.fill('turn ' + t + ' write a long answer');
await page.getByRole('button', { name: 'Send' }).click();
const t0 = Date.now();
let peak = 0; const cpu0 = (await cdp.send('Performance.getMetrics')).metrics.find(x => x.name === 'TaskDuration').value;
for (;;) {
await page.waitForTimeout(2000);
const m = Object.fromEntries((await cdp.send('Performance.getMetrics')).metrics.map(x => [x.name, x.value]));
peak = Math.max(peak, m.JSHeapUsedSize);
const busy = await page.getByRole('button', { name: /Stop/ }).count();
if (!busy || Date.now() - t0 > 240000) break;
}
const cpu1 = (await cdp.send('Performance.getMetrics')).metrics.find(x => x.name === 'TaskDuration').value;
console.log(` turn ${t}: ${(Date.now() - t0) / 1000}s peak-heap=${(peak / 1048576).toFixed(0)}MB main-thread-busy=${(cpu1 - cpu0).toFixed(1)}s`);
await metrics('after turn ' + t);
}
if (MON) { await page.getByText('Monitor', { exact: true }).first().click().catch(() => console.log('no monitor link')); await page.waitForTimeout(25000); await metrics('monitor 25s'); }
try { console.log(execSync('powershell -NoProfile -c "(Get-Process chrome | Measure-Object WorkingSet64 -Sum).Sum/1MB"').toString().trim(), 'MB chrome working set total'); } catch {}
await browser.close();
+2 -4
View File
@@ -19,13 +19,11 @@ import { chromium } from 'playwright-core';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { homedir } from 'node:os';
import { requireChromium } from './browser.mjs';
const BASE = process.env.STRATA_URL ?? 'http://localhost:5199';
const executablePath = [
join(homedir(), 'Library/Caches/ms-playwright/chromium-1234/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing'),
join(homedir(), 'Library/Caches/ms-playwright/chromium_headless_shell-1234/chrome-headless-shell-mac-arm64/chrome-headless-shell'),
].find((p) => existsSync(p));
const executablePath = requireChromium();
if (!executablePath) {
console.error('No cached Chromium found. Run `npm run shot -- x.png <url>` once to confirm the harness.');
+10 -9
View File
@@ -7,11 +7,9 @@ import { chromium } from 'playwright-core';
import { existsSync, mkdirSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { homedir } from 'node:os';
import { requireChromium } from './browser.mjs';
const executablePath = [
join(homedir(), 'Library/Caches/ms-playwright/chromium-1234/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing'),
join(homedir(), 'Library/Caches/ms-playwright/chromium_headless_shell-1234/chrome-headless-shell-mac-arm64/chrome-headless-shell'),
].find((p) => existsSync(p));
const executablePath = requireChromium();
const BASE = process.env.STRATA_URL ?? 'http://localhost:5199';
const flow = process.argv[2] ?? 'chat';
@@ -1237,14 +1235,17 @@ if (flow === 'unread') {
await page.waitForTimeout(600);
// make the older conversation look updated since it was last read
await page.evaluate(() => {
const raw = JSON.parse(localStorage.getItem('strata.sessions.v1') ?? '[]');
await page.evaluate(async () => {
// The chats are files on the host (owner decision 11-04): read them and save the older one again, as the page would.
const h = { 'x-strata-client': 'ui' };
const list = await (await fetch('/api/chats', { headers: h })).json();
const raw = await Promise.all(list.chats.map(async (c) => (await (await fetch('/api/chats/' + encodeURIComponent(c.id), { headers: h })).json()).session));
const older = raw.filter((s) => s.messages.length > 0).at(-1);
if (older) {
older.updatedAt = new Date(Date.now() + 120000).toISOString();
older.lastReadAt = new Date(Date.now() - 120000).toISOString();
await fetch('/api/chats/' + encodeURIComponent(older.id), { method: 'PUT', headers: h, body: JSON.stringify(older) });
}
localStorage.setItem('strata.sessions.v1', JSON.stringify(raw));
});
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForTimeout(1800);
@@ -1354,7 +1355,7 @@ if (flow === 'branch') {
// remember the original so "untouched" means unchanged by the branch, not a
// hardcoded length
const before = await page.evaluate(() =>
JSON.parse(localStorage.getItem('strata.sessions.v1') ?? '[]')
(async () => { const h = { 'x-strata-client': 'ui' }; const l = await (await fetch('/api/chats', { headers: h })).json(); return Promise.all(l.chats.map(async (c) => (await (await fetch('/api/chats/' + encodeURIComponent(c.id), { headers: h })).json()).session)); })()
.map((s) => ({ title: s.title, roles: s.messages.map((m) => m.role) })));
const originalBefore = before.find((s) => !s.title.includes('branch'));
@@ -1372,7 +1373,7 @@ if (flow === 'branch') {
console.log('from user :', JSON.stringify(await view()), '<- branch must show only the user turn, and the url must follow');
const sessions = await page.evaluate(() =>
JSON.parse(localStorage.getItem('strata.sessions.v1') ?? '[]')
(async () => { const h = { 'x-strata-client': 'ui' }; const l = await (await fetch('/api/chats', { headers: h })).json(); return Promise.all(l.chats.map(async (c) => (await (await fetch('/api/chats/' + encodeURIComponent(c.id), { headers: h })).json()).session)); })()
.map((s) => ({ title: s.title, roles: s.messages.map((m) => m.role) })));
console.log('stored :', JSON.stringify(sessions));
const originalAfter = sessions.find((s) => !s.title.includes('branch'));
+156
View File
@@ -0,0 +1,156 @@
"""French (fr) catalog tooling for the desktop app (#3970).
The French texts live in src/locales/fr.json, fix-fr.json, tune-fr.json and tools-fr.json, next to the other languages.
They are edited by hand (they are the translations, not generated), so a string added to an English catalog later has no
French yet. This script lists and merges those. From apps/host:
python scripts/fr_sync.py missing [out.tsv] write every English string that has no French yet
python scripts/fr_sync.py apply done.tsv merge translations, rewrite the four fr files in English key order
python scripts/fr_sync.py check exit 1 if a French string is missing, empty or breaks a placeholder / tag
The TSV has one string per line: <id><TAB><text>. In the text, a newline is written \\n and a backslash \\\\.
In the "done" file a lone = means "keep the English text as it is" (class names, product names, file names).
The id prefix picks the file: s0001 -> fr.json, fx01 -> fix-fr.json, tune01 -> tune-fr.json, tl01 -> tools-fr.json.
Use "vous", plain short words, a next step, and a no-break space (U+00A0) before : ; ? ! and inside guillemets.
"""
import json
import re
import sys
from pathlib import Path
LOCALES = Path(__file__).resolve().parent.parent / "src" / "locales"
PAIRS = [("en.json", "fr.json"), ("fix-en.json", "fix-fr.json"), ("tune-en.json", "tune-fr.json"), ("tools-en.json", "tools-fr.json")]
SLOT = re.compile(r"\{\d+\}")
TAG = re.compile(r"</?[A-Za-z][^>]*>")
def load(name):
path = LOCALES / name
return json.loads(path.read_text(encoding="utf-8")) if path.exists() else {}
def file_of(identifier):
for en_name, fr_name in PAIRS:
if identifier in load(en_name):
return fr_name
raise SystemExit(f"unknown id {identifier}: it is in no English catalog")
def escape(text):
return text.replace("\\", "\\\\").replace("\n", "\\n")
def unescape(text):
out, i = [], 0
while i < len(text):
if text[i] == "\\" and i + 1 < len(text):
out.append({"n": "\n", "\\": "\\"}.get(text[i + 1], text[i + 1]))
i += 2
else:
out.append(text[i])
i += 1
return "".join(out)
NBSP = " "
def typography(text):
"""French spacing and apostrophes, applied to every translated (not kept) string.
l'image -> l'image with a typographic apostrophe (also before a {0} slot); a no-break space before : ; ? ! % and inside the guillemets."""
text = re.sub(r"(?<=[A-Za-zÀ-ſ])'(?=[A-Za-zÀ-ſ{])", "’", text)
text = re.sub(r"(?<=\S) ([:;?!])", NBSP + r"\1", text)
text = re.sub(r"(?<=[\d}]) %", NBSP + "%", text)
text = re.sub("« ?", "«" + NBSP, text)
text = re.sub(" ?»", NBSP + "»", text)
return text
def problems(identifier, source, text):
found = []
if not text.strip():
found.append("empty")
expected = [slot for slot in SLOT.findall(source) if not (identifier == "s2463" and slot == "{1}")] # the tests allow this one to drop {1} (the plural suffix)
if sorted(expected) != sorted(SLOT.findall(text)):
found.append("placeholders differ")
if sorted(TAG.findall(source)) != sorted(TAG.findall(text)):
found.append("markup differs")
return found
def missing():
todo = {}
for en_name, fr_name in PAIRS:
en, fr = load(en_name), load(fr_name)
for identifier, text in en.items():
if identifier not in fr:
todo[identifier] = text
return todo
def main(argv):
command = argv[1] if len(argv) > 1 else ""
if command == "missing":
todo = missing()
lines = [f"{identifier}\t{escape(text)}" for identifier, text in todo.items()]
if len(argv) > 2:
Path(argv[2]).write_text("\n".join(lines) + ("\n" if lines else ""), encoding="utf-8")
print(f"{len(lines)} strings without French written to {argv[2]}")
else:
print("\n".join(lines))
print(f"{len(lines)} strings without French", file=sys.stderr)
return 0
if command == "apply" and len(argv) > 2:
incoming = {}
for number, line in enumerate(Path(argv[2]).read_text(encoding="utf-8").splitlines(), 1):
if not line.strip():
continue
if "\t" not in line:
raise SystemExit(f"{argv[2]}:{number}: no tab")
identifier, text = line.split("\t", 1)
incoming[identifier.strip()] = text
bad = 0
for en_name, fr_name in PAIRS:
en, fr = load(en_name), load(fr_name)
changed = False
for identifier, source in en.items():
if identifier in incoming:
text = source if incoming[identifier] == "=" else typography(unescape(incoming[identifier]))
for why in problems(identifier, source, text):
print(f"{identifier}: {why}: {source!r} -> {text!r}")
bad += 1
fr[identifier] = text
changed = True
if changed:
ordered = {identifier: fr[identifier] for identifier in en if identifier in fr}
(LOCALES / fr_name).write_text(json.dumps(ordered, ensure_ascii=False, indent=1 if en_name != "en.json" else 2) + "\n", encoding="utf-8")
unknown = [identifier for identifier in incoming if not any(identifier in load(en_name) for en_name, _ in PAIRS)]
for identifier in unknown:
print(f"{identifier}: not in any English catalog")
bad += 1
print(f"applied {len(incoming)} strings, {bad} problems")
return 1 if bad else 0
if command == "check":
bad = 0
for en_name, fr_name in PAIRS:
en, fr = load(en_name), load(fr_name)
for identifier, source in en.items():
if identifier not in fr:
print(f"{fr_name}: {identifier} has no French yet: {source!r}")
bad += 1
continue
for why in problems(identifier, source, fr[identifier]):
print(f"{fr_name}: {identifier}: {why}")
bad += 1
for identifier in fr:
if identifier not in en:
print(f"{fr_name}: {identifier} is not in {en_name}")
bad += 1
print("French catalogs complete" if not bad else f"{bad} problems")
return 1 if bad else 0
print(__doc__)
return 2
if __name__ == "__main__":
raise SystemExit(main(sys.argv))
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env node
/**
* Screenshots of the app screens in one interface language, from a hermetic preview host (simulated hardware and models,
* scratch profile, no engine, no weights). The browser is a throw-away headless Chrome profile.
*
* node scripts/language-shots.mjs <lang> <outDir> [route ...] (needs `npm run build` first)
* example: node scripts/language-shots.mjs fr C:/Users/me/Desktop/fr-screens models settings setup
*
* Env: CHROME (path to Chrome/Edge), WIDTH (default 1280), HEIGHT (default 800).
*/
import { spawn } from 'node:child_process';
import { chromium } from 'playwright-core';
import { existsSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
const root = resolve(import.meta.dirname, '..');
const [, , language = 'fr', outDir = 'language-shots', ...routes] = process.argv;
const screens = routes.length ? routes : ['models', 'settings', 'setup'];
if (!existsSync(join(root, 'dist/index.html'))) { console.error('Run npm run build first.'); process.exit(1); }
const chrome = process.env.CHROME ?? ['C:/Program Files/Google/Chrome/Application/chrome.exe', 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe', '/usr/bin/google-chrome', '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'].find(existsSync);
if (!chrome) { console.error('No Chrome or Edge found; set CHROME.'); process.exit(1); }
const home = mkdtempSync(join(tmpdir(), 'strata-language-shots-'));
const hostPort = 8830 + Math.floor(Math.random() * 40), webPort = hostPort + 100;
const scratch = { STRATA_HOME: home, STRATA_MODELS_DIR: join(home, 'models'), STRATA_WORKSPACE: home, STRATA_REAL_ENGINE: '0', STRATA_NATIVE_REPO: join(home, 'no-native-installation'),
STRATA_SETTINGS_FILE: join(home, 'app-settings.json'), STRATA_COMPONENTS_DIR: join(home, 'components'), LOCALAPPDATA: join(home, 'Local'), APPDATA: join(home, 'Roaming'),
XDG_DATA_HOME: join(home, 'xdg-data'), XDG_STATE_HOME: join(home, 'xdg-state'), XDG_CONFIG_HOME: join(home, 'xdg-config'), STRATA_PORT: String(hostPort) };
const children = [];
const run = (args, extra = {}) => { const child = spawn(process.execPath, args, { cwd: root, env: { ...process.env, ...scratch, ...extra }, stdio: 'ignore' }); children.push(child); return child; };
const stop = () => { for (const child of children) { try { child.kill(); } catch {} } try { rmSync(home, { recursive: true, force: true }); } catch {} };
process.on('exit', stop);
run(['server/index.mjs']);
for (let attempt = 0; ; attempt++) {
try { if ((await fetch(`http://127.0.0.1:${hostPort}/api/health`)).ok) break; } catch {}
if (attempt > 200) { console.error('The preview host did not start.'); process.exit(1); }
await new Promise(done => setTimeout(done, 150));
}
run(['tests/fixtures/onboarding-ui-host.mjs'], { STRATA_UI_PREVIEW: '1', STRATA_ONBOARDING_UPSTREAM: `http://127.0.0.1:${hostPort}`, STRATA_ONBOARDING_PORT: String(webPort), STRATA_ONBOARDING_START: 'ready', STRATA_ONBOARDING_SCENARIO: 'telemetry', STRATA_ONBOARDING_PREVIEW: '1' });
for (let attempt = 0; ; attempt++) {
try { if ((await fetch(`http://127.0.0.1:${webPort}/`)).ok) break; } catch {}
if (attempt > 200) { console.error('The preview page did not start.'); process.exit(1); }
await new Promise(done => setTimeout(done, 150));
}
mkdirSync(resolve(outDir), { recursive: true });
const browser = await chromium.launch({ executablePath: chrome, headless: true, args: ['--no-sandbox', '--disable-gpu', '--force-device-scale-factor=1'] });
const page = await browser.newPage({ viewport: { width: Number(process.env.WIDTH ?? 1280), height: Number(process.env.HEIGHT ?? 800) }, deviceScaleFactor: 1 });
await page.addInitScript(code => { try { localStorage.setItem('strata.prefs.v2', JSON.stringify({ language: code, onboardingSeen: true })); } catch {} }, language);
const problems = [];
page.on('pageerror', error => problems.push(`pageerror: ${error.message}`));
for (const screen of screens) {
await page.goto(`http://127.0.0.1:${webPort}/#/${screen}`, { waitUntil: 'load' });
await page.reload({ waitUntil: 'load' });
await page.waitForTimeout(2500);
const file = join(resolve(outDir), `${language}-${screen}.png`);
await page.screenshot({ path: file });
console.log(`${screen}: ${file} (lang=${await page.evaluate(() => document.documentElement.lang)})`);
}
await browser.close();
if (problems.length) console.log(problems.join('\n'));
stop();
process.exit(0);
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env node
/**
* LOAD-PICS end to end: the Models page of a running host, driven in a real Chrome (DevTools protocol, no packages), pressing "Load model" on a model that is
* downloaded, with Read pictures on and the picture files not on the computer.
*
* HOST_URL=http://127.0.0.1:18700 CHROME=/usr/bin/google-chrome OUT=/tmp/lp-out node scripts/load-pictures-e2e.mjs
*
* It looks at the page, presses the button, and writes down once per half second what the page shows (the progress card's words and bar, any dialog, the job's kind
* and state), until the job ends or LIMIT_S passes. It writes <OUT>/timeline.json and screenshots; the files on disk are checked by the shell script around it.
*
* Environment: HOST_URL (required), CHROME (default /usr/bin/google-chrome), OUT (default ./lp-out), LIMIT_S (default 900), CDP_PORT (default 9333),
* MODEL (the version's name on the page, default "High quality"), BUTTON (default "Load model").
*/
import { spawn } from 'node:child_process';
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
const HOST = process.env.HOST_URL;
if (!HOST) { console.error('HOST_URL is required'); process.exit(2); }
const CHROME = process.env.CHROME ?? '/usr/bin/google-chrome';
const OUT = process.env.OUT ?? './lp-out';
const LIMIT = Number(process.env.LIMIT_S ?? 900) * 1000;
const PORT = Number(process.env.CDP_PORT ?? 9333);
const BUTTON = process.env.BUTTON ?? 'Load model';
mkdirSync(OUT, { recursive: true });
const sleep = ms => new Promise(r => setTimeout(r, ms));
const profile = mkdtempSync(join(tmpdir(), 'lp-chrome-'));
const chrome = spawn(CHROME, ['--headless=new', '--no-sandbox', '--disable-gpu', `--remote-debugging-port=${PORT}`, `--user-data-dir=${profile}`, '--window-size=1400,1100', 'about:blank'], { stdio: 'ignore' });
const finish = code => { try { chrome.kill('SIGTERM'); } catch { /* gone */ } setTimeout(() => { try { rmSync(profile, { recursive: true, force: true }); } catch { /* best effort */ } process.exit(code); }, 800); };
process.on('SIGINT', () => finish(130)); process.on('SIGTERM', () => finish(143));
async function target() {
for (let i = 0; i < 80; i++) {
try { const list = await (await fetch(`http://127.0.0.1:${PORT}/json/list`)).json(); const page = list.find(t => t.type === 'page'); if (page) return page.webSocketDebuggerUrl; } catch { /* not up yet */ }
await sleep(250);
}
throw new Error('Chrome did not start');
}
const ws = new WebSocket(await target());
await new Promise((resolve, reject) => { ws.onopen = resolve; ws.onerror = () => reject(new Error('DevTools socket')); });
let seq = 0; const waiting = new Map();
ws.onmessage = event => { const message = JSON.parse(event.data); const w = waiting.get(message.id); if (w) { waiting.delete(message.id); message.error ? w.reject(new Error(message.error.message)) : w.resolve(message.result); } };
const send = (method, params = {}) => new Promise((resolve, reject) => { const id = ++seq; waiting.set(id, { resolve, reject }); ws.send(JSON.stringify({ id, method, params })); });
const run = async expression => { const r = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true }); if (r.exceptionDetails) throw new Error(r.exceptionDetails.text + ' ' + (r.exceptionDetails.exception?.description ?? '')); return r.result.value; };
const shot = async name => { const r = await send('Page.captureScreenshot', { format: 'png' }); writeFileSync(join(OUT, `${name}.png`), Buffer.from(r.data, 'base64')); };
try {
await send('Page.enable');
await send('Page.navigate', { url: `${HOST}/#/models` });
// the page is up when the Load button is on it and enabled
const ready = `(() => { const b = [...document.querySelectorAll('button')].find(b => b.textContent.trim() === ${JSON.stringify(BUTTON)}); return !!b && !b.disabled; })()`;
let up = false;
for (let i = 0; i < 240 && !up; i++) { up = await run(ready).catch(() => false); if (!up) await sleep(500); }
if (!up) { await shot('never-ready'); throw new Error(`The "${BUTTON}" button never came up`); }
const before = await run(`(() => { const t = document.body.innerText; const box = document.querySelector('.native-image-reading input[type=checkbox]'); return { readPicturesOn: !!box && box.checked, offersFiles: t.includes('Picture reading needs a few more files'), downloadButton: [...document.querySelectorAll('button')].some(b => b.textContent.trim() === 'Download picture-reading files'), modelFiles: document.querySelector('.native-path')?.textContent ?? null, shownModel: document.querySelector('.native-version')?.innerText?.slice(0, 200) ?? null }; })()`);
await shot('1-before');
const baseDialogs = await run("document.querySelectorAll('[role=dialog], dialog[open]').length"); // dialogs the page always has (closed ones the app keeps mounted)
const clicked = await run(`(() => { const b = [...document.querySelectorAll('button')].find(b => b.textContent.trim() === ${JSON.stringify(BUTTON)}); b.click(); return true; })()`);
const started = Date.now();
const timeline = []; let last = '', shots = 0, ended = null;
while (Date.now() - started < LIMIT) {
const seen = await run(`(async () => {
const card = document.querySelector('.native-progress');
const state = await (await fetch('/api/native')).json();
const job = [...(state.jobs ?? [])].reverse()[0] ?? null;
return { card: card ? card.innerText.replace(/\\s+/g, ' ').trim() : null, dialogs: document.querySelectorAll('[role=dialog], dialog[open]').length - ${baseDialogs},
job: job && { startedAt: job.startedAt, kind: job.kind, state: job.state, summary: job.summary, error: job.error, details: job.details ?? null, bar: job.result?.install?.download ?? null },
instance: (state.instances ?? []).map(i => ({ status: i.status, vision: i.vision, note: i.picturesNote ?? null, gpus: i.gpus })),
note: [...document.querySelectorAll('.native-spill-note')].map(n => n.innerText.trim()), error: [...document.querySelectorAll('.inline-error, [role=alert]')].map(n => n.innerText.trim()).filter(Boolean) };
})()`);
const key = JSON.stringify([seen.card, seen.dialogs, seen.job?.kind, seen.job?.state, seen.job?.summary, seen.instance, seen.note, seen.error, Math.floor((seen.job?.bar?.percent ?? 0) / 25)]);
if (key !== last) { last = key; timeline.push({ t: +((Date.now() - started) / 1000).toFixed(1), ...seen }); if (seen.card && shots < 2 && seen.job?.state === 'running') await shot(`2-running-${++shots}`); }
if (seen.job && Date.parse(seen.job.startedAt) >= started - 2000 && seen.job.state !== 'running') { ended = seen.job.state; break; }
await sleep(500);
}
await sleep(1500);
await shot('3-after');
const after = await run(`(() => ({ text: [...document.querySelectorAll('.native-spill-note, .native-ready, .toast, [role=status]')].map(n => n.innerText.trim()).filter(Boolean), buttons: [...document.querySelectorAll('.native-model-actions button')].map(b => b.textContent.trim()), pictureOffer: document.body.innerText.includes('Picture reading needs a few more files') }))()`);
const posted = await run("fetch('/__fixture/posted').then(r => r.ok ? r.json() : null).catch(() => null)"); // only the UI-preview fixture keeps what the page posted
writeFileSync(join(OUT, 'timeline.json'), JSON.stringify({ clicked, posted, before, ended, seconds: +((Date.now() - started) / 1000).toFixed(1), timeline, after }, null, 1));
console.log(JSON.stringify({ ended, seconds: +((Date.now() - started) / 1000).toFixed(1), steps: timeline.length, before, after }));
finish(ended === 'done' ? 0 : 1);
} catch (error) {
console.error(error.message);
writeFileSync(join(OUT, 'error.txt'), String(error.stack ?? error));
finish(2);
}
+2 -4
View File
@@ -25,6 +25,7 @@ import { chromium } from 'playwright-core';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { homedir } from 'node:os';
import { requireChromium } from './browser.mjs';
const BASE = process.env.STRATA_URL ?? 'http://localhost:5199';
/** Anything at or above this projected area is "large". Roughly a 64×64 block. */
@@ -43,10 +44,7 @@ const MIN_THICKNESS = 32;
/** Ceiling for the relative luminance of a large surface, in dark mode. */
const CEILING = 0.15;
const executablePath = [
join(homedir(), 'Library/Caches/ms-playwright/chromium-1234/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing'),
join(homedir(), 'Library/Caches/ms-playwright/chromium_headless_shell-1234/chrome-headless-shell-mac-arm64/chrome-headless-shell'),
].find((p) => existsSync(p));
const executablePath = requireChromium();
if (!executablePath) {
console.error('No cached Chromium found.');
@@ -0,0 +1,63 @@
"""Writes src/locales/sav-<lang>.json: the notice for chats the host could not write (owner wording 11-04, #5192).
Run from apps/host: python scripts/make-chatsave-locales.py
Every entry names all nine languages; the keys are sav01.. in the order of S. sav-en.json is the English source. Placeholders ({0}) must be kept in every
language. A separate file keeps this work from colliding with the other catalogs (the same reason upd-, df2-, pick- and the others exist)."""
import json
import re
from pathlib import Path
LANGS = ["en", "zh", "ja", "de", "es", "ru", "pt", "ko", "fr"]
S = []
def add(en, zh, ja, de, es, ru, pt, ko, fr):
S.append(dict(en=en, zh=zh, ja=ja, de=de, es=es, ru=ru, pt=pt, ko=ko, fr=fr))
add("Some chats could not be saved",
"部分聊天无法保存",
"一部のチャットを保存できませんでした",
"Einige Chats konnten nicht gespeichert werden",
"No se pudieron guardar algunos chats",
"Не удалось сохранить некоторые чаты",
"Alguns chats não puderam ser guardados",
"일부 채팅을 저장하지 못했습니다",
"Certaines conversations n'ont pas pu être enregistrées")
add("Strata's chat storage is full, so the newest changes to {0} chat(s) are only in this window. Delete old chats to make room; they then save by themselves.",
"Strata 的聊天存储空间已满,因此 {0} 个聊天的最新更改目前只保存在此窗口中。请删除旧聊天以腾出空间,之后它们会自动保存。",
"Strata のチャット保存領域がいっぱいのため、{0} 件のチャットの最新の変更はこのウィンドウにしかありません。古いチャットを削除して空きを作ると、自動的に保存されます。",
"Der Chat-Speicher von Strata ist voll, daher gibt es die neuesten Änderungen an {0} Chat(s) nur in diesem Fenster. Löschen Sie alte Chats, um Platz zu schaffen; dann werden sie automatisch gespeichert.",
"El almacenamiento de chats de Strata está lleno, así que los cambios más recientes de {0} chat(s) solo están en esta ventana. Elimina chats antiguos para liberar espacio; después se guardarán solos.",
"Хранилище чатов Strata заполнено, поэтому последние изменения в чатах ({0}) есть только в этом окне. Удалите старые чаты, чтобы освободить место; после этого они сохранятся сами.",
"O armazenamento de chats do Strata está cheio, por isso as alterações mais recentes de {0} chat(s) só existem nesta janela. Apague chats antigos para libertar espaço; depois eles guardam-se sozinhos.",
"Strata의 채팅 저장 공간이 가득 차서 {0}개 채팅의 최신 변경 내용이 이 창에만 있습니다. 오래된 채팅을 삭제해 공간을 확보하세요. 그러면 자동으로 저장됩니다.",
"L'espace de stockage des conversations de Strata est plein : les dernières modifications de {0} conversation(s) ne sont que dans cette fenêtre. Supprimez d'anciennes conversations pour faire de la place ; elles s'enregistreront alors d'elles-mêmes.")
add("Copy unsaved chats",
"复制未保存的聊天",
"未保存のチャットをコピー",
"Nicht gespeicherte Chats kopieren",
"Copiar chats sin guardar",
"Скопировать несохранённые чаты",
"Copiar chats não guardados",
"저장되지 않은 채팅 복사",
"Copier les conversations non enregistrées")
here = Path(__file__).resolve().parent.parent / "src" / "locales"
NBSP = " "
def french(text):
text = re.sub(r"(?<=[A-Za-zÀ-ſ])'(?=[A-Za-zÀ-ſ{])", "’", text)
text = re.sub(r"(?<=\S) ([:;?!])", NBSP + r"\1", text)
return text
for lang in LANGS:
data = {}
for i, entry in enumerate(S):
text = entry[lang]
assert sorted(re.findall(r"\{\d+\}", entry["en"])) == sorted(re.findall(r"\{\d+\}", text)), (lang, entry["en"], text)
data[f"sav{i + 1:02d}"] = french(text) if lang == "fr" else text
with (here / f"sav-{lang}.json").open("w", encoding="utf-8", newline="\n") as handle:
handle.write(json.dumps(data, ensure_ascii=False, indent=1) + "\n")
print("written", len(S), "strings in", len(LANGS), "languages")
+242
View File
@@ -0,0 +1,242 @@
"""Writes src/locales/df2-<lang>.json: the strings of the Drafting choice that came with the DFlash2 option (branch f-0291-df2-option): the plain-words
paragraph under the dropdown, the three lines about the person's graphics card, the "not on this card" and "no internet" lines, and the SSD line of the
This computer panel.
Run from apps/host: python scripts/make-df2-locales.py
Every entry names all nine languages; the keys are df01.. in the order of S. df2-en.json is the English source. Placeholders ({0}) must be kept in every
language. A separate file keeps this work from colliding with the other catalogs (the same reason pick-, tune-, tools-, fix-, gh-, ui- and engine- exist).
The English lines of df03-df07 and df09 are also written in strata_control/drafting.py and strata_control/drafting_fetch.py; tests/drafting.test.mjs keeps the two the same."""
import json
import re
from pathlib import Path
LANGS = ["en", "zh", "ja", "de", "es", "ru", "pt", "ko", "fr"]
S = []
def add(en, zh, ja, de, es, ru, pt, ko, fr):
S.append(dict(en=en, zh=zh, ja=ja, de=de, es=es, ru=ru, pt=pt, ko=ko, fr=fr))
add("MTP is the default. DFlash2 is another way to guess the next words ahead, for Qwen3.8-Flash-Next only. On an RTX PRO 5000 it was 7.8% faster than MTP "
"on 19 short prompts and 1.1% faster on long thinking, and faster on 21 of 23 prompts. On the short prompts the answers were identical.",
"默认使用 MTP。DFlash2 是另一种提前猜测后续词语的方式,仅适用于 Qwen3.8-Flash-Next。在 RTX PRO 5000 上,它在 19 个短提示上比 MTP 快 7.8%,在长篇思考上快 1.1%,"
"并且在 23 个提示中有 21 个更快。在短提示上,回答完全相同。",
"既定では MTP を使います。DFlash2 は次の語を先読みするもう一つの方法で、Qwen3.8-Flash-Next 専用です。RTX PRO 5000 では、短いプロンプト 19 件で MTP より 7.8% 速く、"
"長い思考では 1.1% 速く、23 件中 21 件で速くなりました。短いプロンプトでは回答が同一でした。",
"MTP ist die Voreinstellung. DFlash2 ist eine andere Art, die nächsten Wörter vorauszuraten, nur für Qwen3.8-Flash-Next. Auf einer RTX PRO 5000 war es bei 19 kurzen "
"Prompts 7,8 % schneller als MTP, bei langem Nachdenken 1,1 % schneller und bei 21 von 23 Prompts schneller. Bei den kurzen Prompts waren die Antworten identisch.",
"MTP es la opción predeterminada. DFlash2 es otra forma de adivinar las siguientes palabras por adelantado, solo para Qwen3.8-Flash-Next. En una RTX PRO 5000 fue un 7,8 % "
"más rápido que MTP en 19 prompts cortos y un 1,1 % más rápido en razonamientos largos, y más rápido en 21 de 23 prompts. En los prompts cortos las respuestas fueron idénticas.",
"По умолчанию используется MTP. DFlash2 — другой способ угадывать следующие слова наперёд, только для Qwen3.8-Flash-Next. На RTX PRO 5000 он был на 7,8 % быстрее MTP "
"на 19 коротких запросах и на 1,1 % быстрее на длинных рассуждениях, а на 21 из 23 запросов — быстрее. На коротких запросах ответы были идентичны.",
"MTP é o padrão. DFlash2 é outra forma de adivinhar as próximas palavras antecipadamente, só para o Qwen3.8-Flash-Next. Numa RTX PRO 5000 foi 7,8 % mais rápido que o MTP "
"em 19 prompts curtos e 1,1 % mais rápido em raciocínios longos, e mais rápido em 21 de 23 prompts. Nos prompts curtos as respostas foram idênticas.",
"기본값은 MTP입니다. DFlash2는 다음 단어를 미리 추측하는 또 다른 방식이며 Qwen3.8-Flash-Next 전용입니다. RTX PRO 5000에서 짧은 프롬프트 19개에서는 MTP보다 7.8% 빨랐고, "
"긴 사고 과정에서는 1.1% 빨랐으며, 23개 중 21개 프롬프트에서 더 빨랐습니다. 짧은 프롬프트에서는 답변이 동일했습니다.",
"MTP est le réglage par défaut. DFlash2 est une autre façon de deviner à l'avance les mots suivants, uniquement pour Qwen3.8-Flash-Next. Sur une RTX PRO 5000, il a été 7,8 % "
"plus rapide que MTP sur 19 prompts courts et 1,1 % plus rapide en réflexion longue, et plus rapide sur 21 prompts sur 23. Sur les prompts courts, les réponses étaient identiques.")
add("It downloads about 1.2 GB once, works with one graphics card and one request at a time, and uses MTP otherwise.",
"它会一次性下载约 1.2 GB,仅在使用一张显卡且一次处理一个请求时有效,其他情况下使用 MTP。",
"約 1.2 GB を一度だけダウンロードします。グラフィックスカード 1 枚で一度に 1 件のリクエストを処理するときに動作し、それ以外では MTP を使います。",
"Es lädt einmalig etwa 1,2 GB herunter, funktioniert mit einer Grafikkarte und einer Anfrage zur Zeit und nutzt sonst MTP.",
"Descarga unos 1,2 GB una sola vez, funciona con una tarjeta gráfica y una solicitud a la vez, y en los demás casos usa MTP.",
"Один раз скачивает около 1,2 ГБ, работает с одной видеокартой и одним запросом за раз, в остальных случаях используется MTP.",
"Baixa cerca de 1,2 GB uma única vez, funciona com uma placa de vídeo e um pedido de cada vez, e nos outros casos usa o MTP.",
"약 1.2 GB를 한 번만 내려받으며, 그래픽 카드 한 장과 한 번에 하나의 요청에서 작동하고, 그 외에는 MTP를 사용합니다.",
"Il télécharge environ 1,2 Go une seule fois, fonctionne avec une carte graphique et une requête à la fois, et utilise MTP sinon.")
add("On graphics cards like yours, DFlash2 was faster than MTP on some kinds of text and slower on others in our tests.",
"在与您类似的显卡上,我们的测试中 DFlash2 在某些类型的文本上比 MTP 快,在另一些上更慢。",
"お使いのようなグラフィックスカードでは、私たちのテストで DFlash2 は MTP より速い種類の文章と遅い種類の文章がありました。",
"Auf Grafikkarten wie Ihrer war DFlash2 in unseren Tests bei manchen Textarten schneller als MTP und bei anderen langsamer.",
"En tarjetas gráficas como la suya, en nuestras pruebas DFlash2 fue más rápido que MTP con algunos tipos de texto y más lento con otros.",
"На видеокартах, подобных вашей, в наших тестах DFlash2 был быстрее MTP на одних видах текста и медленнее на других.",
"Em placas de vídeo como a sua, nos nossos testes o DFlash2 foi mais rápido que o MTP em alguns tipos de texto e mais lento em outros.",
"고객님과 비슷한 그래픽 카드에서 저희 테스트 결과 DFlash2는 일부 유형의 글에서는 MTP보다 빨랐고 다른 유형에서는 더 느렸습니다.",
"Sur des cartes graphiques comme la vôtre, DFlash2 a été plus rapide que MTP sur certains types de texte et plus lent sur d'autres lors de nos tests.")
add("On graphics cards like yours, DFlash2 was slower than MTP in our tests.",
"在与您类似的显卡上,我们的测试中 DFlash2 比 MTP 更慢。",
"お使いのようなグラフィックスカードでは、私たちのテストで DFlash2 は MTP より遅くなりました。",
"Auf Grafikkarten wie Ihrer war DFlash2 in unseren Tests langsamer als MTP.",
"En tarjetas gráficas como la suya, en nuestras pruebas DFlash2 fue más lento que MTP.",
"На видеокартах, подобных вашей, в наших тестах DFlash2 был медленнее MTP.",
"Em placas de vídeo como a sua, nos nossos testes o DFlash2 foi mais lento que o MTP.",
"고객님과 비슷한 그래픽 카드에서 저희 테스트 결과 DFlash2는 MTP보다 느렸습니다.",
"Sur des cartes graphiques comme la vôtre, DFlash2 a été plus lent que MTP lors de nos tests.")
add("DFlash2 has not been tested on your graphics card yet.",
"DFlash2 尚未在您的显卡上测试过。",
"DFlash2 はまだお使いのグラフィックスカードでテストされていません。",
"DFlash2 wurde auf Ihrer Grafikkarte noch nicht getestet.",
"DFlash2 todavía no se ha probado en su tarjeta gráfica.",
"DFlash2 ещё не проверялся на вашей видеокарте.",
"O DFlash2 ainda não foi testado na sua placa de vídeo.",
"DFlash2는 아직 고객님의 그래픽 카드에서 테스트되지 않았습니다.",
"DFlash2 n'a pas encore été testé sur votre carte graphique.")
add("Drafting: MTP (DFlash2 isn't available on this graphics card)",
"预测方式:MTP(DFlash2 不适用于这张显卡)",
"先読み:MTP(DFlash2 はこのグラフィックスカードでは使えません)",
"Vorausraten: MTP (DFlash2 ist auf dieser Grafikkarte nicht verfügbar)",
"Predicción: MTP (DFlash2 no está disponible en esta tarjeta gráfica)",
"Упреждающая генерация: MTP (DFlash2 недоступен на этой видеокарте)",
"Previsão: MTP (o DFlash2 não está disponível nesta placa de vídeo)",
"미리 추측: MTP (이 그래픽 카드에서는 DFlash2를 사용할 수 없습니다)",
"Anticipation : MTP (DFlash2 n'est pas disponible sur cette carte graphique)")
add("There is no internet connection, so the DFlash2 drafter can't be downloaded now.",
"没有网络连接,因此现在无法下载 DFlash2 草稿模型。",
"インターネットに接続されていないため、今は DFlash2 のドラフターをダウンロードできません。",
"Es besteht keine Internetverbindung, deshalb kann der DFlash2-Drafter jetzt nicht heruntergeladen werden.",
"No hay conexión a internet, así que ahora no se puede descargar el borrador de DFlash2.",
"Нет подключения к интернету, поэтому сейчас нельзя скачать черновую модель DFlash2.",
"Não há ligação à internet, por isso não é possível transferir agora o rascunhador do DFlash2.",
"인터넷에 연결되어 있지 않아 지금은 DFlash2 드래프터를 내려받을 수 없습니다.",
"Il n'y a pas de connexion internet, le drafter DFlash2 ne peut donc pas être téléchargé pour le moment.")
add("SSD: not needed",
"SSD:不需要", "SSD:不要", "SSD: nicht nötig", "SSD: no hace falta", "SSD: не нужен", "SSD: não é necessário", "SSD: 필요 없음", "SSD : pas nécessaire")
add("DFlash2 answers one request at a time, so Strata set this model to one request at a time.",
"DFlash2 一次只处理一个请求,因此 Strata 将此模型设置为一次一个请求。",
"DFlash2 は一度に 1 件のリクエストしか処理しないため、Strata はこのモデルを一度に 1 件に設定しました。",
"DFlash2 beantwortet eine Anfrage nach der anderen, deshalb hat Strata dieses Modell auf eine Anfrage zur Zeit gestellt.",
"DFlash2 responde una solicitud a la vez, así que Strata configuró este modelo para una solicitud a la vez.",
"DFlash2 отвечает на один запрос за раз, поэтому Strata настроила эту модель на один запрос за раз.",
"O DFlash2 responde a um pedido de cada vez, por isso o Strata configurou este modelo para um pedido de cada vez.",
"DFlash2는 한 번에 하나의 요청만 처리하므로 Strata가 이 모델을 한 번에 하나의 요청으로 설정했습니다.",
"DFlash2 répond à une requête à la fois, Strata a donc réglé ce modèle sur une requête à la fois.")
add("Drafting: off (no drafter is downloaded yet)",
"预测草稿:关闭(尚未下载任何草稿模型)",
"先読み:オフ(ドラフトモデルがまだダウンロードされていません)",
"Vorausraten: aus (noch kein Entwurfsbaustein heruntergeladen)",
"Predicción: desactivada (aún no hay ningún predictor descargado)",
"Предсказание: выключено (черновая модель ещё не скачана)",
"Previsão: desativada (nenhum preditor foi baixado ainda)",
"초안 예측: 꺼짐(초안 모델이 아직 내려받아지지 않았습니다)",
"Anticipation : désactivée (aucun module d'anticipation n'est encore téléchargé)")
add("Drafting: DFlash2 (the MTP drafter isn't downloaded yet)",
"预测草稿:DFlash2(MTP 草稿模型尚未下载)",
"先読み:DFlash2(MTP のドラフトモデルはまだダウンロードされていません)",
"Vorausraten: DFlash2 (der MTP-Entwurfsbaustein ist noch nicht heruntergeladen)",
"Predicción: DFlash2 (el predictor de MTP aún no está descargado)",
"Предсказание: DFlash2 (черновая модель MTP ещё не скачана)",
"Previsão: DFlash2 (o preditor do MTP ainda não foi baixado)",
"초안 예측: DFlash2(MTP 초안 모델이 아직 내려받아지지 않았습니다)",
"Anticipation : DFlash2 (le module d'anticipation MTP n'est pas encore téléchargé)")
add("Choosing DFlash2 downloads its drafter: about {0} GB, once.",
"选择 DFlash2 会下载它的草稿模型:约 {0} GB,只下载一次。",
"DFlash2 を選ぶと、そのドラフトモデルを一度だけダウンロードします(約 {0} GB)。",
"Wenn Sie DFlash2 wählen, wird sein Entwurfsbaustein heruntergeladen: etwa {0} GB, einmalig.",
"Elegir DFlash2 descarga su predictor: unos {0} GB, una sola vez.",
"Если выбрать DFlash2, будет скачана его черновая модель: около {0} ГБ, один раз.",
"Escolher o DFlash2 baixa o preditor dele: cerca de {0} GB, uma vez.",
"DFlash2를 고르면 그 초안 모델을 한 번 내려받습니다(약 {0}GB).",
"Choisir DFlash2 télécharge son module d'anticipation : environ {0} Go, une seule fois.")
add("Choosing MTP downloads its drafter: about {0} GB, once.",
"选择 MTP 会下载它的草稿模型:约 {0} GB,只下载一次。",
"MTP を選ぶと、そのドラフトモデルを一度だけダウンロードします(約 {0} GB)。",
"Wenn Sie MTP wählen, wird sein Entwurfsbaustein heruntergeladen: etwa {0} GB, einmalig.",
"Elegir MTP descarga su predictor: unos {0} GB, una sola vez.",
"Если выбрать MTP, будет скачана его черновая модель: около {0} ГБ, один раз.",
"Escolher o MTP baixa o preditor dele: cerca de {0} GB, uma vez.",
"MTP를 고르면 그 초안 모델을 한 번 내려받습니다(약 {0}GB).",
"Choisir MTP télécharge son module d'anticipation : environ {0} Go, une seule fois.")
add("The DFlash2 drafter could not be downloaded, so the model starts without a drafter. Loading the model…",
"无法下载 DFlash2 草稿模型,因此模型将不使用草稿模型启动。正在加载模型…",
"DFlash2 のドラフトモデルをダウンロードできなかったため、ドラフトモデルなしで起動します。モデルを読み込んでいます…",
"Der DFlash2-Entwurfsbaustein konnte nicht heruntergeladen werden, deshalb startet das Modell ohne Entwurfsbaustein. Das Modell wird geladen …",
"No se pudo descargar el predictor de DFlash2, así que el modelo arranca sin predictor. Cargando el modelo…",
"Не удалось скачать черновую модель DFlash2, поэтому модель запускается без черновой модели. Загружается модель…",
"Não foi possível baixar o preditor do DFlash2, então o modelo inicia sem preditor. Carregando o modelo…",
"DFlash2 초안 모델을 내려받지 못해 초안 모델 없이 시작합니다. 모델을 불러오는 중…",
"Le module d'anticipation DFlash2 n'a pas pu être téléchargé, le modèle démarre donc sans module d'anticipation. Chargement du modèle…")
add("Downloading the MTP drafter…",
"正在下载 MTP 草稿模型…",
"MTP のドラフトモデルをダウンロードしています…",
"MTP-Entwurfsbaustein wird heruntergeladen …",
"Descargando el predictor de MTP…",
"Скачивается черновая модель MTP…",
"Baixando o preditor do MTP…",
"MTP 초안 모델을 내려받는 중…",
"Téléchargement du module d'anticipation MTP…")
add("The MTP drafter could not be downloaded.",
"无法下载 MTP 草稿模型。",
"MTP のドラフトモデルをダウンロードできませんでした。",
"Der MTP-Entwurfsbaustein konnte nicht heruntergeladen werden.",
"No se pudo descargar el predictor de MTP.",
"Не удалось скачать черновую модель MTP.",
"Não foi possível baixar o preditor do MTP.",
"MTP 초안 모델을 내려받지 못했습니다.",
"Le module d'anticipation MTP n'a pas pu être téléchargé.")
add("Drafting: MTP (DFlash2 didn't start)",
"预测草稿:MTP(DFlash2 未能启动)",
"先読み:MTP(DFlash2 は起動しませんでした)",
"Vorausraten: MTP (DFlash2 ist nicht gestartet)",
"Predicción: MTP (DFlash2 no arrancó)",
"Предсказание: MTP (DFlash2 не запустился)",
"Previsão: MTP (o DFlash2 não iniciou)",
"초안 예측: MTP(DFlash2가 시작되지 않았습니다)",
"Anticipation : MTP (DFlash2 n'a pas démarré)")
add("Auto uses the all-cards split engine with the MTP draft layer: about {0} GB to download, once.",
"自动模式使用全部显卡的拆分引擎和 MTP 草稿层:约 {0} GB,只下载一次。",
"自動では、全カードのスプリットエンジンと MTP ドラフト層を使います。約 {0} GB を一度だけダウンロードします。",
"Automatisch nutzt die Alle-Karten-Split-Engine mit der MTP-Entwurfsschicht: etwa {0} GB, einmalig herunterzuladen.",
"Automático usa el motor de reparto entre todas las tarjetas con la capa de borrador MTP: unos {0} GB, una sola vez.",
"Авто использует движок с разделением по всем картам и черновой слой MTP: около {0} ГБ, скачивается один раз.",
"O automático usa o motor de divisão entre todas as placas com a camada de rascunho MTP: cerca de {0} GB, uma vez.",
"자동은 모든 카드를 쓰는 분할 엔진과 MTP 초안 층을 사용합니다(약 {0}GB, 한 번만 내려받음).",
"Auto utilise le moteur réparti sur toutes les cartes avec la couche de brouillon MTP : environ {0} Go à télécharger, une seule fois.")
add("The all-cards split engine could not start, so the normal layer split runs.",
"全显卡拆分引擎未能启动,因此改用常规的分层拆分。",
"全カードのスプリットエンジンを起動できなかったため、通常のレイヤー分割で動作しています。",
"Die Alle-Karten-Split-Engine konnte nicht starten, deshalb läuft die normale Schichtaufteilung.",
"El motor de reparto entre todas las tarjetas no pudo arrancar, así que se usa el reparto normal por capas.",
"Движок с разделением по всем картам не запустился, поэтому работает обычное послойное разделение.",
"O motor de divisão entre todas as placas não conseguiu iniciar, então a divisão normal por camadas está em uso.",
"모든 카드를 쓰는 분할 엔진을 시작하지 못해 일반 레이어 분할로 실행합니다.",
"Le moteur réparti sur toutes les cartes n'a pas pu démarrer, la répartition normale par couches est donc utilisée.")
add("Drafting: off (the cards were short of memory)",
"预测草稿:关闭(显卡内存不足)",
"先読み:オフ(グラフィックスカードのメモリが不足していました)",
"Vorausraten: aus (den Karten fehlte Speicher)",
"Predicción: desactivada (a las tarjetas les faltaba memoria)",
"Предсказание: выключено (на картах не хватило памяти)",
"Previsão: desligada (faltou memória nas placas)",
"초안 예측: 꺼짐(카드 메모리가 부족했습니다)",
"Anticipation : désactivée (les cartes manquaient de mémoire)")
add("On your Mac, DFlash2 was faster than MTP on this model in our tests.",
"在您的 Mac 上,我们的测试中 DFlash2 在此模型上比 MTP 更快。",
"お使いの Mac では、私たちのテストでこのモデルにおいて DFlash2 は MTP より速くなりました。",
"Auf Ihrem Mac war DFlash2 bei diesem Modell in unseren Tests schneller als MTP.",
"En su Mac, en nuestras pruebas DFlash2 fue más rápido que MTP con este modelo.",
"На вашем Mac в наших тестах DFlash2 с этой моделью был быстрее MTP.",
"No seu Mac, nos nossos testes o DFlash2 foi mais rápido que o MTP com este modelo.",
"고객님의 Mac에서 저희 테스트 결과 이 모델에서는 DFlash2가 MTP보다 빨랐습니다.",
"Sur votre Mac, DFlash2 a été plus rapide que MTP avec ce modèle lors de nos tests.")
add("DFlash2 is off by default on this graphics card for now; choose it in Drafting to use it.",
"目前在这张显卡上 DFlash2 默认关闭;如需使用,请在“预测草稿”中选择它。",
"現在、このグラフィックスカードでは DFlash2 は既定でオフです。使うには「先読み」で選んでください。",
"DFlash2 ist auf dieser Grafikkarte vorerst standardmäßig aus; wählen Sie es unter „Vorausraten“ aus, um es zu nutzen.",
"DFlash2 está desactivado de forma predeterminada en esta tarjeta gráfica por ahora; elíjalo en Predicción para usarlo.",
"На этой видеокарте DFlash2 пока выключен по умолчанию; чтобы использовать его, выберите его в разделе «Предсказание».",
"O DFlash2 está desligado por predefinição nesta placa de vídeo por agora; escolha-o em Previsão para o usar.",
"현재 이 그래픽 카드에서는 DFlash2가 기본적으로 꺼져 있습니다. 사용하려면 ‘초안 예측’에서 선택하세요.",
"DFlash2 est désactivé par défaut sur cette carte graphique pour le moment ; choisissez-le dans Anticipation pour l'utiliser.")
here = Path(__file__).resolve().parent.parent / "src" / "locales"
NBSP = " "
def french(text):
text = re.sub(r"(?<=[A-Za-zÀ-ſ])'(?=[A-Za-zÀ-ſ{])", "’", text)
text = re.sub(r"(?<=\S) ([:;?!])", NBSP + r"\1", text)
text = re.sub(r"(?<=[\d}]) %", NBSP + "%", text)
return text
for lang in LANGS:
data = {}
for i, entry in enumerate(S):
text = entry[lang]
assert sorted(re.findall(r"\{\d+\}", entry["en"])) == sorted(re.findall(r"\{\d+\}", text)), (lang, entry["en"], text)
data[f"df{i + 1:02d}"] = french(text) if lang == "fr" else text
with (here / f"df2-{lang}.json").open("w", encoding="utf-8", newline="\n") as handle:
handle.write(json.dumps(data, ensure_ascii=False, indent=1) + "\n")
print("written", len(S), "strings in", len(LANGS), "languages")
+226
View File
@@ -0,0 +1,226 @@
"""Writes src/locales/engine-<lang>.json: the strings of Settings > Engine (the list of installed engines: switch, delete) and the words that now point to
About for updating (branch f-0291-engine-tab). Run from apps/host: python scripts/make-engine-locales.py
Every entry names all nine languages; the keys are et01.. in the order of S. engine-en.json is the English source. Placeholders ({0}, {1}) must be kept in
every language. A separate file keeps this work from colliding with the other catalogs (the same reason tune-, tools-, fix-, gh- and ui- exist)."""
import json
import re
from pathlib import Path
LANGS = ["en", "zh", "ja", "de", "es", "ru", "pt", "ko", "fr"]
S = []
def add(en, zh, ja, de, es, ru, pt, ko, fr):
S.append(dict(en=en, zh=zh, ja=ja, de=de, es=es, ru=ru, pt=pt, ko=ko, fr=fr))
# ---- the page
add("Installed engines",
"已安装的引擎", "インストール済みのエンジン", "Installierte Engines", "Motores instalados", "Установленные движки", "Motores instalados", "설치된 엔진", "Moteurs installés")
add("Switch to an older engine to test something, then switch back to the newest. Updating to the latest is in About.",
"切换到较旧的引擎来测试某些内容,然后再切换回最新的。更新到最新版本请前往“关于”。",
"古いエンジンに切り替えて試し、終わったら最新のエンジンに戻せます。最新版への更新は「情報」で行います。",
"Wechsle zu einer älteren Engine, um etwas zu testen, und danach zurück zur neuesten. Das Update auf die neueste Version findest du unter „Über“.",
"Cambia a un motor anterior para probar algo y vuelve después al más reciente. La actualización a la última versión está en «Acerca de».",
"Переключитесь на более старый движок, чтобы что-то проверить, а потом вернитесь на самый новый. Обновление до последней версии — в разделе «О программе».",
"Troque para um motor mais antigo para testar algo e depois volte ao mais recente. A atualização para a versão mais recente fica em «Sobre».",
"이전 엔진으로 바꿔 테스트한 뒤 다시 최신 엔진으로 돌아올 수 있습니다. 최신 버전으로의 업데이트는 '정보'에서 합니다.",
"Passez à un moteur plus ancien pour tester quelque chose, puis revenez au plus récent. La mise à jour vers la dernière version se trouve dans « À propos ».")
add("Engines can be switched and deleted in the installed Strata app.",
"可以在已安装的 Strata 应用中切换和删除引擎。", "エンジンの切り替えと削除は、インストール済みの Strata アプリで行えます。",
"Engines lassen sich in der installierten Strata-App wechseln und löschen.", "Los motores se pueden cambiar y eliminar en la aplicación Strata instalada.",
"Переключать и удалять движки можно в установленном приложении Strata.", "Os motores podem ser trocados e excluídos no aplicativo Strata instalado.",
"엔진 전환과 삭제는 설치된 Strata 앱에서 할 수 있습니다.", "Les moteurs peuvent être changés et supprimés dans l'application Strata installée.")
add("The installed engines couldn't be read. Try again in a moment.",
"无法读取已安装的引擎。请稍后重试。", "インストール済みのエンジンを読み取れませんでした。しばらくしてからもう一度お試しください。",
"Die installierten Engines konnten nicht gelesen werden. Versuche es gleich noch einmal.", "No se pudieron leer los motores instalados. Inténtalo de nuevo en un momento.",
"Не удалось прочитать список установленных движков. Попробуйте ещё раз через минуту.", "Não foi possível ler os motores instalados. Tente novamente em instantes.",
"설치된 엔진을 읽지 못했습니다. 잠시 후 다시 시도하세요.", "Les moteurs installés n'ont pas pu être lus. Réessayez dans un instant.")
add("No engine is installed on this computer yet. Update in About downloads the right one.",
"此电脑上还没有安装引擎。在“关于”中点击更新即可下载合适的引擎。", "このコンピューターにはまだエンジンがありません。「情報」で更新すると、合ったものがダウンロードされます。",
"Auf diesem Computer ist noch keine Engine installiert. Das Update unter „Über“ lädt die passende herunter.",
"Todavía no hay ningún motor instalado en este equipo. Actualizar en «Acerca de» descarga el adecuado.",
"На этом компьютере ещё не установлен движок. Обновление в разделе «О программе» скачает подходящий.",
"Ainda não há nenhum motor instalado neste computador. Atualizar em «Sobre» baixa o adequado.",
"이 컴퓨터에는 아직 설치된 엔진이 없습니다. '정보'에서 업데이트하면 알맞은 엔진을 내려받습니다.",
"Aucun moteur n'est encore installé sur cet ordinateur. La mise à jour dans « À propos » télécharge le bon.")
add("Engines take {0} on this computer.",
"引擎在此电脑上共占用 {0}。", "エンジンはこのコンピューターで {0} を使っています。", "Die Engines belegen auf diesem Computer {0}.", "Los motores ocupan {0} en este equipo.",
"Движки занимают на этом компьютере {0}.", "Os motores ocupam {0} neste computador.", "엔진이 이 컴퓨터에서 {0}을(를) 차지합니다.", "Les moteurs occupent {0} sur cet ordinateur.")
add("Newest", "最新", "最新", "Neueste", "Más reciente", "Новейший", "Mais recente", "최신", "Le plus récent")
add("Use this", "使用此引擎", "これを使う", "Diese verwenden", "Usar este", "Использовать", "Usar este", "이 엔진 사용", "Utiliser celui-ci")
add("Qwen and other models",
"Qwen 及其他模型", "Qwen とその他のモデル", "Qwen und andere Modelle", "Qwen y otros modelos", "Qwen и другие модели", "Qwen e outros modelos", "Qwen 및 기타 모델", "Qwen et autres modèles")
add("A running program is using this engine. Unload your models first.",
"有程序正在使用此引擎。请先卸载你的模型。", "実行中のプログラムがこのエンジンを使っています。先にモデルをアンロードしてください。",
"Ein laufendes Programm benutzt diese Engine. Entlade zuerst deine Modelle.", "Un programa en ejecución está usando este motor. Descarga primero tus modelos.",
"Этот движок используется работающей программой. Сначала выгрузите свои модели.", "Um programa em execução está usando este motor. Descarregue seus modelos primeiro.",
"실행 중인 프로그램이 이 엔진을 사용하고 있습니다. 먼저 모델을 언로드하세요.", "Un programme en cours utilise ce moteur. Déchargez d'abord vos modèles.")
add("Engine {0} is now in use.",
"现在使用引擎 {0}。", "エンジン {0} を使用中です。", "Engine {0} wird jetzt verwendet.", "Ahora se usa el motor {0}.", "Теперь используется движок {0}.", "O motor {0} está em uso agora.",
"이제 엔진 {0}을(를) 사용합니다.", "Le moteur {0} est maintenant utilisé.")
add("Engine {0} was deleted.",
"引擎 {0} 已删除。", "エンジン {0} を削除しました。", "Engine {0} wurde gelöscht.", "Se eliminó el motor {0}.", "Движок {0} удалён.", "O motor {0} foi excluído.",
"엔진 {0}을(를) 삭제했습니다.", "Le moteur {0} a été supprimé.")
add("Use engine {0}?",
"使用引擎 {0}?", "エンジン {0} を使いますか?", "Engine {0} verwenden?", "¿Usar el motor {0}?", "Использовать движок {0}?", "Usar o motor {0}?", "엔진 {0}을(를) 사용할까요?", "Utiliser le moteur {0} ?")
add("Use engine {0}",
"使用引擎 {0}", "エンジン {0} を使う", "Engine {0} verwenden", "Usar el motor {0}", "Использовать движок {0}", "Usar o motor {0}", "엔진 {0} 사용", "Utiliser le moteur {0}")
add("Models you start from now on run on engine {0}. Your models and settings stay as they are. You can switch back at any time.",
"从现在起启动的模型将使用引擎 {0}。你的模型和设置保持不变。你可以随时切换回来。",
"これから起動するモデルはエンジン {0} で動きます。モデルと設定はそのままです。いつでも元に戻せます。",
"Modelle, die du ab jetzt startest, laufen mit Engine {0}. Deine Modelle und Einstellungen bleiben unverändert. Du kannst jederzeit zurückwechseln.",
"Los modelos que inicies a partir de ahora usarán el motor {0}. Tus modelos y ajustes no cambian. Puedes volver cuando quieras.",
"Модели, которые вы запустите с этого момента, будут работать на движке {0}. Ваши модели и настройки останутся как есть. Вернуться можно в любой момент.",
"Os modelos que você iniciar a partir de agora usarão o motor {0}. Seus modelos e ajustes continuam como estão. Você pode voltar quando quiser.",
"지금부터 시작하는 모델은 엔진 {0}에서 실행됩니다. 모델과 설정은 그대로 유지됩니다. 언제든 다시 바꿀 수 있습니다.",
"Les modèles que vous démarrez à partir de maintenant utilisent le moteur {0}. Vos modèles et réglages restent inchangés. Vous pouvez revenir en arrière à tout moment.")
add("These models need a newer engine and may not start on it: {0}.",
"这些模型需要更新的引擎,在此引擎上可能无法启动:{0}。", "次のモデルはより新しいエンジンが必要で、このエンジンでは起動しない可能性があります:{0}。",
"Diese Modelle brauchen eine neuere Engine und starten damit eventuell nicht: {0}.", "Estos modelos necesitan un motor más reciente y puede que no arranquen con este: {0}.",
"Этим моделям нужен более новый движок, и на этом они могут не запуститься: {0}.", "Estes modelos precisam de um motor mais novo e podem não iniciar com este: {0}.",
"다음 모델은 더 새로운 엔진이 필요해 이 엔진에서는 시작되지 않을 수 있습니다: {0}.", "Ces modèles demandent un moteur plus récent et pourraient ne pas démarrer avec celui-ci : {0}.")
add("A model is running. Unload it in Models first, then switch.",
"有模型正在运行。请先在“模型”中卸载它,然后再切换。", "モデルが実行中です。先に「モデル」でアンロードしてから切り替えてください。",
"Ein Modell läuft. Entlade es zuerst unter „Modelle“ und wechsle dann.", "Hay un modelo en ejecución. Descárgalo primero en «Modelos» y luego cambia.",
"Работает модель. Сначала выгрузите её в разделе «Модели», затем переключитесь.", "Há um modelo em execução. Descarregue-o primeiro em «Modelos» e depois troque.",
"모델이 실행 중입니다. 먼저 '모델'에서 언로드한 다음 전환하세요.", "Un modèle est en cours d'exécution. Déchargez-le d'abord dans « Modèles », puis changez de moteur.")
add("Delete engine {0}?",
"删除引擎 {0}?", "エンジン {0} を削除しますか?", "Engine {0} löschen?", "¿Eliminar el motor {0}?", "Удалить движок {0}?", "Excluir o motor {0}?", "엔진 {0}을(를) 삭제할까요?", "Supprimer le moteur {0} ?")
add("Keep engine", "保留引擎", "エンジンを残す", "Engine behalten", "Conservar motor", "Оставить движок", "Manter motor", "엔진 유지", "Garder le moteur")
add("Delete engine", "删除引擎", "エンジンを削除", "Engine löschen", "Eliminar motor", "Удалить движок", "Excluir motor", "엔진 삭제", "Supprimer le moteur")
add("This frees {0}. Your models and settings are kept.",
"这将释放 {0}。你的模型和设置会保留。", "{0} が空きます。モデルと設定はそのまま残ります。", "Das gibt {0} frei. Deine Modelle und Einstellungen bleiben erhalten.",
"Esto libera {0}. Tus modelos y ajustes se conservan.", "Это освободит {0}. Ваши модели и настройки сохранятся.", "Isso libera {0}. Seus modelos e ajustes são mantidos.",
"{0}의 공간이 확보됩니다. 모델과 설정은 그대로 유지됩니다.", "Cela libère {0}. Vos modèles et réglages sont conservés.")
add("The newest engine can be downloaded again with Update in About.",
"最新的引擎可以在“关于”中通过更新重新下载。", "最新のエンジンは、「情報」の更新でもう一度ダウンロードできます。",
"Die neueste Engine lässt sich über das Update unter „Über“ erneut herunterladen.", "El motor más reciente se puede volver a descargar con Actualizar en «Acerca de».",
"Новейший движок можно скачать снова через обновление в разделе «О программе».", "O motor mais recente pode ser baixado de novo com Atualizar em «Sobre».",
"최신 엔진은 '정보'의 업데이트로 다시 내려받을 수 있습니다.", "Le moteur le plus récent peut être retéléchargé avec la mise à jour dans « À propos ».")
add("Strata can only download the newest engine, so a deleted older engine can't be brought back.",
"Strata 只能下载最新的引擎,所以删除后无法找回较旧的引擎。", "Strata がダウンロードできるのは最新のエンジンだけなので、削除した古いエンジンは元に戻せません。",
"Strata kann nur die neueste Engine herunterladen. Eine gelöschte ältere Engine lässt sich deshalb nicht wiederherstellen.",
"Strata solo puede descargar el motor más reciente, así que un motor anterior eliminado no se puede recuperar.",
"Strata может скачать только новейший движок, поэтому удалённый старый движок вернуть нельзя.",
"O Strata só pode baixar o motor mais recente, então um motor antigo excluído não pode ser recuperado.",
"Strata는 최신 엔진만 내려받을 수 있으므로, 삭제한 이전 엔진은 되돌릴 수 없습니다.",
"Strata ne peut télécharger que le moteur le plus récent : un ancien moteur supprimé ne peut donc pas être récupéré.")
# ---- why an engine cannot be used on this computer
add("This engine was made for a different operating system.",
"此引擎是为其他操作系统制作的。", "このエンジンは別のオペレーティングシステム向けです。", "Diese Engine wurde für ein anderes Betriebssystem gebaut.",
"Este motor se creó para otro sistema operativo.", "Этот движок создан для другой операционной системы.", "Este motor foi feito para outro sistema operacional.",
"이 엔진은 다른 운영체제용입니다.", "Ce moteur a été conçu pour un autre système d'exploitation.")
add("This engine was made for a different kind of graphics card than this computer has.",
"此引擎是为与这台电脑不同类型的显卡制作的。", "このエンジンは、このコンピューターとは別の種類のグラフィックスカード向けです。",
"Diese Engine wurde für eine andere Art von Grafikkarte gebaut als die in diesem Computer.", "Este motor se creó para un tipo de tarjeta gráfica distinto del de este equipo.",
"Этот движок создан для другого типа видеокарты, чем в этом компьютере.", "Este motor foi feito para um tipo de placa de vídeo diferente da deste computador.",
"이 엔진은 이 컴퓨터와 다른 종류의 그래픽 카드용입니다.", "Ce moteur a été conçu pour un autre type de carte graphique que celle de cet ordinateur.")
add("This engine needs Strata {0} or newer.",
"此引擎需要 Strata {0} 或更新版本。", "このエンジンには Strata {0} 以降が必要です。", "Diese Engine braucht Strata {0} oder neuer.", "Este motor necesita Strata {0} o posterior.",
"Для этого движка нужна Strata {0} или новее.", "Este motor precisa do Strata {0} ou mais recente.", "이 엔진에는 Strata {0} 이상이 필요합니다.", "Ce moteur nécessite Strata {0} ou plus récent.")
add("This engine doesn't support this computer's graphics card.",
"此引擎不支持这台电脑的显卡。", "このエンジンは、このコンピューターのグラフィックスカードに対応していません。", "Diese Engine unterstützt die Grafikkarte dieses Computers nicht.",
"Este motor no admite la tarjeta gráfica de este equipo.", "Этот движок не поддерживает видеокарту этого компьютера.", "Este motor não oferece suporte à placa de vídeo deste computador.",
"이 엔진은 이 컴퓨터의 그래픽 카드를 지원하지 않습니다.", "Ce moteur ne prend pas en charge la carte graphique de cet ordinateur.")
add("This engine needs NVIDIA driver {0} or newer. This computer has {1}.",
"此引擎需要 NVIDIA 驱动 {0} 或更新版本。这台电脑的驱动是 {1}。", "このエンジンには NVIDIA ドライバー {0} 以降が必要です。このコンピューターは {1} です。",
"Diese Engine braucht den NVIDIA-Treiber {0} oder neuer. Dieser Computer hat {1}.", "Este motor necesita el controlador NVIDIA {0} o posterior. Este equipo tiene {1}.",
"Для этого движка нужен драйвер NVIDIA {0} или новее. На этом компьютере {1}.", "Este motor precisa do driver NVIDIA {0} ou mais recente. Este computador tem o {1}.",
"이 엔진에는 NVIDIA 드라이버 {0} 이상이 필요합니다. 이 컴퓨터는 {1}입니다.", "Ce moteur nécessite le pilote NVIDIA {0} ou plus récent. Cet ordinateur a le {1}.")
add("Some of this engine's files are missing or changed, so it can't be started.",
"此引擎的部分文件缺失或已被更改,因此无法启动。", "このエンジンのファイルの一部が見つからないか変更されているため、起動できません。",
"Einige Dateien dieser Engine fehlen oder wurden verändert, deshalb lässt sie sich nicht starten.", "Faltan archivos de este motor o han cambiado, así que no se puede iniciar.",
"Часть файлов этого движка отсутствует или изменена, поэтому его нельзя запустить.", "Alguns arquivos deste motor estão ausentes ou foram alterados, então ele não pode ser iniciado.",
"이 엔진의 일부 파일이 없거나 변경되어 시작할 수 없습니다.", "Certains fichiers de ce moteur sont absents ou modifiés : il ne peut pas démarrer.")
add("This engine's description is missing or damaged, so it can't be started.",
"此引擎的说明文件缺失或已损坏,因此无法启动。", "このエンジンの説明ファイルが見つからないか壊れているため、起動できません。",
"Die Beschreibung dieser Engine fehlt oder ist beschädigt, deshalb lässt sie sich nicht starten.", "Falta la descripción de este motor o está dañada, así que no se puede iniciar.",
"Описание этого движка отсутствует или повреждено, поэтому его нельзя запустить.", "A descrição deste motor está ausente ou danificada, então ele não pode ser iniciado.",
"이 엔진의 설명 파일이 없거나 손상되어 시작할 수 없습니다.", "La description de ce moteur est absente ou endommagée : il ne peut pas démarrer.")
# ---- updating moved to About: the words that sent people to Settings > Engine
add("Strata didn’t recognise an engine for this computer. The model will download, but it may not run until the right engine is installed (Settings → About).",
"Strata 未能识别这台电脑的引擎。模型会下载,但在安装合适的引擎之前可能无法运行(设置 → 关于)。",
"Strata はこのコンピューター用のエンジンを認識できませんでした。モデルはダウンロードされますが、合ったエンジンをインストールするまで動かない可能性があります(設定 → 情報)。",
"Strata hat für diesen Computer keine Engine erkannt. Das Modell wird heruntergeladen, läuft aber möglicherweise erst, wenn die passende Engine installiert ist (Einstellungen → Über).",
"Strata no reconoció ningún motor para este equipo. El modelo se descargará, pero puede que no funcione hasta que se instale el motor adecuado (Ajustes → Acerca de).",
"Strata не распознала движок для этого компьютера. Модель скачается, но может не запуститься, пока не установлен подходящий движок (Настройки → О программе).",
"O Strata não reconheceu um motor para este computador. O modelo será baixado, mas pode não funcionar até que o motor certo seja instalado (Configurações → Sobre).",
"Strata가 이 컴퓨터용 엔진을 인식하지 못했습니다. 모델은 내려받지만, 알맞은 엔진이 설치되기 전에는 실행되지 않을 수 있습니다(설정 → 정보).",
"Strata n’a pas reconnu de moteur pour cet ordinateur. Le modèle sera téléchargé, mais il risque de ne pas fonctionner tant que le bon moteur n’est pas installé (Réglages → À propos).")
add("Strata didn’t recognise an engine for this computer. The model is downloaded, but it can’t run until the right engine is installed. Open Settings → About.",
"Strata 未能识别这台电脑的引擎。模型已下载,但在安装合适的引擎之前无法运行。请打开 设置 → 关于。",
"Strata はこのコンピューター用のエンジンを認識できませんでした。モデルはダウンロード済みですが、合ったエンジンをインストールするまで動きません。設定 → 情報 を開いてください。",
"Strata hat für diesen Computer keine Engine erkannt. Das Modell ist heruntergeladen, läuft aber erst, wenn die passende Engine installiert ist. Öffne Einstellungen → Über.",
"Strata no reconoció ningún motor para este equipo. El modelo está descargado, pero no puede funcionar hasta que se instale el motor adecuado. Abre Ajustes → Acerca de.",
"Strata не распознала движок для этого компьютера. Модель скачана, но не запустится, пока не установлен подходящий движок. Откройте Настройки → О программе.",
"O Strata não reconheceu um motor para este computador. O modelo foi baixado, mas não pode funcionar até que o motor certo seja instalado. Abra Configurações → Sobre.",
"Strata가 이 컴퓨터용 엔진을 인식하지 못했습니다. 모델은 내려받았지만, 알맞은 엔진이 설치되기 전에는 실행할 수 없습니다. 설정 → 정보를 여세요.",
"Strata n’a pas reconnu de moteur pour cet ordinateur. Le modèle est téléchargé, mais il ne peut pas fonctionner tant que le bon moteur n’est pas installé. Ouvrez Réglages → À propos.")
add("Update the engine (Settings, About), then try again.",
"请更新引擎(设置,关于),然后重试。", "エンジンを更新して(設定、情報)、もう一度お試しください。", "Aktualisiere die Engine (Einstellungen, Über) und versuche es dann erneut.",
"Actualiza el motor (Ajustes, Acerca de) y vuelve a intentarlo.", "Обновите движок (Настройки, О программе) и попробуйте снова.", "Atualize o motor (Configurações, Sobre) e tente novamente.",
"엔진을 업데이트(설정, 정보)한 뒤 다시 시도하세요.", "Mettez le moteur à jour (Réglages, À propos), puis réessayez.")
# ---- answers of the host (shown as they are, so each one is a catalog entry)
add("A model is running, so nothing was changed. Unload the model in Models, then try again.",
"有模型正在运行,所以没有做任何更改。请先在“模型”中卸载模型,然后重试。", "モデルが実行中のため、何も変更していません。「モデル」でモデルをアンロードしてから、もう一度お試しください。",
"Ein Modell läuft, deshalb wurde nichts geändert. Entlade das Modell unter „Modelle“ und versuche es dann erneut.", "Hay un modelo en ejecución, así que no se cambió nada. Descarga el modelo en «Modelos» y vuelve a intentarlo.",
"Работает модель, поэтому ничего не изменено. Выгрузите модель в разделе «Модели» и попробуйте снова.", "Há um modelo em execução, então nada foi alterado. Descarregue o modelo em «Modelos» e tente novamente.",
"모델이 실행 중이라 아무것도 바뀌지 않았습니다. '모델'에서 모델을 언로드한 다음 다시 시도하세요.", "Un modèle est en cours d'exécution, rien n'a donc été modifié. Déchargez le modèle dans « Modèles », puis réessayez.")
add("This is the engine in use. Switch to another one first, then delete it.",
"这是正在使用的引擎。请先切换到另一个引擎,再删除它。", "これは使用中のエンジンです。先に別のエンジンに切り替えてから削除してください。",
"Das ist die verwendete Engine. Wechsle zuerst zu einer anderen und lösche sie dann.", "Este es el motor en uso. Cambia primero a otro y luego elimínalo.",
"Это используемый движок. Сначала переключитесь на другой, затем удалите его.", "Este é o motor em uso. Troque primeiro para outro e depois exclua-o.",
"사용 중인 엔진입니다. 먼저 다른 엔진으로 전환한 다음 삭제하세요.", "C'est le moteur utilisé. Passez d'abord à un autre, puis supprimez-le.")
add("A program is still using this engine, so it was not deleted. Unload your models and try again.",
"仍有程序在使用此引擎,所以没有删除。请卸载你的模型后重试。", "プログラムがまだこのエンジンを使っているため、削除しませんでした。モデルをアンロードしてから、もう一度お試しください。",
"Ein Programm benutzt diese Engine noch, deshalb wurde sie nicht gelöscht. Entlade deine Modelle und versuche es erneut.", "Un programa todavía usa este motor, así que no se eliminó. Descarga tus modelos y vuelve a intentarlo.",
"Этот движок ещё используется программой, поэтому он не удалён. Выгрузите модели и попробуйте снова.", "Um programa ainda está usando este motor, então ele não foi excluído. Descarregue seus modelos e tente novamente.",
"프로그램이 아직 이 엔진을 사용하고 있어 삭제하지 않았습니다. 모델을 언로드한 다음 다시 시도하세요.", "Un programme utilise encore ce moteur : il n'a pas été supprimé. Déchargez vos modèles, puis réessayez.")
add("That engine is already gone. The list has been refreshed.",
"该引擎已经不存在。列表已刷新。", "そのエンジンはすでにありません。一覧を更新しました。", "Diese Engine ist schon nicht mehr da. Die Liste wurde aktualisiert.",
"Ese motor ya no está. La lista se ha actualizado.", "Этого движка уже нет. Список обновлён.", "Esse motor já não existe. A lista foi atualizada.", "해당 엔진은 이미 없습니다. 목록을 새로 고쳤습니다.", "Ce moteur n'existe déjà plus. La liste a été actualisée.")
add("An update is running. Wait for it to finish, then try again.",
"正在进行更新。请等它完成后再试。", "更新を実行中です。終わるまで待ってから、もう一度お試しください。", "Ein Update läuft. Warte, bis es fertig ist, und versuche es dann erneut.",
"Hay una actualización en curso. Espera a que termine y vuelve a intentarlo.", "Идёт обновление. Дождитесь его окончания и попробуйте снова.", "Uma atualização está em andamento. Aguarde o fim e tente novamente.",
"업데이트가 진행 중입니다. 끝날 때까지 기다린 다음 다시 시도하세요.", "Une mise à jour est en cours. Attendez qu'elle se termine, puis réessayez.")
add("This engine can't run on this computer, so nothing was changed.",
"此引擎无法在这台电脑上运行,所以没有做任何更改。", "このエンジンはこのコンピューターでは動かないため、何も変更していません。", "Diese Engine läuft auf diesem Computer nicht, deshalb wurde nichts geändert.",
"Este motor no puede funcionar en este equipo, así que no se cambió nada.", "Этот движок не может работать на этом компьютере, поэтому ничего не изменено.", "Este motor não pode funcionar neste computador, então nada foi alterado.",
"이 엔진은 이 컴퓨터에서 실행할 수 없어 아무것도 바뀌지 않았습니다.", "Ce moteur ne peut pas fonctionner sur cet ordinateur : rien n'a été modifié.")
add("The engine wasn't deleted. Nothing else was changed.",
"引擎没有被删除。其他内容均未更改。", "エンジンは削除されませんでした。ほかには何も変更していません。", "Die Engine wurde nicht gelöscht. Sonst wurde nichts geändert.",
"El motor no se eliminó. No se cambió nada más.", "Движок не удалён. Больше ничего не изменено.", "O motor não foi excluído. Nada mais foi alterado.", "엔진이 삭제되지 않았습니다. 다른 것은 바뀌지 않았습니다.", "Le moteur n'a pas été supprimé. Rien d'autre n'a été modifié.")
add("The engine wasn't switched. Nothing was changed.",
"引擎没有被切换。没有做任何更改。", "エンジンは切り替わりませんでした。何も変更していません。", "Die Engine wurde nicht gewechselt. Es wurde nichts geändert.",
"No se cambió el motor. No se modificó nada.", "Движок не переключён. Ничего не изменено.", "O motor não foi trocado. Nada foi alterado.", "엔진이 전환되지 않았습니다. 아무것도 바뀌지 않았습니다.", "Le moteur n'a pas été changé. Rien n'a été modifié.")
add("Choose an engine from the list.",
"请从列表中选择一个引擎。", "一覧からエンジンを選んでください。", "Wähle eine Engine aus der Liste.", "Elige un motor de la lista.", "Выберите движок из списка.", "Escolha um motor da lista.", "목록에서 엔진을 선택하세요.", "Choisissez un moteur dans la liste.")
add("A newer engine is already on this computer. Update switches to it; nothing is downloaded.",
"这台电脑上已经有更新的引擎。点击更新会切换到它,不会下载任何内容。", "このコンピューターにはすでに新しいエンジンがあります。更新するとそれに切り替わり、ダウンロードはされません。",
"Auf diesem Computer ist schon eine neuere Engine. Das Update wechselt zu ihr; es wird nichts heruntergeladen.", "Este equipo ya tiene un motor más reciente. Actualizar cambia a él; no se descarga nada.",
"На этом компьютере уже есть более новый движок. Обновление переключит на него; ничего не скачивается.", "Este computador já tem um motor mais novo. Atualizar troca para ele; nada é baixado.",
"이 컴퓨터에 이미 더 새로운 엔진이 있습니다. 업데이트하면 그 엔진으로 전환되며, 내려받는 것은 없습니다.", "Cet ordinateur a déjà un moteur plus récent. La mise à jour bascule dessus ; rien n'est téléchargé.")
here = Path(__file__).resolve().parent.parent / "src" / "locales"
NBSP = " "
def french(text):
text = re.sub(r"(?<=[A-Za-zÀ-ſ])'(?=[A-Za-zÀ-ſ{])", "’", text)
text = re.sub(r"(?<=\S) ([:;?!])", NBSP + r"\1", text)
text = re.sub(r"(?<=[\d}]) %", NBSP + "%", text)
return text
for lang in LANGS:
data = {}
for i, entry in enumerate(S):
text = entry[lang]
assert sorted(re.findall(r"\{\d+\}", entry["en"])) == sorted(re.findall(r"\{\d+\}", text)), (lang, entry["en"], text)
data[f"et{i + 1:02d}"] = french(text) if lang == "fr" else text
(here / f"engine-{lang}.json").write_text(json.dumps(data, ensure_ascii=False, indent=1) + "\n", encoding="utf-8")
print("written", len(S), "strings in", len(LANGS), "languages")
+385
View File
@@ -0,0 +1,385 @@
"""Writes src/locales/fix-<lang>.json: the interface strings that had no key in the locale files (#3479, #3825) and the strings of the 0.2.4.3 bug fixes.
Run from apps/host: python scripts/make-fix-locales.py
The keys are fx01.. in this order; fix-en.json is the English source. Placeholders ({0}, {1}) must be kept in every language."""
import json
from pathlib import Path
EN = [
"will stop running and be removed.",
"Downloaded, not loaded. Open Models to load it.",
"{0} lines added, {1} removed",
"Edited {0} files",
"Drop files to add them",
"Pictures and text files",
"Keep goal",
"Remove goal",
"The assistant stops working towards it. Your chat and files stay as they are.",
"This model can read pictures, but the engine installed here does not include the picture reader yet, so Read pictures is off. Update the engine in Settings → About to turn it on.",
"Showing the values of your tuning '{0}'.",
"Your tuning '{0}' sets",
"was {0}",
"· {0} RAM",
"The message was not cancelled",
"Extra settings for the program",
"Draft it with AI",
"Thinking time",
"Writing time",
"tokens/s",
"% of context",
"This page asks for files from the internet, which the preview does not load, so it may look incomplete. View code shows exactly what was written.",
"Keep A",
"Keep B",
"Took",
"Variation",
"Changed",
"Nothing",
"Both versions are ready. Compare A and B, then keep one.",
"The assistant is still answering",
"Send your message when it has finished.",
"The message was not queued",
"Scroll to the latest message",
"Remove task",
"Start a new conversation; keep this one",
"Have the assistant write a plan first; nothing runs until you approve it",
"Search the web, read the pages and answer with sources",
"Copied",
"Response",
"Message",
"PDF added",
"Select the text and copy it with Ctrl+C.",
"{0} is in this chat. Ask about it in your next message.",
]
T = {
"zh": [
"将停止运行并被移除。",
"已下载,尚未加载。打开“模型”页面来加载它。",
"新增 {0} 行,删除 {1} 行",
"已编辑 {0} 个文件",
"拖放文件即可添加",
"图片和文本文件",
"保留目标",
"移除目标",
"助手将不再朝这个目标工作。你的对话和文件保持不变。",
"此模型可以读取图片,但这里安装的引擎还不包含图片读取器,所以“读取图片”已关闭。请在“设置 → 关于”中更新引擎后再开启。",
"显示你的调优“{0}”的取值。",
"你的调优“{0}”设置了",
"原为 {0}",
"· {0} 内存",
"消息没有被取消",
"程序的额外设置",
"用 AI 起草",
"思考时间",
"写作时间",
"词元/秒",
"% 的上下文",
"此页面需要从互联网加载文件,预览不会加载这些文件,所以看起来可能不完整。“查看代码”会显示实际写入的内容。",
"保留 A",
"保留 B",
"用时",
"波动",
"已更改",
"无",
"两个版本都已就绪。比较 A 和 B,然后保留其中一个。",
"助手仍在回答",
"等它回答完再发送你的消息。",
"消息没有进入队列",
"滚动到最新消息",
"移除任务",
"开始新对话;保留当前这个",
"让助手先写一份计划;在你批准之前不会运行任何内容",
"搜索网络、阅读网页,并附上来源作答",
"已复制",
"回复",
"消息",
"已添加 PDF",
"请选中文字,然后按 Ctrl+C 复制。",
"{0} 已在此对话中。请在下一条消息里询问它。",
],
"ja": [
"は停止され、削除されます。",
"ダウンロード済みですが、読み込まれていません。「モデル」を開いて読み込んでください。",
"{0} 行追加、{1} 行削除",
"{0} 個のファイルを編集しました",
"ファイルをドロップして追加",
"画像とテキストファイル",
"目標を残す",
"目標を削除",
"アシスタントはこの目標に向けた作業をやめます。チャットとファイルはそのまま残ります。",
"このモデルは画像を読めますが、ここにインストールされているエンジンにはまだ画像リーダーが含まれていないため、「画像を読む」はオフです。設定 → 情報 でエンジンを更新するとオンにできます。",
"チューニング「{0}」の値を表示しています。",
"チューニング「{0}」が設定する項目",
"変更前: {0}",
"· {0} RAM",
"メッセージはキャンセルされませんでした",
"プログラムの追加設定",
"AI で下書きを作成",
"思考時間",
"執筆時間",
"トークン/秒",
"% のコンテキスト",
"このページはインターネット上のファイルを必要としますが、プレビューでは読み込まれないため、表示が不完全になることがあります。「コードを表示」で書かれた内容をそのまま確認できます。",
"A を残す",
"B を残す",
"所要時間",
"ばらつき",
"変更点",
"なし",
"両方のバージョンの準備ができました。A と B を比べて、どちらかを残してください。",
"アシスタントはまだ回答中です",
"回答が終わってからメッセージを送ってください。",
"メッセージは待機列に入りませんでした",
"最新のメッセージへスクロール",
"タスクを削除",
"新しい会話を始めます。今の会話は残ります",
"アシスタントにまず計画を書かせます。承認するまで何も実行されません",
"ウェブを検索し、ページを読み、出典付きで回答します",
"コピーしました",
"返信",
"メッセージ",
"PDF を追加しました",
"テキストを選択して Ctrl+C でコピーしてください。",
"{0} をこのチャットに追加しました。次のメッセージで質問してください。",
],
"de": [
"wird nicht mehr ausgeführt und entfernt.",
"Heruntergeladen, aber nicht geladen. Öffne „Modelle“, um es zu laden.",
"{0} Zeilen hinzugefügt, {1} entfernt",
"{0} Dateien bearbeitet",
"Dateien hier ablegen, um sie hinzuzufügen",
"Bilder und Textdateien",
"Ziel behalten",
"Ziel entfernen",
"Der Assistent arbeitet nicht mehr darauf hin. Dein Chat und deine Dateien bleiben unverändert.",
"Dieses Modell kann Bilder lesen, aber die hier installierte Engine enthält den Bildleser noch nicht, deshalb ist „Bilder lesen“ aus. Aktualisiere die Engine unter Einstellungen → Info, um es einzuschalten.",
"Zeigt die Werte deiner Abstimmung „{0}“.",
"Deine Abstimmung „{0}“ setzt",
"war {0}",
"· {0} RAM",
"Die Nachricht wurde nicht abgebrochen",
"Zusätzliche Einstellungen für das Programm",
"Mit KI entwerfen",
"Denkzeit",
"Schreibzeit",
"Token/s",
"% des Kontexts",
"Diese Seite lädt Dateien aus dem Internet, was die Vorschau nicht tut. Sie kann deshalb unvollständig aussehen. „Code anzeigen“ zeigt genau, was geschrieben wurde.",
"A behalten",
"B behalten",
"Dauer",
"Schwankung",
"Geändert",
"Nichts",
"Beide Versionen sind fertig. Vergleiche A und B und behalte eine.",
"Der Assistent antwortet noch",
"Sende deine Nachricht, wenn er fertig ist.",
"Die Nachricht wurde nicht eingereiht",
"Zur neuesten Nachricht scrollen",
"Aufgabe entfernen",
"Neue Unterhaltung beginnen; diese bleibt erhalten",
"Der Assistent schreibt zuerst einen Plan; nichts läuft, bis du ihn freigibst",
"Im Web suchen, die Seiten lesen und mit Quellen antworten",
"Kopiert",
"Antwort",
"Nachricht",
"PDF hinzugefügt",
"Markiere den Text und kopiere ihn mit Strg+C.",
"{0} ist in diesem Chat. Frag in deiner nächsten Nachricht danach.",
],
"es": [
"dejará de ejecutarse y se eliminará.",
"Descargado, pero no cargado. Abre Modelos para cargarlo.",
"{0} líneas añadidas, {1} eliminadas",
"{0} archivos editados",
"Suelta los archivos para añadirlos",
"Imágenes y archivos de texto",
"Mantener el objetivo",
"Quitar el objetivo",
"El asistente deja de trabajar en ello. Tu chat y tus archivos se quedan como están.",
"Este modelo puede leer imágenes, pero el motor instalado aquí aún no incluye el lector de imágenes, así que Leer imágenes está desactivado. Actualiza el motor en Ajustes → Acerca de para activarlo.",
"Se muestran los valores de tu ajuste «{0}».",
"Tu ajuste «{0}» establece",
"antes {0}",
"· {0} de RAM",
"El mensaje no se canceló",
"Ajustes adicionales del programa",
"Redactar con IA",
"Tiempo de pensar",
"Tiempo de escribir",
"tokens/s",
"% del contexto",
"Esta página pide archivos de internet, que la vista previa no carga, así que puede verse incompleta. Ver código muestra exactamente lo que se escribió.",
"Mantener A",
"Mantener B",
"Tardó",
"Variación",
"Cambiado",
"Nada",
"Las dos versiones están listas. Compara A y B y quédate con una.",
"El asistente sigue respondiendo",
"Envía tu mensaje cuando termine.",
"El mensaje no se puso en cola",
"Ir al último mensaje",
"Quitar tarea",
"Empieza una conversación nueva; esta se conserva",
"El asistente escribe primero un plan; no se ejecuta nada hasta que lo apruebes",
"Busca en la web, lee las páginas y responde con fuentes",
"Copiado",
"Respuesta",
"Mensaje",
"PDF añadido",
"Selecciona el texto y cópialo con Ctrl+C.",
"{0} está en este chat. Pregunta por él en tu próximo mensaje.",
],
"ru": [
"перестанет работать и будет удалена.",
"Скачана, но не загружена. Откройте «Модели», чтобы загрузить её.",
"Добавлено строк: {0}, удалено: {1}",
"Изменено файлов: {0}",
"Перетащите файлы сюда, чтобы добавить",
"Картинки и текстовые файлы",
"Оставить цель",
"Удалить цель",
"Ассистент перестанет работать над ней. Ваш чат и файлы останутся как есть.",
"Эта модель умеет читать картинки, но установленный здесь движок пока не содержит модуль чтения картинок, поэтому «Читать картинки» выключено. Обновите движок в разделе «Настройки → О программе», чтобы включить.",
"Показаны значения вашей настройки «{0}».",
"Ваша настройка «{0}» задаёт",
"было {0}",
"· {0} ОЗУ",
"Сообщение не было отменено",
"Дополнительные настройки программы",
"Набросать с помощью ИИ",
"Время размышления",
"Время написания",
"токенов/с",
"% контекста",
"Эта страница запрашивает файлы из интернета, а предпросмотр их не загружает, поэтому она может выглядеть неполной. «Показать код» показывает, что именно было написано.",
"Оставить A",
"Оставить B",
"Заняло",
"Разброс",
"Изменено",
"Ничего",
"Обе версии готовы. Сравните A и B и оставьте одну.",
"Ассистент ещё отвечает",
"Отправьте сообщение, когда он закончит.",
"Сообщение не поставлено в очередь",
"К последнему сообщению",
"Удалить задачу",
"Начать новый разговор; этот сохраняется",
"Ассистент сначала напишет план; ничего не запустится, пока вы его не одобрите",
"Искать в интернете, читать страницы и отвечать со ссылками на источники",
"Скопировано",
"Ответ",
"Сообщение",
"PDF добавлен",
"Выделите текст и скопируйте его клавишами Ctrl+C.",
"{0} добавлен в этот чат. Спросите о нём в следующем сообщении.",
],
"pt": [
"vai parar de funcionar e será removido.",
"Baixado, mas não carregado. Abra Modelos para carregá-lo.",
"{0} linhas adicionadas, {1} removidas",
"{0} arquivos editados",
"Solte os arquivos para adicioná-los",
"Imagens e arquivos de texto",
"Manter a meta",
"Remover a meta",
"O assistente deixa de trabalhar nela. Seu chat e seus arquivos continuam como estão.",
"Este modelo consegue ler imagens, mas o motor instalado aqui ainda não inclui o leitor de imagens, então Ler imagens está desligado. Atualize o motor em Configurações → Sobre para ligar.",
"Mostrando os valores do seu ajuste \"{0}\".",
"Seu ajuste \"{0}\" define",
"era {0}",
"· {0} de RAM",
"A mensagem não foi cancelada",
"Configurações extras do programa",
"Rascunhar com IA",
"Tempo de raciocínio",
"Tempo de escrita",
"tokens/s",
"% do contexto",
"Esta página pede arquivos da internet, que a visualização não carrega, então pode parecer incompleta. Ver código mostra exatamente o que foi escrito.",
"Manter A",
"Manter B",
"Levou",
"Variação",
"Alterado",
"Nada",
"As duas versões estão prontas. Compare A e B e mantenha uma.",
"O assistente ainda está respondendo",
"Envie sua mensagem quando ele terminar.",
"A mensagem não entrou na fila",
"Ir para a última mensagem",
"Remover tarefa",
"Começar uma nova conversa; esta continua salva",
"O assistente escreve um plano primeiro; nada roda até você aprovar",
"Pesquisar na web, ler as páginas e responder com fontes",
"Copiado",
"Resposta",
"Mensagem",
"PDF adicionado",
"Selecione o texto e copie com Ctrl+C.",
"{0} está neste chat. Pergunte sobre ele na próxima mensagem.",
],
"ko": [
"실행이 중지되고 제거됩니다.",
"다운로드했지만 아직 불러오지 않았습니다. 모델 화면을 열어 불러오세요.",
"{0}줄 추가, {1}줄 삭제",
"파일 {0}개 편집함",
"파일을 놓아서 추가하세요",
"그림과 텍스트 파일",
"목표 유지",
"목표 제거",
"어시스턴트가 이 목표를 향한 작업을 멈춥니다. 대화와 파일은 그대로 남습니다.",
"이 모델은 그림을 읽을 수 있지만, 여기에 설치된 엔진에는 아직 그림 읽기 기능이 없어서 '그림 읽기'가 꺼져 있습니다. 설정 → 정보에서 엔진을 업데이트하면 켤 수 있습니다.",
"튜닝 '{0}'의 값을 보여 줍니다.",
"튜닝 '{0}'이(가) 설정하는 항목",
"이전 값 {0}",
"· RAM {0}",
"메시지가 취소되지 않았습니다",
"프로그램 추가 설정",
"AI로 초안 작성",
"생각한 시간",
"작성한 시간",
"토큰/초",
"컨텍스트 %",
"이 페이지는 인터넷의 파일을 불러와야 하는데 미리보기는 이를 불러오지 않으므로 불완전하게 보일 수 있습니다. '코드 보기'에서 작성된 내용을 그대로 볼 수 있습니다.",
"A 유지",
"B 유지",
"걸린 시간",
"편차",
"변경됨",
"없음",
"두 버전이 모두 준비되었습니다. A와 B를 비교한 뒤 하나를 유지하세요.",
"어시스턴트가 아직 답하는 중입니다",
"답이 끝나면 메시지를 보내세요.",
"메시지가 대기열에 들어가지 않았습니다",
"최신 메시지로 이동",
"작업 제거",
"새 대화를 시작합니다. 이 대화는 그대로 둡니다",
"어시스턴트가 먼저 계획을 작성합니다. 승인하기 전에는 아무것도 실행되지 않습니다",
"웹을 검색하고 페이지를 읽어 출처와 함께 답합니다",
"복사했습니다",
"답변",
"메시지",
"PDF를 추가했습니다",
"텍스트를 선택한 뒤 Ctrl+C로 복사하세요.",
"{0}이(가) 이 채팅에 있습니다. 다음 메시지에서 물어보세요.",
],
}
here = Path(__file__).resolve().parent.parent / "locales" if False else Path(__file__).resolve().parent.parent / "src" / "locales"
def write(lang, values):
assert len(values) == len(EN), (lang, len(values), len(EN))
for source, text in zip(EN, values):
assert sorted(__import__("re").findall(r"\{\d+\}", source)) == sorted(__import__("re").findall(r"\{\d+\}", text)), (lang, source, text)
data = {f"fx{i + 1:02d}": text for i, text in enumerate(values)}
(here / f"fix-{lang}.json").write_text(json.dumps(data, ensure_ascii=False, indent=1) + "\n", encoding="utf-8")
write("en", EN)
for lang, values in T.items():
write(lang, values)
print("written", len(EN), "strings in", len(T) + 1, "languages")
+365
View File
@@ -0,0 +1,365 @@
"""Writes src/locales/gh-<lang>.json: the interface strings of the 0.2.7 GitHub items done in f-027-gh-app (the Monitor's Power card #3628,
approval notifications and the tray #3477, the finished-task sound #3586, the pinned port #3402, the interface size #3970).
Run from apps/host: python scripts/make-gh-locales.py
The keys are gh01.. in this order; gh-en.json is the English source. Placeholders ({0}, {1}) must be kept in every language.
A separate file keeps this work from colliding with the other catalogs (the same reason tune-, tools- and fix- exist)."""
import json
import re
from pathlib import Path
EN = [
"Energy since start",
"Cost since start",
"GPU fan",
"GPU clock",
"CPU temperature",
"SSD temperature",
"Electricity price",
"Price of one kilowatt-hour. The Monitor then shows what your graphics card has cost since Strata started. Leave it empty to hide the cost.",
"Price per kWh",
"Sound when work finishes",
"Plays a short sound when an answer or task that took a while is finished, or when Strata is in the background.",
"Play sound",
"Notify me when Strata needs an answer",
"In Manual mode, shows a desktop notification when the assistant waits for your approval and Strata is in the background.",
"Strata needs your answer",
"The assistant wants to use {0}. Open Strata to approve or deny.",
"a tool",
"Minimise to tray",
"Closing the window keeps Strata running in the tray, next to the clock. Choose Quit in the tray menu to stop it. Applies the next time Strata starts.",
"App port",
"The port Strata’s window and local address use. Leave it empty to let Strata choose (47831 when it is free). Applies the next time Strata starts.",
"Port (empty = automatic)",
"Using port {0} now.",
"Strata could not use its port",
"Port {0} is used by another program, so Strata started on port {1}. Chats and settings saved on port {0} are not shown until Strata can use it again. Close the other program and restart Strata.",
"Choose a port between 1024 and 65535, or leave it empty.",
"Makes all text, buttons and menus in Strata larger or smaller.",
"Energy and temperature readings",
"per kWh, in {0}",
"per kWh",
"Change currency",
"Automatic ({0})",
"Currency",
"less than {0}",
]
T = {
"zh": [
"启动以来的能耗",
"启动以来的费用",
"GPU 风扇",
"GPU 频率",
"CPU 温度",
"SSD 温度",
"电价",
"每千瓦时的价格。监控页会据此显示 Strata 启动以来显卡用电的费用。留空则不显示费用。",
"每千瓦时价格",
"工作完成时的提示音",
"回答或耗时较长的任务完成时,或 Strata 在后台时,播放一段简短的提示音。",
"播放提示音",
"Strata 需要您答复时通知我",
"在手动模式下,当助手等待您批准而 Strata 在后台时,显示桌面通知。",
"Strata 需要您的答复",
"助手想使用 {0}。打开 Strata 以批准或拒绝。",
"一个工具",
"最小化到托盘",
"关闭窗口后,Strata 会继续在托盘(时钟旁边)运行。要停止它,请在托盘菜单中选择“退出”。下次启动 Strata 时生效。",
"应用端口",
"Strata 窗口和本地地址使用的端口。留空则由 Strata 自行选择(空闲时为 47831)。下次启动 Strata 时生效。",
"端口(留空 = 自动)",
"当前使用端口 {0}。",
"Strata 无法使用它的端口",
"端口 {0} 已被其他程序占用,因此 Strata 改用端口 {1} 启动。在端口 {0} 上保存的聊天和设置在 Strata 能再次使用该端口之前不会显示。请关闭其他程序并重新启动 Strata。",
"请选择 1024 到 65535 之间的端口,或留空。",
"让 Strata 中的所有文字、按钮和菜单变大或变小。",
"能耗和温度读数",
"每千瓦时,币种 {0}",
"每千瓦时",
"更改货币",
"自动({0})",
"货币",
"不足 {0}",
],
"ja": [
"起動後の消費電力量",
"起動後の電気代",
"GPU ファン",
"GPU クロック",
"CPU 温度",
"SSD 温度",
"電気料金",
"1キロワット時あたりの料金です。モニターに、Strata の起動後にグラフィックスカードが使った電気代が表示されます。空欄のままなら電気代は表示されません。",
"1 kWh あたりの料金",
"作業完了時の音",
"回答や時間のかかった作業が終わったとき、または Strata がバックグラウンドにあるときに、短い音を鳴らします。",
"音を鳴らす",
"Strata の返答が必要なときに通知する",
"手動モードで、アシスタントが承認を待っていて Strata がバックグラウンドにあるときに、デスクトップ通知を表示します。",
"Strata があなたの返答を待っています",
"アシスタントが {0} を使おうとしています。Strata を開いて承認または拒否してください。",
"ツール",
"トレイに最小化",
"ウィンドウを閉じても、Strata は時計の横のトレイで動き続けます。止めるには、トレイのメニューで「終了」を選びます。次に Strata を起動したときから有効になります。",
"アプリのポート",
"Strata のウィンドウとローカルアドレスが使うポートです。空欄なら Strata が選びます(空いていれば 47831)。次に Strata を起動したときから有効になります。",
"ポート(空欄 = 自動)",
"現在のポートは {0} です。",
"Strata は指定のポートを使えませんでした",
"ポート {0} は別のプログラムが使っているため、Strata はポート {1} で起動しました。ポート {0} に保存されたチャットと設定は、Strata が再びそのポートを使えるようになるまで表示されません。他のプログラムを閉じて Strata を再起動してください。",
"1024 から 65535 の間のポートを選ぶか、空欄にしてください。",
"Strata のすべての文字、ボタン、メニューを大きくしたり小さくしたりします。",
"消費電力量と温度の値",
"1 kWh あたり、通貨は {0}",
"1 kWh あたり",
"通貨を変更",
"自動({0})",
"通貨",
"{0} 未満",
],
"de": [
"Energie seit dem Start",
"Kosten seit dem Start",
"GPU-Lüfter",
"GPU-Takt",
"CPU-Temperatur",
"SSD-Temperatur",
"Strompreis",
"Preis einer Kilowattstunde. Der Monitor zeigt dann, was Ihre Grafikkarte seit dem Start von Strata gekostet hat. Leer lassen, um die Kosten auszublenden.",
"Preis pro kWh",
"Ton, wenn Arbeit fertig ist",
"Spielt einen kurzen Ton, wenn eine Antwort oder Aufgabe, die länger gedauert hat, fertig ist oder wenn Strata im Hintergrund läuft.",
"Ton abspielen",
"Benachrichtigen, wenn Strata eine Antwort braucht",
"Zeigt im manuellen Modus eine Desktop-Benachrichtigung, wenn der Assistent auf Ihre Freigabe wartet und Strata im Hintergrund läuft.",
"Strata braucht Ihre Antwort",
"Der Assistent möchte {0} verwenden. Öffnen Sie Strata, um zu erlauben oder abzulehnen.",
"ein Werkzeug",
"In den Infobereich minimieren",
"Beim Schließen des Fensters läuft Strata im Infobereich neben der Uhr weiter. Wählen Sie im Menü des Symbols „Beenden“, um es zu stoppen. Gilt ab dem nächsten Start von Strata.",
"App-Port",
"Der Port, den das Fenster und die lokale Adresse von Strata verwenden. Leer lassen, damit Strata ihn wählt (47831, wenn frei). Gilt ab dem nächsten Start von Strata.",
"Port (leer = automatisch)",
"Aktuell wird Port {0} verwendet.",
"Strata konnte seinen Port nicht verwenden",
"Port {0} wird von einem anderen Programm verwendet, daher wurde Strata auf Port {1} gestartet. Unter Port {0} gespeicherte Chats und Einstellungen werden nicht angezeigt, bis Strata ihn wieder verwenden kann. Schließen Sie das andere Programm und starten Sie Strata neu.",
"Wählen Sie einen Port zwischen 1024 und 65535 oder lassen Sie das Feld leer.",
"Macht alle Texte, Schaltflächen und Menüs in Strata größer oder kleiner.",
"Energie- und Temperaturwerte",
"pro kWh, in {0}",
"pro kWh",
"Währung ändern",
"Automatisch ({0})",
"Währung",
"weniger als {0}",
],
"es": [
"Energía desde el inicio",
"Coste desde el inicio",
"Ventilador de la GPU",
"Reloj de la GPU",
"Temperatura de la CPU",
"Temperatura del SSD",
"Precio de la electricidad",
"Precio de un kilovatio hora. El Monitor mostrará entonces lo que ha costado tu tarjeta gráfica desde que se inició Strata. Déjalo vacío para ocultar el coste.",
"Precio por kWh",
"Sonido al terminar el trabajo",
"Reproduce un sonido breve cuando termina una respuesta o tarea que tardó un rato, o cuando Strata está en segundo plano.",
"Reproducir sonido",
"Avisarme cuando Strata necesite una respuesta",
"En el modo manual, muestra una notificación de escritorio cuando el asistente espera tu aprobación y Strata está en segundo plano.",
"Strata necesita tu respuesta",
"El asistente quiere usar {0}. Abre Strata para aprobar o denegar.",
"una herramienta",
"Minimizar a la bandeja",
"Al cerrar la ventana, Strata sigue funcionando en la bandeja, junto al reloj. Elige Salir en el menú de la bandeja para detenerlo. Se aplica la próxima vez que se inicie Strata.",
"Puerto de la aplicación",
"El puerto que usan la ventana y la dirección local de Strata. Déjalo vacío para que Strata elija (47831 si está libre). Se aplica la próxima vez que se inicie Strata.",
"Puerto (vacío = automático)",
"Ahora se usa el puerto {0}.",
"Strata no pudo usar su puerto",
"El puerto {0} lo usa otro programa, así que Strata se inició en el puerto {1}. Los chats y ajustes guardados en el puerto {0} no se muestran hasta que Strata pueda volver a usarlo. Cierra el otro programa y reinicia Strata.",
"Elige un puerto entre 1024 y 65535, o déjalo vacío.",
"Hace más grandes o más pequeños todos los textos, botones y menús de Strata.",
"Lecturas de energía y temperatura",
"por kWh, en {0}",
"por kWh",
"Cambiar moneda",
"Automática ({0})",
"Moneda",
"menos de {0}",
],
"ru": [
"Энергия с запуска",
"Стоимость с запуска",
"Вентилятор GPU",
"Частота GPU",
"Температура CPU",
"Температура SSD",
"Цена электричества",
"Цена одного киловатт-часа. Тогда монитор покажет, во сколько видеокарта обошлась с запуска Strata. Оставьте поле пустым, чтобы скрыть стоимость.",
"Цена за кВт·ч",
"Звук по окончании работы",
"Проигрывает короткий звук, когда закончился долгий ответ или задача, а также когда Strata работает в фоне.",
"Проиграть звук",
"Уведомлять, когда Strata нужен ответ",
"В ручном режиме показывает уведомление на рабочем столе, когда помощник ждёт вашего подтверждения, а Strata работает в фоне.",
"Strata нужен ваш ответ",
"Помощник хочет использовать {0}. Откройте Strata, чтобы разрешить или отклонить.",
"инструмент",
"Сворачивать в трей",
"При закрытии окна Strata продолжает работать в трее рядом с часами. Чтобы остановить её, выберите «Выход» в меню значка. Действует со следующего запуска Strata.",
"Порт приложения",
"Порт, который используют окно и локальный адрес Strata. Оставьте поле пустым, чтобы Strata выбрала сама (47831, если свободен). Действует со следующего запуска Strata.",
"Порт (пусто = автоматически)",
"Сейчас используется порт {0}.",
"Strata не смогла использовать свой порт",
"Порт {0} занят другой программой, поэтому Strata запустилась на порту {1}. Чаты и настройки, сохранённые на порту {0}, не показываются, пока Strata не сможет снова его использовать. Закройте другую программу и перезапустите Strata.",
"Выберите порт от 1024 до 65535 или оставьте поле пустым.",
"Увеличивает или уменьшает весь текст, кнопки и меню в Strata.",
"Показания энергии и температуры",
"за кВт·ч, в валюте {0}",
"за кВт·ч",
"Сменить валюту",
"Автоматически ({0})",
"Валюта",
"меньше {0}",
],
"pt": [
"Energia desde o início",
"Custo desde o início",
"Ventoinha da GPU",
"Clock da GPU",
"Temperatura da CPU",
"Temperatura do SSD",
"Preço da eletricidade",
"Preço de um quilowatt-hora. O Monitor passa a mostrar quanto a sua placa de vídeo custou desde que o Strata iniciou. Deixe vazio para ocultar o custo.",
"Preço por kWh",
"Som ao terminar o trabalho",
"Toca um som curto quando uma resposta ou tarefa que demorou termina, ou quando o Strata está em segundo plano.",
"Tocar som",
"Avisar quando o Strata precisar de uma resposta",
"No modo manual, mostra uma notificação na área de trabalho quando o assistente espera sua aprovação e o Strata está em segundo plano.",
"O Strata precisa da sua resposta",
"O assistente quer usar {0}. Abra o Strata para aprovar ou negar.",
"uma ferramenta",
"Minimizar para a bandeja",
"Ao fechar a janela, o Strata continua em execução na bandeja, ao lado do relógio. Escolha Sair no menu da bandeja para encerrá-lo. Vale a partir da próxima vez que o Strata iniciar.",
"Porta do aplicativo",
"A porta usada pela janela e pelo endereço local do Strata. Deixe vazio para o Strata escolher (47831 quando estiver livre). Vale a partir da próxima vez que o Strata iniciar.",
"Porta (vazio = automático)",
"Usando a porta {0} agora.",
"O Strata não conseguiu usar a sua porta",
"A porta {0} está em uso por outro programa, então o Strata iniciou na porta {1}. Os chats e as configurações salvos na porta {0} não aparecem até o Strata poder usá-la de novo. Feche o outro programa e reinicie o Strata.",
"Escolha uma porta entre 1024 e 65535, ou deixe vazio.",
"Deixa todos os textos, botões e menus do Strata maiores ou menores.",
"Leituras de energia e temperatura",
"por kWh, em {0}",
"por kWh",
"Alterar moeda",
"Automática ({0})",
"Moeda",
"menos de {0}",
],
"ko": [
"시작 이후 에너지",
"시작 이후 비용",
"GPU 팬",
"GPU 클럭",
"CPU 온도",
"SSD 온도",
"전기 요금",
"1킬로와트시의 요금입니다. 모니터에 Strata를 시작한 뒤 그래픽 카드에 든 전기 요금이 표시됩니다. 비워 두면 비용을 표시하지 않습니다.",
"kWh당 요금",
"작업이 끝났을 때 소리",
"답변이나 오래 걸린 작업이 끝났을 때, 또는 Strata가 백그라운드에 있을 때 짧은 소리를 재생합니다.",
"소리 재생",
"Strata가 답을 필요로 할 때 알림",
"수동 모드에서 어시스턴트가 승인을 기다리고 Strata가 백그라운드에 있을 때 데스크톱 알림을 표시합니다.",
"Strata가 답을 기다리고 있습니다",
"어시스턴트가 {0}을(를) 사용하려고 합니다. Strata를 열어 승인하거나 거부하세요.",
"도구",
"트레이로 최소화",
"창을 닫아도 Strata는 시계 옆 트레이에서 계속 실행됩니다. 끝내려면 트레이 메뉴에서 '종료'를 선택하세요. 다음에 Strata를 시작할 때부터 적용됩니다.",
"앱 포트",
"Strata의 창과 로컬 주소가 사용하는 포트입니다. 비워 두면 Strata가 고릅니다(비어 있으면 47831). 다음에 Strata를 시작할 때부터 적용됩니다.",
"포트(비움 = 자동)",
"지금은 포트 {0}을(를) 사용 중입니다.",
"Strata가 지정한 포트를 사용할 수 없었습니다",
"포트 {0}을(를) 다른 프로그램이 사용 중이어서 Strata는 포트 {1}에서 시작했습니다. 포트 {0}에 저장된 채팅과 설정은 Strata가 다시 그 포트를 쓸 수 있을 때까지 표시되지 않습니다. 다른 프로그램을 닫고 Strata를 다시 시작하세요.",
"1024에서 65535 사이의 포트를 고르거나 비워 두세요.",
"Strata의 모든 글자, 버튼, 메뉴를 더 크게 또는 더 작게 만듭니다.",
"에너지 및 온도 측정값",
"kWh당, 통화 {0}",
"kWh당",
"통화 변경",
"자동({0})",
"통화",
"{0} 미만",
],
"fr": [
"Énergie depuis le démarrage",
"Coût depuis le démarrage",
"Ventilateur du GPU",
"Fréquence du GPU",
"Température du CPU",
"Température du SSD",
"Prix de l'électricité",
"Prix d'un kilowattheure. Le Moniteur affiche alors ce que votre carte graphique a coûté depuis le démarrage de Strata. Laissez vide pour masquer le coût.",
"Prix par kWh",
"Son à la fin du travail",
"Joue un court son quand une réponse ou une tâche qui a pris du temps est terminée, ou quand Strata est en arrière-plan.",
"Jouer le son",
"Me prévenir quand Strata a besoin d'une réponse",
"En mode manuel, affiche une notification sur le bureau quand l'assistant attend votre accord et que Strata est en arrière-plan.",
"Strata a besoin de votre réponse",
"L'assistant veut utiliser {0}. Ouvrez Strata pour accepter ou refuser.",
"un outil",
"Réduire dans la zone de notification",
"Fermer la fenêtre garde Strata actif dans la zone de notification, près de l'horloge. Choisissez Quitter dans le menu de l'icône pour l'arrêter. S'applique au prochain démarrage de Strata.",
"Port de l'application",
"Le port que la fenêtre et l'adresse locale de Strata utilisent. Laissez vide pour que Strata choisisse (47831 s'il est libre). S'applique au prochain démarrage de Strata.",
"Port (vide = automatique)",
"Port {0} utilisé en ce moment.",
"Strata n'a pas pu utiliser son port",
"Le port {0} est utilisé par un autre programme, donc Strata a démarré sur le port {1}. Les conversations et réglages enregistrés sur le port {0} ne s'affichent pas tant que Strata ne peut pas le réutiliser. Fermez l'autre programme et relancez Strata.",
"Choisissez un port entre 1024 et 65535, ou laissez vide.",
"Agrandit ou réduit tous les textes, boutons et menus de Strata.",
"Relevés d'énergie et de température",
"par kWh, en {0}",
"par kWh",
"Changer de devise",
"Automatique ({0})",
"Devise",
"moins de {0}",
],
}
here = Path(__file__).resolve().parent.parent / "src" / "locales"
NBSP = "\u00a0"
def french(text):
"""The same French spacing as scripts/fr_sync.py: a typographic apostrophe, a no-break space before : ; ? ! % and inside the guillemets."""
text = re.sub(r"(?<=[A-Za-z\u00c0-\u017f])'(?=[A-Za-z\u00c0-\u017f{])", "\u2019", text)
text = re.sub(r"(?<=\S) ([:;?!])", NBSP + r"\1", text)
text = re.sub(r"(?<=[\d}]) %", NBSP + "%", text)
return text
def write(lang, values):
assert len(values) == len(EN), (lang, len(values), len(EN))
for source, text in zip(EN, values):
assert sorted(re.findall(r"\{\d+\}", source)) == sorted(re.findall(r"\{\d+\}", text)), (lang, source, text)
if lang == "fr":
values = [french(v) for v in values]
data = {f"gh{i + 1:02d}": text for i, text in enumerate(values)}
(here / f"gh-{lang}.json").write_text(json.dumps(data, ensure_ascii=False, indent=1) + "\n", encoding="utf-8")
write("en", EN)
for lang, values in T.items():
write(lang, values)
print("written", len(EN), "strings in", len(T) + 1, "languages")
+60
View File
@@ -0,0 +1,60 @@
"""Writes src/locales/gui-<lang>.json: the strings of the GUI feedback round 2 (branch f-0291-gui-fb2): the new name and hint of the "Leave room for my work"
choice (it keeps 5-8 GB of memory free; it was "Leave 20% for my work"), the memory-error sentence that names it, and the words of the compact files block
("Delivered to", "Working folder", "{0} files").
Run from apps/host: python scripts/make-gui-locales.py
Every entry names all nine languages; the keys are gf01.. in the order of S. gui-en.json is the English source. Placeholders ({0}) must be kept in every language.
A separate file keeps this work from colliding with the other catalogs (the same reason pick-, tune-, tools-, fix-, gh-, ui-, engine- and df2- exist)."""
import json
import re
from pathlib import Path
LANGS = ["en", "zh", "ja", "de", "es", "ru", "pt", "ko", "fr"]
S = []
def add(en, zh, ja, de, es, ru, pt, ko, fr):
S.append(dict(en=en, zh=zh, ja=ja, de=de, es=es, ru=ru, pt=pt, ko=ko, fr=fr))
add("Leave room for my work",
"为我的工作留出空间", "作業用の余裕を残す", "Platz für meine Arbeit lassen", "Dejar espacio para mi trabajo",
"Оставить запас для моей работы", "Deixar espaço para o meu trabalho", "내 작업을 위한 여유 남기기", "Garder de la place pour mon travail")
add("Keeps 5-8 GB of memory free for other programs.",
"为其他程序保留 5-8 GB 空闲内存。", "他のプログラム用に 5~8 GB のメモリを空けておきます。", "Hält 5-8 GB Arbeitsspeicher für andere Programme frei.",
"Mantiene libres 5-8 GB de memoria para otros programas.", "Оставляет 5-8 ГБ памяти свободными для других программ.",
"Mantém 5-8 GB de memória livres para outros programas.", "다른 프로그램을 위해 메모리 5-8 GB를 비워 둡니다.", "Garde 5-8 Go de mémoire libres pour les autres programmes.")
add("Close other programs and try again, or choose “Leave room for my work”. In Expert mode you can also turn on the low-RAM option, so part of the model is read from the SSD.",
"请关闭其他程序后重试,或选择“为我的工作留出空间”。在专家模式下,你还可以开启低内存选项,让部分模型从 SSD 读取。",
"他のプログラムを閉じてもう一度試すか、「作業用の余裕を残す」を選んでください。エキスパートモードでは低 RAM オプションをオンにして、モデルの一部を SSD から読み込むこともできます。",
"Schließen Sie andere Programme und versuchen Sie es erneut, oder wählen Sie „Platz für meine Arbeit lassen“. Im Expertenmodus können Sie außerdem die Option für wenig Arbeitsspeicher aktivieren, sodass ein Teil des Modells von der SSD gelesen wird.",
"Cierre otros programas e inténtelo de nuevo, o elija «Dejar espacio para mi trabajo». En el modo experto también puede activar la opción de poca RAM, para que una parte del modelo se lea desde el SSD.",
"Закройте другие программы и повторите попытку или выберите «Оставить запас для моей работы». В экспертном режиме можно также включить режим малого объёма ОЗУ, и часть модели будет читаться с SSD.",
"Feche outros programas e tente novamente, ou escolha “Deixar espaço para o meu trabalho”. No modo especialista, você também pode ativar a opção de pouca RAM, para que parte do modelo seja lida do SSD.",
"다른 프로그램을 닫고 다시 시도하거나 “내 작업을 위한 여유 남기기”를 선택하세요. 전문가 모드에서는 저RAM 옵션을 켜서 모델의 일부를 SSD에서 읽게 할 수도 있습니다.",
"Fermez les autres programmes et réessayez, ou choisissez « Garder de la place pour mon travail ». En mode expert, vous pouvez aussi activer l'option de faible RAM : une partie du modèle est alors lue depuis le SSD.")
add("Delivered to",
"已交付到", "保存先", "Geliefert nach", "Entregado en", "Результат в", "Entregue em", "전달 위치", "Livré dans")
add("Working folder",
"工作文件夹", "作業フォルダー", "Arbeitsordner", "Carpeta de trabajo", "Рабочая папка", "Pasta de trabalho", "작업 폴더", "Dossier de travail")
add("{0} files",
"{0} 个文件", "{0} 個のファイル", "{0} Dateien", "{0} archivos", "Файлов: {0}", "{0} arquivos", "파일 {0}개", "{0} fichiers")
here = Path(__file__).resolve().parent.parent / "src" / "locales"
NBSP = " "
def french(text):
text = re.sub(r"(?<=[A-Za-zÀ-ſ])'(?=[A-Za-zÀ-ſ{])", "’", text)
text = re.sub(r"(?<=\S) ([:;?!])", NBSP + r"\1", text)
text = re.sub(r"(?<=[\d}]) %", NBSP + "%", text)
return text
for lang in LANGS:
data = {}
for i, entry in enumerate(S):
text = entry[lang]
assert sorted(re.findall(r"\{\d+\}", entry["en"])) == sorted(re.findall(r"\{\d+\}", text)), (lang, entry["en"], text)
data[f"gf{i + 1:02d}"] = french(text) if lang == "fr" else text
with (here / f"gui-{lang}.json").open("w", encoding="utf-8", newline="\n") as handle:
handle.write(json.dumps(data, ensure_ascii=False, indent=1) + "\n")
print("written", len(S), "strings in", len(LANGS), "languages")
+75
View File
@@ -0,0 +1,75 @@
"""Writes src/locales/pick-<lang>.json: the strings of the model version list (VersionPicker), the code-block preview button and the This computer panel (branch f-0291-ui-fixes2).
Run from apps/host: python scripts/make-pick-locales.py
Every entry names all nine languages; the keys are vp01.. in the order of S. pick-en.json is the English source. Placeholders ({0}, {1}) must be kept
in every language. A separate file keeps this work from colliding with the other catalogs (the same reason tune-, tools-, fix-, gh-, ui- and engine- exist)."""
import json
import re
from pathlib import Path
LANGS = ["en", "zh", "ja", "de", "es", "ru", "pt", "ko", "fr"]
S = []
def add(en, zh, ja, de, es, ru, pt, ko, fr):
S.append(dict(en=en, zh=zh, ja=ja, de=de, es=es, ru=ru, pt=pt, ko=ko, fr=fr))
add("{0} download",
"下载 {0}", "ダウンロード {0}", "{0} Download", "Descarga de {0}", "Загрузка {0}", "Download de {0}", "다운로드 {0}", "Téléchargement de {0}")
add("Needs {0} of memory",
"需要 {0} 内存", "{0} のメモリーが必要", "Braucht {0} Speicher", "Necesita {0} de memoria", "Нужно {0} памяти", "Precisa de {0} de memória", "메모리 {0} 필요", "Nécessite {0} de mémoire")
add("Needs {0} of memory and about {1} GB of disk",
"需要 {0} 内存和约 {1} GB 磁盘空间", "{0} のメモリーと約 {1} GB のディスク容量が必要", "Braucht {0} Speicher und etwa {1} GB Festplattenplatz", "Necesita {0} de memoria y unos {1} GB de disco",
"Нужно {0} памяти и около {1} ГБ на диске", "Precisa de {0} de memória e cerca de {1} GB de disco", "메모리 {0}와 디스크 약 {1} GB 필요", "Nécessite {0} de mémoire et environ {1} Go de disque")
add("Fits",
"可以运行", "問題なく動きます", "Passt", "Cabe", "Помещается", "Cabe", "문제없이 실행됩니다", "Tient")
add("Fits with SSD streaming",
"可以运行,部分内容从 SSD 读取", "SSD から読み込みながら動きます", "Passt mit SSD-Streaming", "Cabe con streaming desde el SSD", "Помещается с потоковой подгрузкой с SSD", "Cabe com streaming do SSD", "SSD 스트리밍으로 실행됩니다", "Tient avec streaming depuis le SSD")
add("Doesn't fit",
"放不下", "収まりません", "Passt nicht", "No cabe", "Не помещается", "Não cabe", "맞지 않습니다", "Ne tient pas")
add("Show details",
"显示详情", "詳細を表示", "Details anzeigen", "Mostrar detalles", "Показать подробности", "Mostrar detalhes", "자세히 보기", "Afficher les détails")
add("Hide details",
"隐藏详情", "詳細を隠す", "Details ausblenden", "Ocultar detalles", "Скрыть подробности", "Ocultar detalhes", "자세히 숨기기", "Masquer les détails")
add("{0} RAM + VRAM",
"{0} 内存 + 显存", "{0} の RAM + VRAM", "{0} RAM + VRAM", "{0} de RAM + VRAM", "{0} RAM + VRAM", "{0} de RAM + VRAM", "RAM + VRAM {0}", "{0} de RAM + VRAM")
add("Close preview",
"关闭预览", "プレビューを閉じる", "Vorschau schließen", "Cerrar vista previa", "Закрыть предпросмотр", "Fechar pré-visualização", "미리보기 닫기", "Fermer l'aperçu")
add("Free right now",
"当前可用", "現在の空き", "Gerade frei", "Libre ahora", "Свободно сейчас", "Livre agora", "지금 비어 있음", "Libre en ce moment")
add("{0} free",
"可用 {0}", "空き {0}", "{0} frei", "{0} libres", "свободно {0}", "{0} livres", "{0} 사용 가능", "{0} libres")
add("{0}: {1} free of {2}",
"{0}:{2} 中有 {1} 可用", "{0}:{2} のうち {1} が空き", "{0}: {1} frei von {2}", "{0}: {1} libres de {2}", "{0}: свободно {1} из {2}", "{0}: {1} livres de {2}", "{0}: {2} 중 {1} 사용 가능", "{0} : {1} libres sur {2}")
add("It takes about",
"它大约占用", "使用量の目安", "Es belegt etwa", "Ocupa aproximadamente", "Занимает примерно", "Ocupa cerca de", "대략 사용하는 양", "Il occupe environ")
add("Nothing. It fits in memory.",
"没有。它完全放得进内存。", "ありません。すべてメモリーに収まっています。", "Nichts. Es passt in den Speicher.", "Nada. Cabe en la memoria.", "Ничего. Всё помещается в память.", "Nada. Cabe na memória.", "없음. 메모리에 모두 들어갑니다.", "Rien. Tout tient en mémoire.")
add("Reading from your SSD now: {0}",
"此刻正从 SSD 读取:{0}", "現在 SSD から読み込み中:{0}", "Gerade vom SSD gelesen: {0}", "Leyendo del SSD ahora: {0}", "Сейчас читается с SSD: {0}", "Lendo do SSD agora: {0}", "지금 SSD에서 읽는 중: {0}", "Lecture depuis le SSD en cours : {0}")
add("This version would take about {0} of graphics memory and {1} of RAM.",
"这个版本大约会占用 {0} 显存和 {1} 内存。", "このバージョンはグラフィックスメモリーを約 {0}、RAM を約 {1} 使います。", "Diese Version würde etwa {0} Grafikspeicher und {1} RAM belegen.",
"Esta versión ocuparía aproximadamente {0} de memoria gráfica y {1} de RAM.", "Эта версия займёт примерно {0} видеопамяти и {1} оперативной памяти.", "Esta versão ocuparia cerca de {0} de memória gráfica e {1} de RAM.",
"이 버전은 그래픽 메모리 약 {0}와 RAM 약 {1}을 사용합니다.", "Cette version occuperait environ {0} de mémoire graphique et {1} de RAM.")
here = Path(__file__).resolve().parent.parent / "src" / "locales"
NBSP = " "
def french(text):
text = re.sub(r"(?<=[A-Za-zÀ-ſ])'(?=[A-Za-zÀ-ſ{])", "’", text)
text = re.sub(r"(?<=\S) ([:;?!])", NBSP + r"\1", text)
text = re.sub(r"(?<=[\d}]) %", NBSP + "%", text)
return text
for lang in LANGS:
data = {}
for i, entry in enumerate(S):
text = entry[lang]
assert sorted(re.findall(r"\{\d+\}", entry["en"])) == sorted(re.findall(r"\{\d+\}", text)), (lang, entry["en"], text)
data[f"vp{i + 1:02d}"] = french(text) if lang == "fr" else text
(here / f"pick-{lang}.json").write_text(json.dumps(data, ensure_ascii=False, indent=1) + "\n", encoding="utf-8")
print("written", len(S), "strings in", len(LANGS), "languages")
+67
View File
@@ -0,0 +1,67 @@
"""Writes src/locales/r3-<lang>.json: the strings added by the owner's GUI feedback round 3 (branch f-0291-ui-r3).
Run from apps/host: python scripts/make-r3-locales.py
Every entry names all nine languages; the keys are r3_01.. in the order of S. r3-en.json is the English source. Placeholders ({0}, {1}) must be kept in every
language. A separate file keeps this work from colliding with the other catalogs (the same reason pick-, upd-, df2- and the others exist)."""
import json
import re
from pathlib import Path
LANGS = ["en", "zh", "ja", "de", "es", "ru", "pt", "ko", "fr"]
S = []
def add(en, zh, ja, de, es, ru, pt, ko, fr):
S.append(dict(en=en, zh=zh, ja=ja, de=de, es=es, ru=ru, pt=pt, ko=ko, fr=fr))
# 2: the Read pictures row while the installed engine has no picture reader (a tooltip, not a paragraph)
add("Needs an engine update",
"需要更新引擎", "エンジンの更新が必要です", "Braucht ein Engine-Update", "Necesita actualizar el motor", "Нужно обновить движок", "Precisa de uma atualização do motor", "엔진 업데이트가 필요합니다", "Nécessite une mise à jour du moteur")
# 5: the one word the version list writes for a version that does not fully fit in memory
add("SSD streaming · will run slower",
"SSD 流式读取 · 运行会更慢", "SSD ストリーミング · 動作は遅くなります", "SSD-Streaming · läuft langsamer", "Streaming desde el SSD · irá más lento", "Потоковая подгрузка с SSD · будет работать медленнее",
"Streaming do SSD · vai rodar mais devagar", "SSD 스트리밍 · 더 느리게 실행됩니다", "Streaming depuis le SSD · plus lent")
# 6: the warning under the conversation context when a long context leaves little graphics memory for the model (nothing is refused or lowered)
add("This much context takes about {0} GB of graphics memory and leaves little room for the model on this card. Strata will try it; if it does not start, choose a shorter context.",
"这么长的上下文大约占用 {0} GB 显存,留给模型的空间很少。Strata 会尝试运行;如果无法启动,请选择更短的上下文。",
"このコンテキストはグラフィックスメモリーを約 {0} GB 使い、このカードではモデルに残る余裕がわずかです。Strata は試しますが、起動しない場合はより短いコンテキストを選んでください。",
"So viel Kontext belegt etwa {0} GB Grafikspeicher und lässt auf dieser Karte wenig Platz für das Modell. Strata versucht es; startet es nicht, wähle einen kürzeren Kontext.",
"Este contexto ocupa unos {0} GB de memoria gráfica y deja poco espacio para el modelo en esta tarjeta. Strata lo intentará; si no arranca, elige un contexto más corto.",
"Такой контекст занимает около {0} ГБ видеопамяти и оставляет модели мало места на этой карте. Strata попробует запустить; если не получится, выберите контекст короче.",
"Esse contexto ocupa cerca de {0} GB de memória gráfica e deixa pouco espaço para o modelo nesta placa. O Strata vai tentar; se não iniciar, escolha um contexto menor.",
"이 정도의 컨텍스트는 그래픽 메모리를 약 {0} GB 사용하며 이 카드에서는 모델이 쓸 공간이 거의 남지 않습니다. Strata가 시도하지만 시작되지 않으면 더 짧은 컨텍스트를 선택하세요.",
"Ce contexte occupe environ {0} Go de mémoire graphique et laisse peu de place au modèle sur cette carte. Strata va essayer ; s'il ne démarre pas, choisissez un contexte plus court.")
# 9: a chat that is working while the person is elsewhere (the ring on its row, and the model chip's line)
add("Working · {0}",
"进行中 · {0}", "作業中 · {0}", "In Arbeit · {0}", "Trabajando · {0}", "В работе · {0}", "Trabalhando · {0}", "작업 중 · {0}", "En cours de travail · {0}")
add("{0} chats working",
"{0} 个对话进行中", "{0} 件のチャットが作業中", "{0} Chats in Arbeit", "{0} chats trabajando", "В работе чатов: {0}", "{0} chats trabalhando", "대화 {0}개 작업 중", "{0} conversations en cours")
# 10: the size line of a file being written
add("{0} lines",
"{0} 行", "{0} 行", "{0} Zeilen", "{0} líneas", "{0} строк", "{0} linhas", "{0}줄", "{0} lignes")
#@@MORE@@
here = Path(__file__).resolve().parent.parent / "src" / "locales"
NBSP = " "
def french(text):
text = re.sub(r"(?<=[A-Za-zÀ-ſ])'(?=[A-Za-zÀ-ſ{])", "’", text)
text = re.sub(r"(?<=\S) ([:;?!])", NBSP + r"\1", text)
text = re.sub(r"(?<=[\d}]) %", NBSP + "%", text)
return text
for lang in LANGS:
data = {}
for i, entry in enumerate(S):
text = entry[lang]
assert sorted(re.findall(r"\{\d+\}", entry["en"])) == sorted(re.findall(r"\{\d+\}", text)), (lang, entry["en"], text)
data[f"r3_{i + 1:02d}"] = french(text) if lang == "fr" else text
(here / f"r3-{lang}.json").write_text(json.dumps(data, ensure_ascii=False, indent=1) + "\n", encoding="utf-8")
print("written", len(S), "strings in", len(LANGS), "languages")
+249
View File
@@ -0,0 +1,249 @@
"""Writes src/locales/ui-<lang>.json: the interface strings of the 0.2.10 interface changes (branch f-0291-ui).
Run from apps/host: python scripts/make-ui-locales.py
Every entry names all nine languages; the keys are ui01.. in the order of S. ui-en.json is the English source. Placeholders ({0}, {1}) must be
kept in every language. A separate file keeps this work from colliding with the other catalogs (the same reason tune-, tools-, fix- and gh- exist)."""
import json
import re
from pathlib import Path
LANGS = ["en", "zh", "ja", "de", "es", "ru", "pt", "ko", "fr"]
S = []
def add(**kw):
assert sorted(kw) == sorted(LANGS), sorted(set(LANGS) ^ set(kw))
S.append(kw)
# #4725 the approval stays in view
add(en="Waiting for your answer",
zh="等待你的回答", ja="あなたの返答を待っています", de="Wartet auf deine Antwort", es="Esperando tu respuesta",
ru="Ждёт вашего ответа", pt="Aguardando a sua resposta", ko="답변을 기다리는 중", fr="En attente de votre réponse")
add(en="Waiting for your answer below",
zh="等待你在下方回答", ja="下で返答をお待ちしています", de="Wartet unten auf deine Antwort", es="Esperando tu respuesta abajo",
ru="Ждёт вашего ответа ниже", pt="Aguardando a sua resposta abaixo", ko="아래에서 답변을 기다리는 중", fr="En attente de votre réponse ci-dessous")
# #4804 why Start tuning is greyed out
add(en="Strata is still checking this model for saved tunings. Start tuning works as soon as that is done.",
zh="Strata 仍在检查此模型是否有已保存的调优。检查完成后即可开始调优。",
ja="Strata がこのモデルの保存済みチューニングを確認しています。確認が終わりしだい、チューニングを開始できます。",
de="Strata prüft dieses Modell noch auf gespeicherte Abstimmungen. Sobald das fertig ist, kannst du die Abstimmung starten.",
es="Strata todavía está comprobando si este modelo tiene ajustes guardados. Podrás empezar el ajuste en cuanto termine.",
ru="Strata ещё проверяет, есть ли у этой модели сохранённые настройки. Как только проверка закончится, можно будет запустить настройку.",
pt="O Strata ainda está verificando se este modelo tem ajustes salvos. Assim que terminar, você poderá iniciar o ajuste.",
ko="Strata가 이 모델의 저장된 최적화를 확인하는 중입니다. 확인이 끝나면 바로 최적화를 시작할 수 있습니다.",
fr="Strata vérifie encore si ce modèle a des optimisations enregistrées. Dès que c’est fini, vous pourrez lancer l’optimisation.")
add(en="Strata could not read the tuning details for this model, so tuning cannot start. Close this window and open it again.",
zh="Strata 无法读取此模型的调优信息,因此无法开始调优。请关闭此窗口后重新打开。",
ja="Strata がこのモデルのチューニング情報を読み取れなかったため、チューニングを開始できません。このウィンドウを閉じて、もう一度開いてください。",
de="Strata konnte die Abstimmungsdaten dieses Modells nicht lesen, deshalb lässt sich die Abstimmung nicht starten. Schließe dieses Fenster und öffne es erneut.",
es="Strata no pudo leer los datos de ajuste de este modelo, así que el ajuste no puede empezar. Cierra esta ventana y ábrela de nuevo.",
ru="Strata не смогла прочитать данные настройки этой модели, поэтому настройку нельзя запустить. Закройте это окно и откройте его снова.",
pt="O Strata não conseguiu ler os dados de ajuste deste modelo, então o ajuste não pode começar. Feche esta janela e abra-a novamente.",
ko="Strata가 이 모델의 최적화 정보를 읽지 못해 최적화를 시작할 수 없습니다. 이 창을 닫았다가 다시 여세요.",
fr="Strata n’a pas pu lire les données d’optimisation de ce modèle, l’optimisation ne peut donc pas démarrer. Fermez cette fenêtre et rouvrez-la.")
add(en="Checking for saved tunings\u2026",
zh="正在检查已保存的调优\u2026", ja="保存済みチューニングを確認しています\u2026", de="Gespeicherte Abstimmungen werden geprüft\u2026", es="Comprobando los ajustes guardados\u2026",
ru="Проверяем сохранённые настройки\u2026", pt="Verificando os ajustes salvos\u2026", ko="저장된 최적화를 확인하는 중\u2026", fr="Vérification des optimisations enregistrées\u2026")
# #4802 the progress card
add(en="Still working for {0}. You don't need to do anything.",
zh="仍在处理,已用时 {0}。你无需做任何操作。",
ja="処理中です({0}経過)。操作は必要ありません。",
de="Läuft noch seit {0}. Du musst nichts tun.",
es="Sigue trabajando desde hace {0}. No tienes que hacer nada.",
ru="Всё ещё работает: {0}. От вас ничего не требуется.",
pt="Ainda trabalhando há {0}. Você não precisa fazer nada.",
ko="{0}째 작업 중입니다. 따로 할 일은 없습니다.",
fr="Toujours en cours depuis {0}. Vous n'avez rien à faire.")
add(en="The buttons below wait until this finishes.",
zh="下方的按钮会在此操作完成后恢复可用。",
ja="この処理が終わるまで、下のボタンは使えません。",
de="Die Schaltflächen unten warten, bis das fertig ist.",
es="Los botones de abajo esperan a que esto termine.",
ru="Кнопки ниже подождут, пока это не закончится.",
pt="Os botões abaixo esperam até isto terminar.",
ko="아래 버튼은 이 작업이 끝날 때까지 기다립니다.",
fr="Les boutons ci-dessous attendent la fin de cette opération.")
# #4798 two addresses
add(en="API base URL on this computer",
zh="此电脑上的 API 基础地址", ja="このパソコンでの API ベース URL", de="API-Basis-URL auf diesem Computer", es="URL base de la API en este equipo",
ru="Базовый URL API на этом компьютере", pt="URL base da API neste computador", ko="이 컴퓨터의 API 기본 URL", fr="URL de base de l'API sur cet ordinateur")
add(en="Address for other devices",
zh="其他设备使用的地址", ja="他のデバイス用のアドレス", de="Adresse für andere Geräte", es="Dirección para otros dispositivos",
ru="Адрес для других устройств", pt="Endereço para outros dispositivos", ko="다른 기기용 주소", fr="Adresse pour les autres appareils")
add(en="Other devices use this address. It has its own port, separate from the one Strata's window uses, because only the model's API is shared on the network, not the app.",
zh="其他设备使用此地址。它有自己的端口,与 Strata 窗口使用的端口分开,因为网络上只共享模型的 API,而不是应用本身。",
ja="他のデバイスはこのアドレスを使います。ネットワークで共有されるのはモデルの API だけでアプリ本体ではないため、Strata のウィンドウが使うポートとは別のポートを使います。",
de="Andere Geräte verwenden diese Adresse. Sie hat einen eigenen Port, getrennt von dem des Strata-Fensters, weil im Netzwerk nur die API des Modells freigegeben wird, nicht die App.",
es="Los demás dispositivos usan esta dirección. Tiene su propio puerto, distinto del que usa la ventana de Strata, porque en la red solo se comparte la API del modelo, no la aplicación.",
ru="Другие устройства используют этот адрес. У него свой порт, отдельный от порта окна Strata, потому что в сети доступен только API модели, а не само приложение.",
pt="Os outros dispositivos usam este endereço. Ele tem a própria porta, separada da que a janela do Strata usa, porque só a API do modelo é compartilhada na rede, não o aplicativo.",
ko="다른 기기는 이 주소를 사용합니다. 네트워크에는 앱이 아니라 모델의 API만 공유되므로, Strata 창이 쓰는 포트와는 별도의 포트를 씁니다.",
fr="Les autres appareils utilisent cette adresse. Elle a son propre port, distinct de celui de la fenêtre de Strata, car seule l'API du modèle est partagée sur le réseau, pas l'application.")
# #3723 Context memory and the out-of-graphics-memory reason
add(en="INT8 is the standard: good quality and a medium amount of memory.",
zh="INT8 是标准选项:质量好,内存占用适中。",
ja="INT8 は標準の設定です。品質が良く、メモリ使用量は中程度です。",
de="INT8 ist der Standard: gute Qualität und mittlerer Speicherbedarf.",
es="INT8 es el estándar: buena calidad y un uso de memoria medio.",
ru="INT8 — стандартный вариант: хорошее качество и средний расход памяти.",
pt="INT8 é o padrão: boa qualidade e uso médio de memória.",
ko="INT8은 표준 설정입니다. 품질이 좋고 메모리 사용량은 보통입니다.",
fr="INT8 est le réglage standard : bonne qualité et une consommation de mémoire moyenne.")
add(en="Q4 uses about half the memory of INT8, but is less precise on very long documents.",
zh="Q4 占用的内存约为 INT8 的一半,但在很长的文档上精度较低。",
ja="Q4 は INT8 の約半分のメモリで済みますが、非常に長い文書では精度が少し下がります。",
de="Q4 braucht etwa halb so viel Speicher wie INT8, ist aber bei sehr langen Dokumenten weniger genau.",
es="Q4 usa aproximadamente la mitad de memoria que INT8, pero es menos preciso con documentos muy largos.",
ru="Q4 занимает примерно вдвое меньше памяти, чем INT8, но менее точен на очень длинных документах.",
pt="Q4 usa cerca de metade da memória do INT8, mas é menos preciso em documentos muito longos.",
ko="Q4는 INT8의 약 절반 메모리를 쓰지만, 아주 긴 문서에서는 정밀도가 떨어집니다.",
fr="Q4 utilise environ moitié moins de mémoire que INT8, mais il est moins précis sur de très longs documents.")
add(en="K8 / V4 uses about a quarter less memory than INT8 and is more precise than Q4. Prompts are 2 to 5% slower.",
zh="K8 / V4 比 INT8 约少占用四分之一的内存,精度高于 Q4。处理提示词会慢 2% 到 5%。",
ja="K8 / V4 は INT8 より約4分の1少ないメモリで済み、Q4 より精度が高い設定です。プロンプトの処理は 2〜5% 遅くなります。",
de="K8 / V4 braucht etwa ein Viertel weniger Speicher als INT8 und ist genauer als Q4. Prompts sind 2 bis 5 % langsamer.",
es="K8 / V4 usa aproximadamente una cuarta parte menos de memoria que INT8 y es más preciso que Q4. Los prompts son entre un 2 y un 5 % más lentos.",
ru="K8 / V4 занимает примерно на четверть меньше памяти, чем INT8, и точнее, чем Q4. Запросы обрабатываются на 2–5 % медленнее.",
pt="K8 / V4 usa cerca de um quarto a menos de memória que o INT8 e é mais preciso que o Q4. Os prompts ficam de 2 a 5 % mais lentos.",
ko="K8 / V4는 INT8보다 메모리를 약 4분의 1 적게 쓰고 Q4보다 정밀합니다. 프롬프트 처리는 2~5% 느려집니다.",
fr="K8 / V4 utilise environ un quart de mémoire en moins que INT8 et est plus précis que Q4. Les prompts sont 2 à 5 % plus lents.")
add(en="The model did not fit in your graphics card’s memory.",
zh="模型放不进你的显卡显存。",
ja="モデルがグラフィックスカードのメモリに収まりませんでした。",
de="Das Modell hat nicht in den Speicher deiner Grafikkarte gepasst.",
es="El modelo no cabía en la memoria de tu tarjeta gráfica.",
ru="Модель не поместилась в память вашей видеокарты.",
pt="O modelo não coube na memória da sua placa de vídeo.",
ko="모델이 그래픽 카드 메모리에 들어가지 않았습니다.",
fr="Le modèle ne tenait pas dans la mémoire de votre carte graphique.")
add(en="Lower the conversation context, or set Context memory to Q4 in Expert settings, then try again.",
zh="请调低对话上下文,或在专家设置中把“上下文内存”设为 Q4,然后重试。",
ja="会話のコンテキストを小さくするか、上級設定で「コンテキストメモリー」を Q4 にして、もう一度お試しください。",
de="Verkleinere den Gesprächskontext oder stelle in den Experteneinstellungen „Kontextspeicher“ auf Q4 und versuche es erneut.",
es="Reduce el contexto de la conversación o pon «Memoria de contexto» en Q4 en Ajustes expertos, y vuelve a intentarlo.",
ru="Уменьшите контекст разговора или установите «Память контекста» в Q4 в разделе «Экспертные настройки» и повторите попытку.",
pt="Reduza o contexto da conversa ou defina «Memória de contexto» como Q4 em Configurações avançadas e tente de novo.",
ko="대화 컨텍스트를 줄이거나 전문가 설정에서 ‘컨텍스트 메모리’를 Q4로 바꾼 뒤 다시 시도하세요.",
fr="Réduisez le contexte de la conversation, ou réglez « Mémoire de contexte » sur Q4 dans Réglages experts, puis réessayez.")
# #4749 a message for a helper
add(en="Message this helper",
zh="给这个助手发消息", ja="このヘルパーにメッセージを送る", de="Diesem Helfer schreiben", es="Escribir a este ayudante",
ru="Написать этому помощнику", pt="Enviar mensagem a este ajudante", ko="이 도우미에게 메시지 보내기", fr="Écrire à cet assistant")
add(en="Message to {0}",
zh="发给 {0} 的消息", ja="{0} へのメッセージ", de="Nachricht an {0}", es="Mensaje para {0}",
ru="Сообщение для {0}", pt="Mensagem para {0}", ko="{0}에게 보낼 메시지", fr="Message pour {0}")
add(en="Write to {0}\u2026",
zh="写给 {0}\u2026", ja="{0} に書く\u2026", de="An {0} schreiben\u2026", es="Escribe a {0}\u2026",
ru="Напишите {0}\u2026", pt="Escreva para {0}\u2026", ko="{0}에게 쓰기\u2026", fr="Écrire à {0}\u2026")
add(en="Sent to {0}.",
zh="已发给 {0}。", ja="{0} に送りました。", de="An {0} gesendet.", es="Enviado a {0}.",
ru="Отправлено: {0}.", pt="Enviado para {0}.", ko="{0}에게 보냈습니다.", fr="Envoyé à {0}.")
# #4783 a message while an answer is being written
add(en="While an answer is being written",
zh="回答生成期间", ja="回答の作成中", de="Während eine Antwort geschrieben wird", es="Mientras se escribe una respuesta",
ru="Пока пишется ответ", pt="Enquanto uma resposta é escrita", ko="답변을 작성하는 동안", fr="Pendant qu'une réponse s'écrit")
add(en="What Enter does when you send a message while the assistant is still answering.",
zh="助手仍在回答时,你发送消息后按回车会发生什么。", ja="アシスタントがまだ回答している間にメッセージを送ったとき、Enter で何が起こるかを選びます。",
de="Was die Eingabetaste bewirkt, wenn du eine Nachricht sendest, während der Assistent noch antwortet.",
es="Qué hace Intro cuando envías un mensaje mientras el asistente todavía está respondiendo.",
ru="Что делает Enter, когда вы отправляете сообщение, пока ассистент ещё отвечает.",
pt="O que o Enter faz quando você envia uma mensagem enquanto o assistente ainda está respondendo.",
ko="어시스턴트가 아직 답하는 중에 메시지를 보낼 때 Enter가 하는 일입니다.",
fr="Ce que fait Entrée quand vous envoyez un message pendant que l'assistant répond encore.")
add(en="Wait for the answer",
zh="等回答完成", ja="回答が終わるまで待つ", de="Auf die Antwort warten", es="Esperar la respuesta",
ru="Дождаться ответа", pt="Esperar a resposta", ko="답변이 끝날 때까지 기다리기", fr="Attendre la réponse")
add(en="Send after the answer",
zh="回答完成后再发送", ja="回答が終わってから送る", de="Nach der Antwort senden", es="Enviar después de la respuesta",
ru="Отправить после ответа", pt="Enviar depois da resposta", ko="답변이 끝난 뒤 보내기", fr="Envoyer après la réponse")
add(en="Interrupt the answer",
zh="打断回答", ja="回答を中断する", de="Die Antwort unterbrechen", es="Interrumpir la respuesta",
ru="Прервать ответ", pt="Interromper a resposta", ko="답변 중단하기", fr="Interrompre la réponse")
add(en="Enter sends it when the answer is done.",
zh="回答完成后,按回车即可发送。", ja="回答が終わったら Enter で送信されます。", de="Die Eingabetaste sendet sie, sobald die Antwort fertig ist.",
es="Intro lo envía cuando termina la respuesta.", ru="Enter отправит сообщение, когда ответ закончится.",
pt="O Enter envia quando a resposta terminar.", ko="답변이 끝나면 Enter로 보냅니다.", fr="Entrée l'envoie quand la réponse est terminée.")
add(en="Enter stops the answer here and sends it now.",
zh="按回车会在此处停止回答并立即发送。", ja="Enter で回答をここで止めて、すぐに送信します。", de="Die Eingabetaste stoppt die Antwort hier und sendet die Nachricht sofort.",
es="Intro detiene la respuesta aquí y lo envía ahora.", ru="Enter остановит ответ здесь и сразу отправит сообщение.",
pt="O Enter para a resposta aqui e envia agora.", ko="Enter를 누르면 여기서 답변을 멈추고 바로 보냅니다.", fr="Entrée arrête la réponse ici et l'envoie tout de suite.")
add(en="Waiting to send",
zh="等待发送", ja="送信待ち", de="Wartet auf das Senden", es="Esperando para enviar",
ru="Ждёт отправки", pt="Aguardando envio", ko="보내기 대기 중", fr="En attente d'envoi")
add(en="Take back",
zh="撤回", ja="取り消す", de="Zurücknehmen", es="Retirar",
ru="Забрать обратно", pt="Desfazer", ko="되돌리기", fr="Reprendre")
# #4785 quoting
add(en="Quote",
zh="引用", ja="引用", de="Zitieren", es="Citar",
ru="Цитировать", pt="Citar", ko="인용", fr="Citer")
add(en="Quoting the assistant",
zh="引用助手的话", ja="アシスタントの発言を引用", de="Zitat des Assistenten", es="Citando al asistente",
ru="Цитата ассистента", pt="Citando o assistente", ko="어시스턴트의 말을 인용", fr="Citation de l'assistant")
add(en="Quoting you",
zh="引用你的话", ja="あなたの発言を引用", de="Zitat von dir", es="Citándote a ti",
ru="Ваша цитата", pt="Citando você", ko="내 말을 인용", fr="Citation de vous")
add(en="Remove this quote",
zh="移除这条引用", ja="この引用を外す", de="Dieses Zitat entfernen", es="Quitar esta cita",
ru="Убрать эту цитату", pt="Remover esta citação", ko="이 인용 제거", fr="Retirer cette citation")
add(en="Go to the quoted message",
zh="跳到被引用的消息", ja="引用元のメッセージへ移動", de="Zur zitierten Nachricht springen", es="Ir al mensaje citado",
ru="Перейти к цитируемому сообщению", pt="Ir para a mensagem citada", ko="인용한 메시지로 이동", fr="Aller au message cité")
add(en="Quoted",
zh="引用", ja="引用", de="Zitiert", es="Citado",
ru="Цитата", pt="Citado", ko="인용됨", fr="Cité")
add(en="Show or hide the rest of the quote",
zh="显示或隐藏引用的其余部分", ja="引用の続きを表示または非表示", de="Rest des Zitats ein- oder ausblenden", es="Mostrar u ocultar el resto de la cita",
ru="Показать или скрыть остальную часть цитаты", pt="Mostrar ou ocultar o resto da citação", ko="인용의 나머지 부분 보기 또는 숨기기", fr="Afficher ou masquer la suite de la citation")
# #4244 the agent's own questions
add(en="Skip",
zh="跳过", ja="スキップ", de="Überspringen", es="Omitir",
ru="Пропустить", pt="Pular", ko="건너뛰기", fr="Passer")
add(en="The assistant has a question for you",
zh="助手有一个问题要问你", ja="アシスタントからの質問があります", de="Der Assistent hat eine Frage an dich", es="El asistente tiene una pregunta para ti",
ru="У ассистента есть вопрос к вам", pt="O assistente tem uma pergunta para você", ko="어시스턴트가 질문이 있습니다", fr="L'assistant a une question pour vous")
add(en="Type your own answer",
zh="输入你自己的回答", ja="自分の答えを入力", de="Eigene Antwort eingeben", es="Escribe tu propia respuesta",
ru="Введите свой ответ", pt="Digite a sua própria resposta", ko="직접 답변 입력", fr="Saisissez votre propre réponse")
add(en="Send answer",
zh="发送回答", ja="回答を送る", de="Antwort senden", es="Enviar respuesta",
ru="Отправить ответ", pt="Enviar resposta", ko="답변 보내기", fr="Envoyer la réponse")
add(en="Questions for you",
zh="向你提问", ja="あなたへの質問", de="Fragen an dich", es="Preguntas para ti",
ru="Вопросы к вам", pt="Perguntas para você", ko="내게 하는 질문", fr="Questions pour vous")
add(en="Lets the assistant stop and ask you a question, with answers to pick from, when it needs your decision.",
zh="当助手需要你做决定时,可以暂停并向你提问,并给出可选的答案。", ja="アシスタントがあなたの判断を必要とするとき、選択肢つきで質問できるようにします。",
de="Der Assistent kann anhalten und dir eine Frage mit Antworten zur Auswahl stellen, wenn er deine Entscheidung braucht.",
es="Permite que el asistente se detenga y te haga una pregunta, con respuestas para elegir, cuando necesita tu decisión.",
ru="Позволяет ассистенту остановиться и задать вам вопрос с вариантами ответа, когда ему нужно ваше решение.",
pt="Permite que o assistente pare e faça uma pergunta, com respostas para escolher, quando precisa da sua decisão.",
ko="어시스턴트가 결정이 필요할 때 멈추고 고를 수 있는 답과 함께 질문할 수 있게 합니다.",
fr="Permet à l'assistant de s'arrêter et de vous poser une question, avec des réponses à choisir, quand il a besoin de votre décision.")
here = Path(__file__).resolve().parent.parent / "src" / "locales"
NBSP = " "
def french(text):
text = re.sub(r"(?<=[A-Za-zÀ-ſ])'(?=[A-Za-zÀ-ſ{])", "’", text)
text = re.sub(r"(?<=\S) ([:;?!])", NBSP + r"\1", text)
text = re.sub(r"(?<=[\d}]) %", NBSP + "%", text)
return text
for lang in LANGS:
data = {}
for i, entry in enumerate(S):
text = entry[lang]
assert sorted(re.findall(r"\{\d+\}", entry["en"])) == sorted(re.findall(r"\{\d+\}", text)), (lang, entry["en"], text)
data[f"ui{i + 1:02d}"] = french(text) if lang == "fr" else text
(here / f"ui-{lang}.json").write_text(json.dumps(data, ensure_ascii=False, indent=1) + "\n", encoding="utf-8")
print("written", len(S), "strings in", len(LANGS), "languages")
+125
View File
@@ -0,0 +1,125 @@
"""Writes src/locales/upd-<lang>.json: the strings of the engine update beside a loaded model (the dialog of Settings > About, branch f-0291-ui-final).
Run from apps/host: python scripts/make-update-locales.py
Every entry names all nine languages; the keys are upd01.. in the order of S. upd-en.json is the English source. Placeholders ({0}) must be kept in every
language. A separate file keeps this work from colliding with the other catalogs (the same reason pick-, tune-, tools-, fix-, gh-, ui-, engine- and df2- exist)."""
import json
import re
from pathlib import Path
LANGS = ["en", "zh", "ja", "de", "es", "ru", "pt", "ko", "fr"]
S = []
def add(en, zh, ja, de, es, ru, pt, ko, fr):
S.append(dict(en=en, zh=zh, ja=ja, de=de, es=es, ru=ru, pt=pt, ko=ko, fr=fr))
add("To update the engine, Strata has to stop the model that is loaded: {0}.",
"要更新引擎,Strata 必须先停止当前已加载的模型:{0}。",
"エンジンを更新するには、読み込み中のモデルを Strata が停止する必要があります:{0}。",
"Um die Engine zu aktualisieren, muss Strata das geladene Modell anhalten: {0}.",
"Para actualizar el motor, Strata tiene que detener el modelo cargado: {0}.",
"Чтобы обновить движок, Strata должна остановить загруженную модель: {0}.",
"Para atualizar o motor, o Strata precisa parar o modelo carregado: {0}.",
"엔진을 업데이트하려면 Strata가 불러온 모델을 중지해야 합니다: {0}.",
"Pour mettre à jour le moteur, Strata doit arrêter le modèle chargé : {0}.")
add("The new engine downloads first while the model keeps answering. Then the model stops (answers in progress finish first), the engine is updated, and you can start the model again with the same settings. Your models and settings are kept.",
"新引擎会先下载,此时模型仍可继续回答。然后模型停止(正在进行的回答会先完成),引擎随即更新,之后你可以用相同的设置再次启动该模型。你的模型和设置都会保留。",
"まず新しいエンジンをダウンロードします。その間、モデルは応答を続けます。その後モデルが停止し(進行中の回答は先に完了します)、エンジンが更新され、同じ設定でモデルをもう一度起動できます。モデルと設定はそのまま残ります。",
"Zuerst wird die neue Engine heruntergeladen, während das Modell weiter antwortet. Danach hält das Modell an (laufende Antworten werden zuerst beendet), die Engine wird aktualisiert, und Sie können das Modell mit denselben Einstellungen wieder starten. Ihre Modelle und Einstellungen bleiben erhalten.",
"Primero se descarga el motor nuevo mientras el modelo sigue respondiendo. Después el modelo se detiene (las respuestas en curso terminan antes), se actualiza el motor y puedes volver a iniciar el modelo con los mismos ajustes. Tus modelos y ajustes se conservan.",
"Сначала новый движок скачивается, пока модель продолжает отвечать. Затем модель останавливается (ответы в процессе сначала завершаются), движок обновляется, и вы можете снова запустить модель с теми же настройками. Ваши модели и настройки сохраняются.",
"Primeiro o novo motor é baixado enquanto o modelo continua a responder. Depois o modelo para (as respostas em curso terminam antes), o motor é atualizado e pode iniciar o modelo de novo com as mesmas definições. Os seus modelos e definições são mantidos.",
"먼저 모델이 계속 응답하는 동안 새 엔진을 다운로드합니다. 그다음 모델이 중지되고(진행 중인 답변은 먼저 끝납니다) 엔진이 업데이트되며, 같은 설정으로 모델을 다시 시작할 수 있습니다. 모델과 설정은 그대로 유지됩니다.",
"Le nouveau moteur est d'abord téléchargé pendant que le modèle continue de répondre. Ensuite le modèle s'arrête (les réponses en cours se terminent d'abord), le moteur est mis à jour, et vous pouvez redémarrer le modèle avec les mêmes réglages. Vos modèles et réglages sont conservés.")
add("Stop and update", "停止并更新", "停止して更新", "Anhalten und aktualisieren", "Detener y actualizar", "Остановить и обновить", "Parar e atualizar", "중지하고 업데이트", "Arrêter et mettre à jour")
add("Getting the new engine ready. {0} keeps working until it is ready.",
"正在准备新引擎。在准备好之前,{0} 会继续工作。",
"新しいエンジンを準備しています。準備ができるまで {0} は動き続けます。",
"Die neue Engine wird vorbereitet. {0} arbeitet weiter, bis sie bereit ist.",
"Preparando el motor nuevo. {0} sigue funcionando hasta que esté listo.",
"Готовим новый движок. {0} продолжает работать, пока он не будет готов.",
"A preparar o novo motor. {0} continua a funcionar até estar pronto.",
"새 엔진을 준비하는 중입니다. 준비될 때까지 {0}은(는) 계속 작동합니다.",
"Préparation du nouveau moteur. {0} continue de fonctionner jusqu'à ce qu'il soit prêt.")
add("Stopping {0}. Answers in progress finish first.",
"正在停止 {0}。正在进行的回答会先完成。",
"{0} を停止しています。進行中の回答は先に完了します。",
"{0} wird angehalten. Laufende Antworten werden zuerst beendet.",
"Deteniendo {0}. Las respuestas en curso terminan antes.",
"Останавливаем {0}. Ответы в процессе сначала завершаются.",
"A parar {0}. As respostas em curso terminam antes.",
"{0}을(를) 중지하는 중입니다. 진행 중인 답변은 먼저 끝납니다.",
"Arrêt de {0}. Les réponses en cours se terminent d'abord.")
add("{0} is stopped. Updating the engine…",
"{0} 已停止。正在更新引擎…",
"{0} を停止しました。エンジンを更新しています…",
"{0} ist angehalten. Die Engine wird aktualisiert…",
"{0} está detenido. Actualizando el motor…",
"{0} остановлена. Обновляем движок…",
"{0} está parado. A atualizar o motor…",
"{0}이(가) 중지되었습니다. 엔진을 업데이트하는 중입니다…",
"{0} est arrêté. Mise à jour du moteur…")
add("Updating the engine", "正在更新引擎", "エンジンを更新しています", "Engine wird aktualisiert", "Actualizando el motor", "Обновление движка", "A atualizar o motor", "엔진 업데이트 중", "Mise à jour du moteur")
add("The engine is updated", "引擎已更新", "エンジンを更新しました", "Die Engine ist aktualisiert", "El motor está actualizado", "Движок обновлён", "O motor está atualizado", "엔진이 업데이트되었습니다", "Le moteur est à jour")
add("The engine was not updated", "引擎未更新", "エンジンは更新されませんでした", "Die Engine wurde nicht aktualisiert", "El motor no se actualizó", "Движок не обновлён", "O motor não foi atualizado", "엔진이 업데이트되지 않았습니다", "Le moteur n'a pas été mis à jour")
add("The engine is updated. Start {0} again with the same settings?",
"引擎已更新。要用相同的设置再次启动 {0} 吗?",
"エンジンを更新しました。同じ設定で {0} をもう一度起動しますか?",
"Die Engine ist aktualisiert. {0} mit denselben Einstellungen wieder starten?",
"El motor está actualizado. ¿Iniciar {0} de nuevo con los mismos ajustes?",
"Движок обновлён. Запустить {0} снова с теми же настройками?",
"O motor está atualizado. Iniciar {0} de novo com as mesmas definições?",
"엔진이 업데이트되었습니다. 같은 설정으로 {0}을(를) 다시 시작할까요?",
"Le moteur est à jour. Redémarrer {0} avec les mêmes réglages ?")
add("The update didn't finish. Your old engine is unchanged and still works. Start {0} again on it?",
"更新没有完成。你原来的引擎没有改动,仍然可用。要在它上面再次启动 {0} 吗?",
"更新は完了しませんでした。以前のエンジンは変更されておらず、そのまま使えます。そのエンジンで {0} をもう一度起動しますか?",
"Die Aktualisierung wurde nicht abgeschlossen. Ihre alte Engine ist unverändert und funktioniert weiter. {0} damit wieder starten?",
"La actualización no terminó. Tu motor anterior no cambió y sigue funcionando. ¿Iniciar {0} de nuevo con él?",
"Обновление не завершилось. Прежний движок не изменён и работает. Запустить {0} снова на нём?",
"A atualização não terminou. O seu motor anterior não foi alterado e continua a funcionar. Iniciar {0} de novo com ele?",
"업데이트가 끝나지 않았습니다. 기존 엔진은 그대로이며 계속 작동합니다. 기존 엔진으로 {0}을(를) 다시 시작할까요?",
"La mise à jour ne s'est pas terminée. Votre ancien moteur est inchangé et fonctionne toujours. Redémarrer {0} avec lui ?")
add("Not now", "暂不", "今はしない", "Jetzt nicht", "Ahora no", "Не сейчас", "Agora não", "나중에", "Pas maintenant")
add("Starting {0}. Loading can take a few minutes.",
"正在启动 {0}。加载可能需要几分钟。",
"{0} を起動しています。読み込みには数分かかることがあります。",
"{0} wird gestartet. Das Laden kann einige Minuten dauern.",
"Iniciando {0}. La carga puede tardar unos minutos.",
"Запускаем {0}. Загрузка может занять несколько минут.",
"A iniciar {0}. O carregamento pode demorar alguns minutos.",
"{0}을(를) 시작하는 중입니다. 불러오는 데 몇 분이 걸릴 수 있습니다.",
"Démarrage de {0}. Le chargement peut prendre quelques minutes.")
add("Starting {0} again", "正在再次启动 {0}", "{0} をもう一度起動しています", "{0} wird wieder gestartet", "Iniciando {0} de nuevo", "Снова запускаем {0}", "A iniciar {0} de novo", "{0}을(를) 다시 시작하는 중", "Redémarrage de {0}")
add("The model did not start again", "模型没有重新启动", "モデルを再起動できませんでした", "Das Modell wurde nicht wieder gestartet", "El modelo no se inició de nuevo", "Модель не запустилась снова", "O modelo não voltou a iniciar", "모델이 다시 시작되지 않았습니다", "Le modèle n'a pas redémarré")
add("You can load it again from Models.", "你可以在“模型”中再次加载它。", "「モデル」からもう一度読み込めます。", "Sie können es unter „Modelle“ erneut laden.", "Puedes cargarlo de nuevo desde Modelos.", "Вы можете загрузить её снова в разделе «Модели».", "Pode carregá-lo de novo em Modelos.", "모델 화면에서 다시 불러올 수 있습니다.", "Vous pouvez le recharger depuis Modèles.")
add("A model that was not started from this app is running, so nothing was changed. Stop it, then update the engine again.",
"有一个不是从此应用启动的模型正在运行,因此没有做任何更改。请先停止它,然后再次更新引擎。",
"このアプリから起動していないモデルが動作しているため、何も変更していません。それを停止してから、もう一度エンジンを更新してください。",
"Ein Modell, das nicht aus dieser App gestartet wurde, läuft noch, daher wurde nichts geändert. Halten Sie es an und aktualisieren Sie die Engine dann erneut.",
"Hay un modelo en ejecución que no se inició desde esta aplicación, así que no se cambió nada. Deténlo y vuelve a actualizar el motor.",
"Работает модель, запущенная не из этого приложения, поэтому ничего не изменено. Остановите её и обновите движок снова.",
"Está em execução um modelo que não foi iniciado a partir desta aplicação, por isso nada foi alterado. Pare-o e atualize o motor de novo.",
"이 앱에서 시작하지 않은 모델이 실행 중이어서 아무것도 변경하지 않았습니다. 그 모델을 중지한 다음 엔진을 다시 업데이트하세요.",
"Un modèle qui n'a pas été démarré depuis cette application est en cours d'exécution, rien n'a donc été modifié. Arrêtez-le, puis mettez à nouveau le moteur à jour.")
here = Path(__file__).resolve().parent.parent / "src" / "locales"
NBSP = " "
def french(text):
text = re.sub(r"(?<=[A-Za-zÀ-ſ])'(?=[A-Za-zÀ-ſ{])", "’", text)
text = re.sub(r"(?<=\S) ([:;?!])", NBSP + r"\1", text)
return text
for lang in LANGS:
data = {}
for i, entry in enumerate(S):
text = entry[lang]
assert sorted(re.findall(r"\{\d+\}", entry["en"])) == sorted(re.findall(r"\{\d+\}", text)), (lang, entry["en"], text)
data[f"upd{i + 1:02d}"] = french(text) if lang == "fr" else text
with (here / f"upd-{lang}.json").open("w", encoding="utf-8", newline="\n") as handle:
handle.write(json.dumps(data, ensure_ascii=False, indent=1) + "\n")
print("written", len(S), "strings in", len(LANGS), "languages")
+2 -4
View File
@@ -20,15 +20,13 @@ import { chromium } from 'playwright-core';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { homedir } from 'node:os';
import { requireChromium } from './browser.mjs';
const BASE = process.env.STRATA_URL ?? 'http://localhost:5199';
const MIN = 45;
const MAX = 75;
const executablePath = [
join(homedir(), 'Library/Caches/ms-playwright/chromium-1234/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing'),
join(homedir(), 'Library/Caches/ms-playwright/chromium_headless_shell-1234/chrome-headless-shell-mac-arm64/chrome-headless-shell'),
].find((p) => existsSync(p));
const executablePath = requireChromium();
if (!executablePath) {
console.error('No cached Chromium found.');
+2 -4
View File
@@ -21,6 +21,7 @@ import { promisify } from 'node:util';
import { existsSync, mkdirSync, rmSync, readdirSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { homedir } from 'node:os';
import { requireChromium } from './browser.mjs';
const run = promisify(execFile);
const BASE = process.env.STRATA_URL ?? 'http://localhost:5199';
@@ -28,10 +29,7 @@ const ROOT = resolve(process.env.STRATA_SHOTS ?? '.shots');
const TMP = join(ROOT, 'motion-frames');
mkdirSync(TMP, { recursive: true });
const executablePath = [
join(homedir(), 'Library/Caches/ms-playwright/chromium-1234/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing'),
join(homedir(), 'Library/Caches/ms-playwright/chromium_headless_shell-1234/chrome-headless-shell-mac-arm64/chrome-headless-shell'),
].find((p) => existsSync(p));
const executablePath = requireChromium();
const ffmpeg = [
join(homedir(), 'Library/Caches/ms-playwright/ffmpeg-1011/ffmpeg-mac'),
+2 -4
View File
@@ -20,14 +20,12 @@ import { chromium } from 'playwright-core';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { homedir } from 'node:os';
import { requireChromium } from './browser.mjs';
const BASE = process.env.STRATA_URL ?? 'http://localhost:5199';
const BUDGET = 1.2;
const executablePath = [
join(homedir(), 'Library/Caches/ms-playwright/chromium-1234/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing'),
join(homedir(), 'Library/Caches/ms-playwright/chromium_headless_shell-1234/chrome-headless-shell-mac-arm64/chrome-headless-shell'),
].find((p) => existsSync(p));
const executablePath = requireChromium();
if (!executablePath) {
console.error('No cached Chromium found. Run `npm run shot -- x.png <url>` once to confirm the harness.');
+7 -1
View File
@@ -5,6 +5,12 @@ import {spawn} from 'node:child_process';
import {resolve} from 'node:path';
const root=resolve(import.meta.dirname,'..');
const files=readdirSync(resolve(root,'tests')).filter(name=>name.endsWith('.test.mjs')).sort().map(name=>resolve(root,'tests',name));
const child=spawn(process.execPath,['--test',...files],{cwd:root,stdio:'inherit'});
/* Every test file runs against scratch copies of the per-user places the host writes to (the owner's real profile is never touched). */
import {mkdtempSync} from 'node:fs';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
const scratch=mkdtempSync(join(tmpdir(),'strata-host-suite-profile-'));
const env={...process.env,STRATA_SETTINGS_FILE:join(scratch,'app-settings.json'),STRATA_COMPONENTS_DIR:join(scratch,'components'),LOCALAPPDATA:join(scratch,'Local'),APPDATA:join(scratch,'Roaming'),XDG_DATA_HOME:join(scratch,'xdg-data'),XDG_STATE_HOME:join(scratch,'xdg-state'),XDG_CONFIG_HOME:join(scratch,'xdg-config')};
const child=spawn(process.execPath,['--test',...files],{cwd:root,stdio:'inherit',env});
child.on('error',error=>{console.error(error.message);process.exitCode=1;});
child.on('exit',(code,signal)=>{process.exitCode=code??(signal?1:0);});
+794
View File
@@ -0,0 +1,794 @@
#!/usr/bin/env node
/**
* Drives the built app in a headless Chrome against a real host with a clean temporary profile (no engine, no model), for the 0.2.10
* interface changes (branch f-0291-ui). Each case seeds the chat history it needs into the page's local storage, so the real screens
* draw real data without a model.
*
* npx vite build && node scripts/ui-do-drive.mjs <shots-dir> [case ...]
*
* Never point it at a real install: the host it starts uses a fresh STRATA_HOME in the temp folder and a random port.
* Light and dark, at a normal and a narrow window. Prints "ok" / "FAIL" lines and exits 1 on a failure.
*/
import { spawn, spawnSync } from 'node:child_process';
import { existsSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs';
import { createServer } from 'node:net';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
import { startScriptedWorker } from '../tests/helpers/scripted-worker.mjs';
const HOST_DIR = resolve(import.meta.dirname, '..');
const SHOTS = resolve(process.argv[2] ?? 'ui-shots');
const ONLY = process.argv.slice(3);
mkdirSync(SHOTS, { recursive: true });
const { chromium } = await import(pathToFileURL(join(HOST_DIR, 'node_modules/playwright-core/index.mjs')).href);
const CHROME = process.env.CHROME ?? ['C:/Program Files/Google/Chrome/Application/chrome.exe', '/usr/bin/google-chrome'].find(existsSync);
const freePort = () => new Promise(r => { const s = createServer(); s.listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => r(p)); }); });
const sleep = ms => new Promise(r => setTimeout(r, ms));
let failed = false;
export const check = (ok, label, extra = '') => { console.log(`${ok ? 'ok ' : 'FAIL'} ${label}${extra ? ' ' + extra : ''}`); if (!ok) failed = true; };
const home = mkdtempSync(join(tmpdir(), 'strata-ui-do-'));
const ws = join(home, 'workspace'); mkdirSync(ws, { recursive: true });
const port = await freePort();
const env = { ...process.env, STRATA_HOME: home, STRATA_PORT: String(port), STRATA_WORKSPACE: ws, STRATA_REAL_ENGINE: '0', STRATA_COMPONENTS_DIR: join(home, 'components'), STRATA_SETTINGS_FILE: join(home, 'app-settings.json'), LOCALAPPDATA: join(home, 'Local'), APPDATA: join(home, 'Roaming'), XDG_DATA_HOME: join(home, 'xdg-data'), XDG_CONFIG_HOME: join(home, 'xdg-config'), XDG_STATE_HOME: join(home, 'xdg-state') };
delete env.STRATA_HARNESS_DIR;
const child = spawn(process.execPath, [join(HOST_DIR, 'server', 'index.mjs')], { cwd: HOST_DIR, env, stdio: 'ignore', windowsHide: true });
export const base = `http://127.0.0.1:${port}`;
for (let i = 0; i < 100; i++) { try { if ((await fetch(`${base}/api/health`)).ok) break; } catch { /* starting */ } await sleep(200); }
// A saved API connection to a scripted model gives the app a model to talk to, so it stays on the chat screen (no model loaded would send it to Models).
const worker = await startScriptedWorker({ model: 'cloud-model' });
const saved = await (await fetch(`${base}/api/model-providers`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ kind: 'custom', name: 'Scripted API', baseUrl: worker.url, protocol: 'openai-chat', apiKey: 'sk-ui-do-test-0123456789', models: [{ id: 'cloud-model', name: 'Cloud model', contextMax: 64000 }] }) })).json();
const remoteModel = { connectionId: saved.connections?.[0]?.id, modelId: 'cloud-model' };
check(!!remoteModel.connectionId, 'a scripted API model is connected');
// "Go straight to the app": the first-run screens are not what these cases look at.
await fetch(`${base}/api/setup/step`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ step: 1, dismissed: true, flowVersion: 2 }) });
const browser = await chromium.launch({ executablePath: CHROME, headless: true, args: ['--no-sandbox'] });
const iso = (n = 0) => new Date(Date.UTC(2026, 9, 31, 9, 0, n)).toISOString();
export const user = (id, content, n = 0) => ({ id, role: 'user', content, at: iso(n) });
export const reply = (id, content, extra = {}) => ({ id, role: 'assistant', content, at: iso(5), tokens: 120, tps: 40, durationMs: 3000, ...extra });
export const chat = (id, title, messages) => ({ id, title, defaultTitle: false, updatedAt: iso(9), createdAt: iso(0), messages, workspace: ws, tokensIn: 0, tokensOut: 0, preferences: { useTools: true, mode: 'manual', remoteModel } });
/** A page with the given chats already in local storage. `prefs` are merged into the saved preferences. */
export async function open(scheme, width, height, sessions, { hash = '#/chat', prefs = {}, origin = base, before = null, locale = 'en-US', keepSetup = false } = {}) {
const context = await browser.newContext({ viewport: { width, height }, colorScheme: scheme, locale, permissions: ['clipboard-read', 'clipboard-write'] });
const page = await context.newPage();
pages.push(page);
const problems = [];
page.on('pageerror', e => problems.push(e.message));
page.problems = problems;
await page.addInitScript(({ sessions, prefs }) => {
try {
if (sessionStorage.getItem('ui-do-seeded')) return;
sessionStorage.setItem('ui-do-seeded', '1');
// The chats are files on the host: put the seed there (a synchronous request, so it is in place before the page starts).
for (const chat of sessions ?? []) { const request = new XMLHttpRequest(); request.open('PUT', '/api/chats/' + encodeURIComponent(chat.id), false); request.setRequestHeader('x-strata-client', 'ui'); request.send(JSON.stringify({ messages: [], ...chat })); }
if (Object.keys(prefs).length) localStorage.setItem('strata.prefs.v1', JSON.stringify(prefs));
} catch { /* storage refused */ }
}, { sessions, prefs });
if (before) await before(page);
await page.goto(`${origin}/${hash}`);
await page.waitForTimeout(1800);
const skip = page.getByText('Go straight to the app');
if (!keepSetup && await skip.count()) { await skip.click(); await page.waitForTimeout(600); }
return page;
}
export const shot = (page, name) => page.screenshot({ path: join(SHOTS, `${name}.png`) });
export const sizes = { wide: [1280, 860], narrow: [390, 800] };
export const cases = {};
const pages = [];
/* ------------------------------------------------------------------------------------------------------------------- #4725 */
const longReply = Array.from({ length: 70 }, (_, i) => `Paragraph ${i + 1}. This is part of a long answer from the assistant that goes on for many screens before it asks to change a file in the project.`).join('\n\n');
const askTool = (id, state = 'proposed', file = 'notes.txt') => ({ id, name: 'write', args: { file_path: file }, preview: `write ${file}\n\na\nb\nc`, effect: 'Creates a file.', scope: 'write', verb: 'Create a file', target: file, state, ask: { id: 'create-file', params: [file], why: 'replaces-if-exists' }, live: false });
const plainTool = (id, state = 'proposed') => ({ id, name: 'bash', args: {}, preview: 'npm install left-pad', effect: 'Runs a command on your computer.', scope: 'shell', verb: 'Run a command', target: 'npm install left-pad', state });
cases.approval = async () => {
for (const scheme of ['light', 'dark']) for (const [sizeName, [w, h]] of Object.entries(sizes)) {
const tag = `${scheme}-${sizeName}`;
const sessions = [chat('s1', 'Long reply', [user('u1', 'Please write notes.txt'), reply('a1', longReply, { tools: [askTool('t1')] })])];
const page = await open(scheme, w, h, sessions);
const dock = page.locator('.approvaldock');
await dock.waitFor({ timeout: 15000 });
const view = page.viewportSize();
const inView = async sel => { const b = await page.locator(sel).first().boundingBox(); return !!b && b.y >= 0 && b.y + b.height <= view.height + 1 && b.x >= 0 && b.x + b.width <= view.width + 1; };
check(await page.locator('.askcard').count() === 1, `${tag}: one Approve/Deny card on the page`);
check(await page.getByRole('button', { name: 'Approve' }).count() === 1 && await page.getByRole('button', { name: 'Deny' }).count() === 1, `${tag}: one Approve and one Deny`);
check(await inView('.approvaldock'), `${tag}: the card is inside the window at the end of the reply`);
const scroller = page.locator('.main__body.chat');
const atEnd = () => scroller.evaluate(el => el.scrollHeight - el.scrollTop - el.clientHeight);
check(await atEnd() < 4, `${tag}: the reply is still scrolled to its end`, String(await atEnd()));
check(await page.locator('.approval-wait').count() === 1, `${tag}: the reply says it waits for an answer`);
await shot(page, `4725-${tag}-end`);
await scroller.evaluate(el => { el.scrollTop = 0; });
await page.waitForTimeout(300);
check(await inView('.approvaldock'), `${tag}: the card stays in the window when the reader is at the top`);
const composerTop = await page.locator('.composer .st-composer').boundingBox();
const dockBox = await dock.boundingBox();
check(dockBox.y + dockBox.height <= composerTop.y, `${tag}: the card sits above the message box`);
check((await page.evaluate(() => document.documentElement.scrollWidth)) <= w, `${tag}: no sideways scroll`);
await shot(page, `4725-${tag}-top`);
await page.getByRole('button', { name: 'Approve' }).click();
await page.waitForTimeout(500);
check(await page.locator('.approvaldock').count() === 0, `${tag}: the card goes away after Approve`);
check(await page.locator('.approval-wait').count() === 0, `${tag}: and so does the waiting line`);
check(page.problems.length === 0, `${tag}: no page error`, page.problems.join('; '));
await page.context().close();
}
// Two waiting at once, one in each card style; Deny works from the dock.
const sessions = [chat('s1', 'Two waiting', [user('u1', 'Do two things'), reply('a1', longReply.slice(0, 1500), { tools: [askTool('t1'), plainTool('t2')] })])];
const page = await open('light', 1280, 860, sessions);
await page.locator('.approvaldock').waitFor({ timeout: 15000 });
check(await page.locator('.approvaldock > section').count() === 2, 'two waiting: both cards are in the dock');
await shot(page, '4725-two-waiting');
await page.getByRole('button', { name: 'Deny' }).first().click();
await page.waitForTimeout(400);
check(await page.locator('.approvaldock > section').count() === 1, 'two waiting: Deny on the first leaves the second');
await page.context().close();
};
/* ------------------------------------------------------------------------------------------------------------------- #4748 */
const codeReply = 'Here is a command.\n\n```bash\nnpm install left-pad\n```\n\nThat is all.';
cases.copy = async () => {
for (const scheme of ['light', 'dark']) for (const [sizeName, [w, h]] of Object.entries(sizes)) {
const tag = `${scheme}-${sizeName}`;
const sessions = [chat('s1', 'Copy', [user('u1', 'Show me a command'), reply('a1', codeReply), user('u2', 'Thanks'), reply('a2', 'You are welcome. Second reply.')])];
const page = await open(scheme, w, h, sessions);
await page.locator('.st-msg--assistant').last().waitFor({ timeout: 15000 });
const flag = async loc => loc.evaluate(el => ({ copied: el.hasAttribute('data-copied'), icon: el.querySelector('use')?.getAttribute('href') ?? '' }));
const read = () => page.evaluate(() => navigator.clipboard.readText());
// The last reply's copy button (an icon in the action row).
const copyResponse = page.getByRole('button', { name: 'Copy response' });
await copyResponse.last().click();
await page.mouse.move(2, 2);
await page.waitForTimeout(250);
let f = await flag(copyResponse.last());
check(f.copied && f.icon.endsWith('i-check'), `${tag}: Copy response turns into a check mark`, JSON.stringify(f));
check((await read()) === 'You are welcome. Second reply.', `${tag}: the reply is on the clipboard`);
const row = page.locator('.st-msg--assistant').last().locator('.st-msg__actions');
check(await row.evaluate(el => getComputedStyle(el).opacity) === '1', `${tag}: the action row stays visible while it says so, mouse away`);
check(/Copied/.test(await page.locator('.st-msg--assistant').last().locator('[role=status]').first().innerText()), `${tag}: a screen reader hears Copied`);
await shot(page, `4748-${tag}-reply`);
await page.waitForTimeout(2200);
f = await flag(copyResponse.last());
check(!f.copied && f.icon.endsWith('i-copy'), `${tag}: after about two seconds it is a Copy button again`, JSON.stringify(f));
// The code block.
const code = page.locator('.st-code__head').getByRole('button', { name: 'Copy code' });
await code.click();
await page.waitForTimeout(250);
f = await flag(code);
check(f.copied && f.icon.endsWith('i-check'), `${tag}: the code block's Copy turns into a check mark`, JSON.stringify(f));
check((await read()) === 'npm install left-pad', `${tag}: the code is on the clipboard`);
await shot(page, `4748-${tag}-code`);
await page.waitForTimeout(2200);
// The menu item of an earlier reply: says Copied, then the menu closes.
await page.locator('.st-msg--assistant').first().hover();
await page.locator('.st-msg--assistant').first().getByRole('button', { name: 'More response actions' }).click();
const item = page.getByRole('menuitem', { name: 'Copy response' });
await item.click();
await page.waitForTimeout(250);
check(/Copied/.test(await page.locator('.menu [data-copied]').innerText()), `${tag}: the menu item says Copied`);
check((await read()).startsWith('Here is a command'), `${tag}: the earlier reply is on the clipboard`);
await shot(page, `4748-${tag}-menu`);
await page.waitForTimeout(1300);
check(await page.locator('.menu').count() === 0, `${tag}: and the menu closes by itself`);
check(page.problems.length === 0, `${tag}: no page error`, page.problems.join('; '));
await page.context().close();
}
// A refused clipboard: nothing says Copied on the button, and the person is told in a toast.
{
const page = await open('light', 1280, 860, [chat('s1', 'Refused', [user('u1', 'Hi'), reply('a1', 'Hello there.')])]);
await page.locator('.st-msg--assistant').last().waitFor({ timeout: 15000 });
await page.evaluate(() => { Object.defineProperty(navigator, 'clipboard', { value: { writeText: () => Promise.reject(new Error('denied')) }, configurable: true }); document.execCommand = () => false; });
const b = page.getByRole('button', { name: 'Copy response' }).last();
await b.click();
await page.waitForTimeout(400);
check(await b.evaluate(el => !el.hasAttribute('data-copied')), 'refused clipboard: the button does not claim success');
check(/Couldn.t copy/.test(await page.locator('.st-toast').first().innerText()), 'refused clipboard: a toast says it did not work');
await page.context().close();
}
// The Serving screen: the address and the request template.
for (const scheme of ['light', 'dark']) {
const page = await open(scheme, 1280, 860, [chat('s1', 'Serving', [user('u1', 'Hi'), reply('a1', 'Hello there.')])], { hash: '#/serving' });
const c = page.getByRole('button', { name: 'Copy API base URL' });
await c.waitFor({ timeout: 15000 });
await c.click();
await page.waitForTimeout(250);
check(await c.evaluate(el => el.hasAttribute('data-copied') && (el.querySelector('use')?.getAttribute('href') ?? '').endsWith('i-check')), `${scheme}: Serving, Copy API base URL turns into a check mark`);
check(/^http:\/\/127\.0\.0\.1:\d+\/v1$/.test(await page.evaluate(() => navigator.clipboard.readText())), `${scheme}: Serving, the address is on the clipboard`);
await shot(page, `4748-serving-${scheme}`);
await page.getByRole('button', { name: 'Connection guide' }).click();
const t = page.getByRole('button', { name: 'Copy request template' });
await t.click();
await page.waitForTimeout(250);
check(await t.evaluate(el => el.hasAttribute('data-copied')), `${scheme}: Serving, Copy request template turns into a check mark`);
check((await page.evaluate(() => navigator.clipboard.readText())).startsWith('curl http'), `${scheme}: Serving, the template is on the clipboard`);
await page.context().close();
}
};
/* ------------------------------------------------------------------------------------------------------------------- #4804 */
let tuneHostInfo = null;
async function tuneHost() {
if (tuneHostInfo) return tuneHostInfo;
const tport = await freePort();
const proc = spawn(process.execPath, [join(HOST_DIR, 'tests/fixtures/tune-ui-host.mjs')], { cwd: HOST_DIR, env: { ...process.env, TUNE_UI_PORT: String(tport), TUNE_UPSTREAM: base, TUNE_SHOT_DIR: SHOTS }, stdio: 'ignore', windowsHide: true });
const origin = `http://127.0.0.1:${tport}`;
for (let i = 0; i < 100; i++) { try { if ((await fetch(`${origin}/api/native`)).ok) break; } catch { /* starting */ } await sleep(200); }
tuneHostInfo = { origin, proc, mode: m => fetch(`${origin}/__fixture/status-mode`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ mode: m }) }) };
return tuneHostInfo;
}
cases.tune = async () => {
const t = await tuneHost();
const sessions = [chat('s1', 'Tune', [user('u1', 'Hi'), reply('a1', 'Hello there.')])];
const openDialog = async (scheme, w, h, mode, inject = false) => {
await t.mode(mode);
const page = await open(scheme, w, h, sessions, { hash: '#/models', origin: t.origin });
if (inject) await page.route(/\/api\/native(\?.*)?$/, async route => {
if (!page.injectJob) return route.continue();
const r = await route.fetch(); const j = await r.json();
j.jobs = [...(j.jobs ?? []), { id: 'job-x', kind: 'download', state: 'running', startedAt: new Date().toISOString(), summary: 'Downloading the model', error: null, result: null, progress: { text: 'Downloading', percent: 12 } }];
return route.fulfill({ response: r, json: j });
});
const tune = page.getByRole('button', { name: 'Tune model' });
await tune.waitFor({ timeout: 20000 });
for (let i = 0; i < 40 && await tune.isDisabled(); i++) await page.waitForTimeout(250);
await tune.click();
await page.getByRole('dialog').waitFor({ timeout: 10000 });
return page;
};
const start = page => page.getByRole('button', { name: 'Start tuning' });
// The sentence must be on screen without scrolling the dialog, beside the button (above it when the window is narrow).
const seen = async (page, w, h) => {
const s = await page.locator('.tune-why').boundingBox(), b = await start(page).boundingBox();
return !!s && !!b && s.y >= 0 && s.y + s.height <= h && s.x >= 0 && s.x + s.width <= w && Math.abs((s.y + s.height / 2) - (b.y + b.height / 2)) < 110;
};
for (const scheme of ['light', 'dark']) for (const [sizeName, [w, h]] of Object.entries(sizes)) {
const tag = `${scheme}-${sizeName}`;
// 1. The saved tunings are still being read: greyed out, with the reason, and not "No saved tuning".
let page = await openDialog(scheme, w, h, 'slow');
await page.waitForTimeout(1200);
check(await start(page).isDisabled(), `${tag}: loading: Start tuning is greyed out`);
let why = await page.locator('.tune-why').innerText().catch(() => '');
check(/still checking this model for saved tunings/.test(why), `${tag}: loading: one sentence says why`, why);
check(await page.locator('.tune-why').count() === 1, `${tag}: loading: only one such sentence`);
check(!/No saved tuning for these choices yet/.test(await page.getByRole('dialog').innerText()), `${tag}: loading: it does not claim there is no saved tuning`);
check(await seen(page, w, h), `${tag}: loading: the sentence is on screen next to the button`);
await shot(page, `4804-${tag}-loading`);
await page.context().close();
// 2. Reading them failed.
page = await openDialog(scheme, w, h, 'fail');
await page.waitForTimeout(1200);
check(await start(page).isDisabled(), `${tag}: unreadable: Start tuning is greyed out`);
why = await page.locator('.tune-why').innerText().catch(() => '');
check(/could not read the tuning details/.test(why), `${tag}: unreadable: one sentence says why`, why);
check(await seen(page, w, h), `${tag}: unreadable: the sentence is on screen next to the button`);
check(!/No saved tuning/.test(await page.getByRole('dialog').innerText()), `${tag}: unreadable: it does not claim there is no saved tuning`);
await shot(page, `4804-${tag}-unreadable`);
await page.context().close();
// 3. Another operation is running.
page = await openDialog(scheme, w, h, 'ok', true);
await page.waitForTimeout(800);
check(await start(page).isEnabled() && await page.locator('.tune-why').count() === 0, `${tag}: ready: Start tuning works and shows no sentence`);
page.injectJob = true;
await page.waitForTimeout(3500);
check(await start(page).isDisabled(), `${tag}: another operation: Start tuning is greyed out`);
why = await page.locator('.tune-why').innerText().catch(() => '');
check(/Finish the current operation first/.test(why), `${tag}: another operation: one sentence says why`, why);
check((await page.evaluate(() => document.documentElement.scrollWidth)) <= w, `${tag}: no sideways scroll`);
check(await seen(page, w, h), `${tag}: other: the sentence is on screen next to the button`);
await shot(page, `4804-${tag}-other`);
check(page.problems.length === 0, `${tag}: no page error`, page.problems.join('; '));
await page.context().close();
}
};
/* ------------------------------------------------------------------------------------------------------------------- #4721 */
cases.links = async () => {
const sessions = () => [chat('s1', 'Links', [user('u1', 'Show links'), reply('a1', `Read [the docs](https://example.com/docs) or open [the logo](${base}/icons/sprite.svg).`)])];
for (const scheme of ['light', 'dark']) {
// A browser tab: the browser opens the address in a new tab; a file stays in the side panel.
let page = await open(scheme, 1280, 860, sessions());
await page.getByRole('link', { name: 'the docs' }).waitFor({ timeout: 15000 });
await page.evaluate(() => { window.__opened = []; window.open = (...a) => { window.__opened.push(a); return null; }; });
await page.getByRole('link', { name: 'the docs' }).click();
await page.waitForTimeout(300);
let opened = await page.evaluate(() => window.__opened);
check(JSON.stringify(opened) === JSON.stringify([['https://example.com/docs', '_blank', 'noopener,noreferrer']]), `${scheme}: a browser tab: a plain click on a web address opens it in a new tab`, JSON.stringify(opened));
check(await page.locator('.artifact-panel').count() === 0, `${scheme}: a browser tab: and no side panel opens`);
await page.getByRole('link', { name: 'the logo' }).click();
await page.locator('.artifact-panel').waitFor({ timeout: 8000 });
check((await page.evaluate(() => window.__opened)).length === 1, `${scheme}: a file link keeps the side panel and opens no tab`);
await shot(page, `4721-${scheme}-file`);
await page.context().close();
// The app window: the host hands the address to the default browser; window.open is not used.
const posts = [];
page = await open(scheme, 1280, 860, sessions(), { before: async p => {
await p.route('**/api/app/mode', r => r.fulfill({ json: { mode: 'window', reason: null, canQuit: true, appUpdate: 'unavailable' } }));
await p.route('**/api/artifacts/open', r => { posts.push(r.request().postDataJSON()); return r.fulfill({ json: { ok: true } }); });
} });
await page.getByRole('link', { name: 'the docs' }).waitFor({ timeout: 15000 });
await page.evaluate(() => { window.__opened = []; window.open = (...a) => { window.__opened.push(a); return null; }; });
await page.getByRole('link', { name: 'the docs' }).click();
await page.waitForTimeout(400);
check(JSON.stringify(posts) === JSON.stringify([{ action: 'open', url: 'https://example.com/docs' }]), `${scheme}: the app window: the host is asked to open it in the default browser`, JSON.stringify(posts));
check((await page.evaluate(() => window.__opened)).length === 0 && await page.locator('.artifact-panel').count() === 0, `${scheme}: the app window: nothing else happens`);
check(page.problems.length === 0, `${scheme}: no page error`, page.problems.join('; '));
await page.context().close();
}
};
/* ------------------------------------------------------------------------------------------------------------------- #4802 */
cases.progress = async () => {
const t = await tuneHost();
await t.mode('ok');
const sessions = [chat('s1', 'Progress', [user('u1', 'Hi'), reply('a1', 'Hello there.')])];
for (const scheme of ['light', 'dark']) for (const [sizeName, [w, h]] of Object.entries(sizes)) {
const tag = `${scheme}-${sizeName}`;
const page = await open(scheme, w, h, sessions, { hash: '#/models', origin: t.origin });
const started = new Date(Date.now() - (3 * 60 + 12) * 1000).toISOString();
await page.route(/\/api\/native(\?.*)?$/, async route => {
const r = await route.fetch(); const j = await r.json();
j.jobs = [...(j.jobs ?? []), { id: 'job-x', kind: 'install', model: 'iq2_xs', state: 'running', startedAt: started, summary: 'Preparing', error: null, result: { install: { step: '6/7', label: 'Preparing the model data for the engine (one time)', parts: [{ label: 'Model files', done_gb: 0.6, total_gb: 0.6, state: 'done' }], download: { file: 'model files', done_gb: 0.6, total_gb: 0.6, remaining_gb: 0, percent: 99 } } }, progress: null }];
return route.fulfill({ response: r, json: j });
});
await page.reload();
const card = page.locator('.native-progress');
await card.waitFor({ timeout: 20000 });
await page.waitForTimeout(1500);
check(await card.locator('.spinner').count() === 1, `${tag}: a spinner shows that it is working`);
const alive = () => card.locator('.native-progress__alive').innerText();
const a1 = await alive();
check(/^Still working for 3m \d+s\. You don't need to do anything\.$/.test(a1), `${tag}: it says how long and that nothing is needed`, a1);
await page.waitForTimeout(2200);
const a2 = await alive();
check(a1 !== a2, `${tag}: the time keeps counting`, `${a1} -> ${a2}`);
check(/The buttons below wait until this finishes/.test(await page.locator('.native-wait-note').innerText()), `${tag}: a sentence says why the buttons are greyed out`);
const border = await card.evaluate(el => getComputedStyle(el).borderTopColor);
const other = await page.locator('.st-card:not(.native-progress)').first().evaluate(el => getComputedStyle(el).borderTopColor);
check(border !== other, `${tag}: the card has its own border colour`, `${border} vs ${other}`);
check((await page.evaluate(() => document.documentElement.scrollWidth)) <= w, `${tag}: no sideways scroll`);
await card.scrollIntoViewIfNeeded();
await shot(page, `4802-${tag}`);
check(page.problems.length === 0, `${tag}: no page error`, page.problems.join('; '));
await page.context().close();
}
};
/* ------------------------------------------------------------------------------------------------------------------- #4798 */
cases.serving = async () => {
const sessions = [chat('s1', 'Serving', [user('u1', 'Hi'), reply('a1', 'Hello there.')])];
for (const scheme of ['light', 'dark']) for (const [sizeName, [w, h]] of Object.entries(sizes)) {
const tag = `${scheme}-${sizeName}`;
// Sharing off: one address, no extra line. Sharing on: the app's own address and the one for other devices, with one sentence.
for (const sharing of [false, true]) {
const page = await open(scheme, w, h, sessions, { hash: '#/serving', before: async p => {
if (!sharing) return;
await p.route(/\/api\/serving$/, async route => {
if (route.request().method() !== 'GET') return route.continue();
const r = await route.fetch(); const j = await r.json();
return route.fulfill({ response: r, json: { ...j, localUrl: 'http://127.0.0.1:8079/v1', baseUrl: 'http://100.76.61.73:8080/v1', sharing: { active: true, url: 'http://100.76.61.73:8080/v1' } } });
});
} });
await page.getByText('API base URL', { exact: false }).first().waitFor({ timeout: 15000 });
await page.waitForTimeout(800);
const text = await page.locator('.serving-overview').innerText();
if (sharing) {
check(/API base URL on this computer/.test(text) && text.includes('http://127.0.0.1:8079/v1'), `${tag}: sharing on: the app's own address is shown`);
check(/Address for other devices/.test(text) && text.includes('http://100.76.61.73:8080/v1'), `${tag}: sharing on: and the address for other devices`);
check(await page.locator('.serving-two-addresses').count() === 1 && /its own port/.test(await page.locator('.serving-two-addresses').innerText()), `${tag}: sharing on: one sentence says why they differ`);
const t = page.getByRole('button', { name: 'Copy local network URL' });
await t.click(); await page.waitForTimeout(250);
check((await page.evaluate(() => navigator.clipboard.readText())) === 'http://100.76.61.73:8080/v1', `${tag}: sharing on: the copy button gives the network address`);
check((await page.evaluate(() => document.documentElement.scrollWidth)) <= w, `${tag}: sharing on: no sideways scroll`);
await page.locator('.serving-overview').scrollIntoViewIfNeeded();
await shot(page, `4798-${tag}-sharing`);
} else {
check(!/Address for other devices/.test(text) && await page.locator('.serving-two-addresses').count() === 0, `${tag}: sharing off: one address, no extra line`);
}
check(page.problems.length === 0, `${tag}: no page error`, page.problems.join('; '));
await page.context().close();
}
}
};
/* ------------------------------------------------------------------------------------------------------------------- #4722 */
cases.width = async () => {
const text = '用中文写一段九十字左右的段落,说明本地模型为什么适合处理私人文件,并且不需要把任何内容上传到网络上去,这样可以让人放心。\n\n- 第一条要点同样写得比较长,用来观察列表项在气泡里到底会排到多宽才换行,看看它是否与上面的段落一样。\n- 第二条要点也一样长,看看两条要点是不是排得整齐,不会比段落更宽也不会更窄。\n\nAnd an English paragraph that is long enough to wrap onto at least two lines so that the measure of the prose can be read off it by the test.\n\n1. A numbered point in English that is also long enough to wrap onto a second line in the same bubble as the paragraph above it.\n2. Another numbered point.\n\n| Name | Value |\n| --- | --- |\n| a long cell that is wide | another long cell that is wide too |\n\n```bash\nnpm install a-very-long-package-name another-very-long-package-name yet-another-long-package-name --save-dev --legacy-peer-deps\n```';
for (const scheme of ['light', 'dark']) for (const [sizeName, [w, h]] of Object.entries(sizes)) {
const tag = `${scheme}-${sizeName}`;
const page = await open(scheme, w, h, [chat('s1', 'Width', [user('u1', 'Write'), reply('a1', text)])]);
await page.locator('.st-bubble ul').first().waitFor({ timeout: 15000 });
const m = await page.evaluate(() => {
const b = document.querySelector('.st-msg--assistant .st-bubble'), r = el => Math.round(el.getBoundingClientRect().width);
return { bubble: r(b), p: r(b.querySelector('p')), ul: r(b.querySelector('ul')), ol: r(b.querySelector('ol')), table: r(b.querySelector('.md-table-wrap')), code: r(b.querySelector('.st-code')) };
});
const note = JSON.stringify(m);
if (w > 700) {
check(Math.abs(m.ul - m.p) <= 2 && Math.abs(m.ol - m.p) <= 2, `${tag}: bullets and numbers have the paragraph's width`, note);
check(m.table > m.p + 20 && m.code > m.p + 20, `${tag}: the table and the code block keep the full width`, note);
} else {
check(Math.abs(m.ul - m.p) <= 2 && m.p <= m.bubble, `${tag}: narrow window: everything fits the column`, note);
}
check((await page.evaluate(() => document.documentElement.scrollWidth)) <= w, `${tag}: no sideways scroll`);
await shot(page, `4722-${tag}`);
await page.context().close();
}
};
/* ------------------------------------------------------------------------------------------------------------------- #3723 */
cases.firstrun = async () => {
const setupStep = dismissed => fetch(`${base}/api/setup/step`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ step: 1, dismissed, flowVersion: 2 }) });
await setupStep(false);
// A computer set to a language Strata does not have: English.
const page = await open('light', 1280, 860, null, { hash: '#/setup', locale: 'sv-SE', keepSetup: true });
await page.locator('.onboard-language').waitFor({ timeout: 20000 });
check(/English/.test(await page.locator('.onboard-language').innerText()), 'Swedish computer: English');
await page.context().close();
// A first start (nothing saved) on a computer set to Chinese opens in Chinese, with a language choice on the first screen.
for (const scheme of ['light', 'dark']) for (const [sizeName, [w, h]] of Object.entries(sizes)) {
const tag = `${scheme}-${sizeName}`;
const page = await open(scheme, w, h, null, { hash: '#/setup', locale: 'zh-CN', keepSetup: true });
await page.locator('.onboard-language').waitFor({ timeout: 20000 });
await page.waitForTimeout(500);
const pick = page.locator('.onboard-language [role=combobox], .onboard-language button').first();
const shown = await pick.innerText();
check(/中文/.test(shown), `${tag}: a computer set to Chinese starts in Chinese`, shown);
check(/[\u4e00-\u9fff]/.test(await page.locator('.guided-onboarding h1').first().innerText()), `${tag}: the first screen is in Chinese`);
const box = await page.locator('.onboard-language').boundingBox();
check(!!box && box.x >= 0 && box.x + box.width <= w && box.y >= 0, `${tag}: the choice is on screen`, JSON.stringify(box));
const brand = await page.locator('.guided-onboarding .onboard-header .brand').first().boundingBox();
check(!brand || box.x >= brand.x + brand.width - 1 || box.x + box.width <= brand.x + 1 || box.y >= brand.y + brand.height || box.y + box.height <= brand.y, `${tag}: the choice does not cover the logo`, JSON.stringify({ brand, box }));
await shot(page, `3723-${tag}-first-zh`);
// Choosing German changes the screen at once, and it is kept.
await pick.click();
await page.getByRole('option', { name: 'Deutsch' }).click();
await page.waitForTimeout(500);
check(/[A-Za-zäöü]/.test(await page.locator('.guided-onboarding h1').first().innerText()) && !/[\u4e00-\u9fff]/.test(await page.locator('.guided-onboarding h1').first().innerText()), `${tag}: choosing Deutsch changes the first screen`);
await shot(page, `3723-${tag}-first-de`);
await page.reload();
await page.locator('.onboard-language').waitFor({ timeout: 20000 });
check(/Deutsch/.test(await page.locator('.onboard-language').innerText()), `${tag}: the choice is kept after a restart, whatever the computer says`);
check(page.problems.length === 0, `${tag}: no page error`, page.problems.join('; '));
await page.context().close();
}
await setupStep(true);
};
cases.kv = async () => {
const t = await tuneHost();
await t.mode('ok');
const sessions = [chat('s1', 'KV', [user('u1', 'Hi'), reply('a1', 'Hello there.')])];
for (const scheme of ['light', 'dark']) for (const [sizeName, [w, h]] of Object.entries(sizes)) {
const tag = `${scheme}-${sizeName}`;
const page = await open(scheme, w, h, sessions, { hash: '#/models', origin: t.origin });
await page.route(/\/api\/native(\?.*)?$/, async route => {
const r = await route.fetch(); const j = await r.json();
j.jobs = [...(j.jobs ?? []), { id: 'job-f', kind: 'load', model: 'iq2_xs', state: 'error', startedAt: new Date().toISOString(), summary: 'Load failed', error: "server_exited (exit code 1): EngineDied: the engine exited before it was ready | the engine process ended: exit code 1: \nthe engine log's last lines:\n strata generate: expert cache auto: 1.92 GiB free, 2048 MiB reserved -> 0 slots\n strata generate: no VRAM is left for the expert cache: it needs at least 332 slots (737 MiB), about 1000 MiB more than this card has free.", result: null, progress: null }];
return route.fulfill({ response: r, json: j });
});
await page.reload();
const expert = page.getByRole('button', { name: 'Expert mode' });
await expert.waitFor({ timeout: 20000 });
await page.waitForTimeout(1500);
await expert.click();
const sel = page.getByRole('combobox', { name: 'Native KV format' });
if (!await sel.isVisible().catch(() => false)) { await page.waitForTimeout(800); if (!await sel.isVisible().catch(() => false)) await page.getByRole('button', { name: /expert mode/i }).first().click(); }
await sel.waitFor({ timeout: 10000 });
const hint = page.locator('.native-kv-hint');
check(/INT8 is the standard/.test(await hint.innerText()), `${tag}: Context memory: one line says what the chosen format is`);
await sel.click();
check(await page.getByRole('option').count() === 3, `${tag}: Context memory: three options`);
const titles = await page.locator('.select__option-label').evaluateAll(els => els.map(e => e.getAttribute('title')));
check(titles.length === 3 && titles.every(x => x && x.length > 20), `${tag}: Context memory: each option carries its line`, JSON.stringify(titles));
await page.getByRole('option', { name: 'Q4' }).click();
await page.waitForTimeout(300);
check(/Q4 uses about half the memory/.test(await hint.innerText()), `${tag}: Context memory: the line follows the choice`);
await hint.scrollIntoViewIfNeeded();
await shot(page, `3723-${tag}-kv`);
const notice = page.locator('.error-notice');
await notice.first().waitFor({ timeout: 10000 });
const said = await notice.first().innerText();
check(/did not fit in your graphics card.s memory/.test(said) && /Context memory to Q4/.test(said), `${tag}: a start that ran out of graphics memory says so in plain words`, said.replace(/\n+/g, ' | ').slice(0, 200));
await notice.first().scrollIntoViewIfNeeded();
await shot(page, `3723-${tag}-vram`);
check((await page.evaluate(() => document.documentElement.scrollWidth)) <= w, `${tag}: no sideways scroll`);
check(page.problems.length === 0, `${tag}: no page error`, page.problems.join('; '));
await page.context().close();
}
};
/* ------------------------------------------------------------------------------------------------------------------- #4749 */
cases.helper = async () => {
const agents = [
{ id: 'child-1', name: 'Check the tests', description: 'All 12 tests pass.', status: 'completed', durationMs: 42000, tokens: 1800, updatedAt: Date.now() },
{ id: 'child-2', name: 'Write the notes', description: 'Could not write notes.txt', status: 'failed', durationMs: 9000, tokens: 400, updatedAt: Date.now() },
];
for (const scheme of ['light', 'dark']) for (const [sizeName, [w, h]] of Object.entries(sizes)) {
const tag = `${scheme}-${sizeName}`;
const sessions = [{ ...chat('s1', 'Helpers', [user('u1', 'Do two things'), reply('a1', 'Both helpers are on it.')]), agents }];
const posts = []; let refuse = false;
const page = await open(scheme, w, h, sessions, { before: async p => {
await p.route('**/api/chat/helper', async r => { posts.push(r.request().postDataJSON()); if (refuse) return r.fulfill({ status: 409, json: { error: 'This helper cannot take a message any more. Ask the assistant in the chat to start a new one.' } }); return r.fulfill({ json: { ok: true, messageId: 'm1' } }); });
} });
await page.getByRole('button', { name: /Subagents/ }).first().click();
const rows = page.locator('.subagent-row');
await rows.first().waitFor({ timeout: 15000 });
// The finished helper: queue.
await rows.nth(0).getByRole('button', { name: 'Message this helper' }).click();
const input = rows.nth(0).getByRole('textbox', { name: 'Message to Check the tests' });
await input.fill('use ISO dates in the file you just wrote');
await shot(page, `4749-${tag}-writing`);
await input.press('Enter');
await rows.nth(0).getByRole('status').waitFor({ timeout: 5000 });
check(JSON.stringify(posts[0]) === JSON.stringify({ sessionId: 's1', helperId: 'child-1', message: 'use ISO dates in the file you just wrote', delivery: 'queue' }), `${tag}: a finished helper gets the message as a new turn (queue)`, JSON.stringify(posts[0]));
check(/Sent to Check the tests\./.test(await rows.nth(0).innerText()) && await input.inputValue() === '', `${tag}: the row says it was sent and the box is empty again`);
// A second helper (a failed one is listed too).
await rows.nth(1).getByRole('button', { name: 'Message this helper' }).click();
await rows.nth(1).getByRole('textbox').fill('also add the date');
await rows.nth(1).getByRole('button', { name: 'Send' }).click();
await rows.nth(1).getByRole('status').waitFor({ timeout: 5000 });
check(posts[1]?.delivery === 'queue' && posts[1]?.helperId === 'child-2', `${tag}: the message goes to the helper that was chosen`, JSON.stringify(posts[1]));
// A refusal says so in plain words.
refuse = true;
await rows.nth(0).getByRole('textbox').fill('one more thing');
await rows.nth(0).getByRole('button', { name: 'Send' }).click();
await rows.nth(0).getByRole('alert').waitFor({ timeout: 5000 });
check(/cannot take a message any more/.test(await rows.nth(0).getByRole('alert').innerText()), `${tag}: a refused message says why in plain words`);
check((await page.evaluate(() => document.documentElement.scrollWidth)) <= w, `${tag}: no sideways scroll`);
await shot(page, `4749-${tag}-sent`);
check(page.problems.length === 0, `${tag}: no page error`, page.problems.join('; '));
await page.context().close();
}
};
/* ------------------------------------------------------------------------------------------------------------------- #4783 */
cases.busy = async () => {
const fresh = (scheme, w, h, whileBusy) => open(scheme, w, h, [chat('s1', 'Busy', [])], { prefs: whileBusy ? { whileBusy } : {} });
const send = async (page, text) => { await page.locator('textarea').first().fill(text); await page.keyboard.press('Enter'); };
const lastReply = page => page.locator('.st-msg--assistant .st-bubble').last();
for (const scheme of ['light', 'dark']) {
const w = 1280, h = 860, tag = scheme;
// Wait (the default): Enter while it answers sends nothing, the message stays in the box, and a note says why.
let page = await fresh(scheme, w, h, null);
await send(page, 'SCRIPT:slow 80');
await page.locator('.st-msg--assistant').first().waitFor({ timeout: 15000 });
await page.locator('textarea').first().fill('my second message');
await page.keyboard.press('Enter');
await page.waitForTimeout(600);
check(await page.locator('textarea').first().inputValue() === 'my second message', `${tag}: Wait: the message stays in the box`);
check(await page.locator('.parked').count() === 0, `${tag}: Wait: nothing is parked`);
await page.getByText('The assistant is still answering').first().waitFor({ timeout: 4000 }).catch(() => {});
check(await page.getByText('The assistant is still answering').count() > 0, `${tag}: Wait: a note says the assistant is still answering`);
await page.context().close();
// Send after: it waits (shown), and goes when the answer is done, after the whole first answer.
page = await fresh(scheme, w, h, 'queue');
await send(page, 'SCRIPT:slow 80');
await page.locator('.st-msg--assistant').first().waitFor({ timeout: 15000 });
await page.locator('textarea').first().fill('SCRIPT:echo the second answer');
check(/Enter sends it when the answer is done/.test(await page.locator('.composer__busy-hint').innerText()), `${tag}: Send after: the hint says what Enter will do`);
await page.keyboard.press('Enter');
await page.locator('.parked').waitFor({ timeout: 3000 });
check(/Waiting to send/.test(await page.locator('.parked').innerText()) && /second answer/.test(await page.locator('.parked').innerText()), `${tag}: Send after: the waiting message is shown`);
if (scheme === 'light') await shot(page, `4783-${tag}-waiting`);
await page.locator('.st-msg--assistant').nth(1).waitFor({ timeout: 30000 });
await page.waitForFunction(() => document.querySelectorAll('.parked').length === 0, null, { timeout: 15000 });
const firstText = await page.locator('.st-msg--assistant .st-bubble').first().innerText();
check(/w79/.test(firstText), `${tag}: Send after: the first answer was written to its end`, firstText.slice(-30));
check(/the second answer/.test(await lastReply(page).innerText()), `${tag}: Send after: then the second message was answered`);
await page.context().close();
// Interrupt: the answer stops where it is, what is written stays, and the message goes at once.
page = await fresh(scheme, w, h, 'interject');
await send(page, 'SCRIPT:slow 600');
await page.locator('.st-msg--assistant').first().waitFor({ timeout: 15000 });
await page.waitForTimeout(800);
await page.locator('textarea').first().fill('SCRIPT:echo interrupted and answered');
check(/Enter stops the answer here and sends it now/.test(await page.locator('.composer__busy-hint').innerText()), `${tag}: Interrupt: the hint says what Enter will do`);
await page.keyboard.press('Enter');
const t0 = Date.now();
await page.locator('.st-msg--assistant').nth(1).waitFor({ timeout: 30000 });
check(Date.now() - t0 < 12000, `${tag}: Interrupt: the new message was answered long before the first answer would have ended`, `${Date.now() - t0} ms`);
const cut = await page.locator('.st-msg--assistant .st-bubble').first().innerText();
check(cut.includes('w0') && !cut.includes('w599'), `${tag}: Interrupt: the first answer stopped where it was and stays`, `${cut.length} characters`);
await page.waitForFunction(() => /interrupted and answered/.test([...document.querySelectorAll('.st-msg--assistant .st-bubble')].at(-1)?.innerText ?? ''), null, { timeout: 15000 }).catch(() => {});
check(/interrupted and answered/.test(await lastReply(page).innerText()), `${tag}: Interrupt: the second message was answered`);
if (scheme === 'light') await shot(page, `4783-${tag}-interrupted`);
check(page.problems.length === 0, `${tag}: no page error`, page.problems.join('; '));
await page.context().close();
}
};
/* ------------------------------------------------------------------------------------------------------------------- #4785 */
cases.quote = async () => {
const long = 'First point: local models keep your files on your own computer. Second point: nothing has to be uploaded to a service. Third point: you can work without a network connection.';
for (const scheme of ['light', 'dark']) for (const [sizeName, [w, h]] of Object.entries(sizes)) {
const tag = `${scheme}-${sizeName}`;
const page = await open(scheme, w, h, [chat('s1', 'Quote', [user('u1', 'Why run a model locally?'), reply('a1', long), user('u2', 'Thanks, one more question'), reply('a2', 'Ask away.')])]);
await page.locator('.st-msg--assistant').first().waitFor({ timeout: 15000 });
// A whole turn, from the message menu.
await page.locator('.st-msg--assistant').first().hover();
await page.locator('.st-msg--assistant').first().getByRole('button', { name: 'More response actions' }).click();
await page.getByRole('menuitem', { name: 'Quote' }).click();
await page.locator('.quote').first().waitFor({ timeout: 5000 });
check(/Quoting the assistant/.test(await page.locator('.quote').first().innerText()) && /First point/.test(await page.locator('.quote').first().innerText()), `${tag}: Quote in the message menu puts the turn above the box`);
// Just a passage: select the second sentence and quote it.
await page.evaluate(() => {
const p = document.querySelector('.st-msg--assistant .st-bubble p');
const node = p.firstChild; const at = node.textContent.indexOf('Second point'); const end = node.textContent.indexOf('Third point');
const r = document.createRange(); r.setStart(node, at); r.setEnd(node, end);
const s = window.getSelection(); s.removeAllRanges(); s.addRange(r);
});
await page.locator('.quote-selection').waitFor({ timeout: 5000 });
const sbox = await page.locator('.quote-selection').boundingBox();
check(!!sbox && sbox.x >= 0 && sbox.y >= 0 && sbox.x + sbox.width <= w, `${tag}: a Quote button appears by the selected words, on screen`, JSON.stringify(sbox));
await page.locator('.quote-selection').click();
check(await page.locator('.quote').count() === 2 && /^Second point: nothing has to be uploaded to a service\.$/.test((await page.locator('.quote .quote__text').nth(1).innerText()).trim()), `${tag}: the second quote is just the selected passage, listed after the first`);
check(await page.locator('.quote-selection').count() === 0, `${tag}: the button goes away`);
await shot(page, `4785-${tag}-quotes`);
// Remove one, quote it again, jump to the message.
await page.locator('.quote').first().getByRole('button', { name: 'Remove this quote' }).click();
check(await page.locator('.quote').count() === 1 && /Second point/.test(await page.locator('.quote').first().innerText()), `${tag}: one quote can be removed without touching the others`);
await page.locator('.quote__body').first().click();
await page.waitForTimeout(400);
// Send an answer: the model gets the fixed form, the transcript shows the quote apart from the reply.
await page.locator('textarea').first().fill('SCRIPT:echo thanks for the quote');
await page.keyboard.press('Enter');
await page.locator('.sent-quote').first().waitFor({ timeout: 15000 });
await page.waitForTimeout(300);
const sent = await page.locator('.st-msg--user').last().innerText();
check(/Second point: nothing has to be uploaded/.test(sent) && /SCRIPT:echo thanks for the quote/.test(sent) && !/Quoting:/.test(sent) && !/「/.test(sent), `${tag}: the sent message shows the quote apart from the reply, without the markup`, sent.replace(/\n+/g, ' | ').slice(0, 160));
const got = worker.requests.at(-1)?.raw ?? '';
check(got.includes('Quoting:\\n「Second point: nothing has to be uploaded to a service.」\\n→ Reply: SCRIPT:echo thanks for the quote'), `${tag}: the model receives the quote in its fixed form`, got.slice(-220));
check(await page.locator('.quote').count() === 0, `${tag}: the quotes are cleared after sending`);
await page.locator('.sent-quote').first().click();
check(await page.locator('.sent-quotes[data-open]').count() === 1, `${tag}: a click opens the rest of a sent quote`);
await shot(page, `4785-${tag}-sent`);
check((await page.evaluate(() => document.documentElement.scrollWidth)) <= w, `${tag}: no sideways scroll`);
check(page.problems.length === 0, `${tag}: no page error`, page.problems.join('; '));
await page.context().close();
}
};
/* ------------------------------------------------------------------------------------------------------------------- #4244 */
cases.ask = async () => {
const t = await tuneHost();
await t.mode('ok');
const asked = () => fetch(`${t.origin}/__fixture/asked`).then(r => r.json());
await fetch(`${t.origin}/__fixture/ask`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ on: true }) });
for (const scheme of ['light', 'dark']) for (const [sizeName, [w, h]] of Object.entries(sizes)) {
const tag = `${scheme}-${sizeName}`;
const before = (await asked()).length;
const page = await open(scheme, w, h, [chat('s1', 'Ask', [])], { origin: t.origin });
await page.locator('textarea').first().fill('Build me a page');
await page.keyboard.press('Enter');
const card = page.locator('.askq');
await card.waitFor({ timeout: 15000 });
const text = await card.innerText();
check(/Which colour should the page be\?/.test(text) && /Which extras do you want\?/.test(text) && /Warm and strong/.test(text) && /It sets the whole theme/.test(text), `${tag}: the question, its detail and the option descriptions are shown`);
check(await page.locator('.askcard').count() === 1 && await page.locator('.approvaldock').count() === 1, `${tag}: it is one card in the dock above the box`);
const send = page.getByRole('button', { name: 'Send answer' });
check(await send.isDisabled(), `${tag}: Send answer waits until every question has an answer`);
await page.getByRole('radio', { name: /Red/ }).click();
await page.getByRole('radio', { name: /Blue/ }).click();
check(await page.getByRole('radio', { name: /Red/ }).getAttribute('aria-checked') === 'false' && await page.getByRole('radio', { name: /Blue/ }).getAttribute('aria-checked') === 'true', `${tag}: a single-select question takes one choice`);
await page.getByRole('checkbox', { name: 'Dark mode' }).click();
await page.getByRole('checkbox', { name: 'Animations' }).click();
check(await send.isEnabled(), `${tag}: Send answer works once every question has an answer`);
await page.waitForTimeout(300);
const sb = await send.boundingBox();
const top = await page.evaluate(({ x, y }) => { const e = document.elementFromPoint(x, y); return e?.closest('button')?.textContent ?? ''; }, { x: sb.x + sb.width / 2, y: sb.y + sb.height / 2 });
check(sb.y >= 0 && sb.y + sb.height <= h && /Send answer/.test(top), `${tag}: the answer buttons are in view and reachable without scrolling the card`, JSON.stringify(sb));
await shot(page, `4244-${tag}`);
check((await page.evaluate(() => document.documentElement.scrollWidth)) <= w, `${tag}: no sideways scroll`);
const box = await page.locator('.approvaldock').boundingBox();
check(!!box && box.y >= 0 && box.y + box.height <= h + 1, `${tag}: the dock with the card is inside the window`, JSON.stringify(box));
await send.click();
await page.waitForFunction(() => document.querySelectorAll('.askq').length === 0, null, { timeout: 8000 });
const got = (await asked()).slice(before);
check(JSON.stringify(got.at(-1)) === JSON.stringify({ sessionId: 's1', callId: 'call-ask-1', answers: [{ id: 'q1', selected: ['Blue'] }, { id: 'q2', selected: ['Dark mode', 'Animations'] }] }), `${tag}: the answer goes to the host as chosen`, JSON.stringify(got.at(-1)));
check(await page.locator('.askq').count() === 0, `${tag}: the card goes away, and the turn ends`);
// The person's own words, and a skip.
await page.locator('textarea').first().fill('Again');
await page.keyboard.press('Enter');
await card.waitFor({ timeout: 15000 });
await page.getByRole('textbox', { name: 'Type your own answer' }).first().fill('Green, like the logo');
await page.getByRole('checkbox', { name: 'Animations' }).click();
await send.click();
await page.waitForFunction(() => document.querySelectorAll('.askq').length === 0, null, { timeout: 8000 });
const own = (await asked()).slice(before).at(-1);
check(JSON.stringify(own?.answers) === JSON.stringify([{ id: 'q1', selected: [], custom: 'Green, like the logo' }, { id: 'q2', selected: ['Animations'] }]), `${tag}: the person's own words are sent as such`, JSON.stringify(own));
await page.locator('textarea').first().fill('Once more');
await page.keyboard.press('Enter');
await card.waitFor({ timeout: 15000 });
await page.getByRole('button', { name: 'Skip' }).click();
await page.waitForFunction(() => document.querySelectorAll('.askq').length === 0, null, { timeout: 8000 });
check((await asked()).slice(before).at(-1)?.skip === true, `${tag}: Skip is sent as a skip`);
check(page.problems.length === 0, `${tag}: no page error`, page.problems.join('; '));
await page.context().close();
}
// A /loop runs without anyone there: the question is skipped at once, no card, and the loop is not left waiting.
{
const before = (await asked()).length;
const page = await open('light', 1280, 860, [chat('s1', 'Loop', [])], { origin: t.origin });
await page.locator('textarea').first().fill('/loop 10m Build me a page stop after 1 answer');
await page.keyboard.press('Enter');
await page.waitForFunction(async () => (await (await fetch('/__fixture/asked')).json()).length > 0, null, { timeout: 15000 }).catch(() => {});
const got = (await asked()).slice(before).at(-1);
check(got?.skip === true && got?.callId === 'call-ask-1', 'a loop: the question is skipped at once', JSON.stringify(got));
check(await page.locator('.askq').count() === 0, 'a loop: no question card is shown');
await page.context().close();
}
await fetch(`${t.origin}/__fixture/ask`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ on: false }) });
};
/* ------------------------------------------------------------------------------------------------------------------- #3818 (a probe, no fix) */
cases.select = async () => {
const paras = Array.from({ length: 40 }, (_, i) => `Paragraph ${i + 1}. This is a sentence of a long answer that goes on for a while so that the reply overflows the window.`).join('\n\n');
const pick = async (page, fromText, toText) => {
const a = await page.getByText(fromText, { exact: false }).first().boundingBox(), b = await page.getByText(toText, { exact: false }).first().boundingBox();
await page.mouse.move(a.x + 2, a.y + a.height / 2);
await page.mouse.down();
await page.mouse.move(b.x + b.width - 4, b.y + b.height / 2, { steps: 12 });
await page.mouse.up();
return page.evaluate(() => window.getSelection().toString());
};
const page = await open('light', 1280, 860, [chat('s1', 'Select', [user('u1', 'Write a lot'), reply('a1', paras)])]);
await page.locator('.st-bubble p').first().waitFor({ timeout: 15000 });
await page.locator('.main__body.chat').evaluate(el => { el.scrollTop = el.scrollHeight; });
await page.waitForTimeout(400);
// After the reply has finished: a drag across several paragraphs.
const visible = await page.evaluate(() => [...document.querySelectorAll('.st-bubble p')].filter(p => { const r = p.getBoundingClientRect(); return r.top > 100 && r.bottom < innerHeight - 220; }).map(p => p.textContent.slice(0, 12)));
const first = visible[0], last = visible.at(-1);
const got = await pick(page, first, last);
check(got.includes(first) && got.includes(last) && got.split('\n').filter(Boolean).length >= visible.length, `finished reply: a drag across ${visible.length} paragraphs selects all of them`, `${got.length} characters`);
await page.context().close();
// While a reply streams: select a passage of the older reply, and see whether the selection and the view stay.
const page2 = await open('light', 1280, 860, [chat('s1', 'Select', [user('u1', 'Write a lot'), reply('a1', paras)])]);
await page2.locator('.st-bubble p').first().waitFor({ timeout: 15000 });
await page2.locator('textarea').first().fill('SCRIPT:slow 400');
await page2.keyboard.press('Enter');
await page2.waitForTimeout(1500);
const v2 = await page2.evaluate(() => [...document.querySelector('.st-msg--assistant').querySelectorAll('.st-bubble p')].filter(p => { const r = p.getBoundingClientRect(); return r.top > 100 && r.bottom < innerHeight - 220; }).map(p => p.textContent.slice(0, 12)));
if (v2.length >= 2) {
// The selection is made in code here (a drag moves with the layout and made this probe flaky): words of the older reply, as a drag would select them.
const sel0 = await page2.evaluate(([a, b]) => {
const ps = [...document.querySelector('.st-msg--assistant').querySelectorAll('.st-bubble p')], from = ps.find(p => p.textContent.startsWith(a)), to = ps.find(p => p.textContent.startsWith(b));
const r = document.createRange(); r.setStart(from.firstChild, 0); r.setEnd(to.lastChild, to.lastChild.textContent.length);
const s = window.getSelection(); s.removeAllRanges(); s.addRange(r); return s.toString();
}, [v2[0], v2.at(-1)]);
const top0 = await page2.locator('.main__body.chat').evaluate(el => el.scrollTop);
await page2.waitForTimeout(3000);
const sel1 = await page2.evaluate(() => window.getSelection().toString());
const top1 = await page2.locator('.main__body.chat').evaluate(el => el.scrollTop);
console.log(`INFO streaming: selection ${sel0.length} -> ${sel1.length} characters, scrollTop ${Math.round(top0)} -> ${Math.round(top1)}`);
console.log(`INFO before: ${JSON.stringify(sel0.slice(0, 40))} ... ${JSON.stringify(sel0.slice(-40))}
INFO after: ${JSON.stringify(sel1.slice(0, 40))} ... ${JSON.stringify(sel1.slice(-40))}`);
check(sel1 === sel0, 'streaming: a selection in the older reply stays as it was while the new reply is written (a selection inside the paragraph that is being written does not: React replaces its text node on every token)', `${sel0.length} -> ${sel1.length}`);
check(Math.abs(top1 - top0) < 40, 'streaming: the view is not pulled away from the selection', `${Math.round(top0)} -> ${Math.round(top1)}`);
} else console.log('INFO streaming: the older reply was not on screen, nothing to select');
await page2.context().close();
};
/* ------------------------------------------------------------------------------------------------------------------ run */
let ran = 0;
for (const [name, fn] of Object.entries(cases)) {
if (ONLY.length && !ONLY.includes(name)) continue;
console.log(`--- ${name}`);
ran += 1;
try { await fn(); } catch (e) {
check(false, `${name} threw`, String(e?.stack ?? e).split('\n').slice(0, 4).join(' | '));
const last = pages.at(-1);
if (last && !last.isClosed()) {
await last.screenshot({ path: join(SHOTS, `debug-${name}.png`) }).catch(() => {});
console.log('PAGE TEXT:', ((await last.evaluate(() => document.body.innerText).catch(() => '')) || '').slice(0, 600).replace(/\n+/g, ' | '));
console.log('PAGE ERRORS:', last.problems?.join('; '));
}
}
}
check(ran > 0, 'at least one case ran');
await browser.close();
await worker.close();
for (const proc of [child, tuneHostInfo?.proc]) if (proc) { if (process.platform === 'win32') spawnSync('taskkill', ['/PID', String(proc.pid), '/T', '/F'], { windowsHide: true }); else proc.kill('SIGTERM'); }
try { rmSync(home, { recursive: true, force: true, maxRetries: 3 }); } catch { /* best effort */ }
process.exit(failed ? 1 : 0);
+31 -10
View File
@@ -1,5 +1,8 @@
# Agent bridge (Strata chats as DeepSeek Harness sessions)
This page is for developers who work on the app host's agent code. It lists the files, the frames and routes, and the rules the
bridge enforces. Tests are listed at the end.
The Harness is a separate, pinned component (`DSH 0.2.1-alpha.3`, commit `57190202`, MIT). The host finds it through
`STRATA_HARNESS_DIR` (or `<STRATA_COMPONENTS_DIR>/harness`), starts one `dsh --profile web` process on a loopback port,
and talks to its Gateway. Nothing here downloads or builds anything.
@@ -14,12 +17,13 @@ and talks to its Gateway. Nothing here downloads or builds anything.
| `children.mjs` | pure projection of one helper agent's events into its row and its approval card |
| `preset.mjs` | the agent preset (which tools the chat has) |
| `plugins/strata-gate.mjs` | the Manual-mode approval gate (a Harness plugin, loaded by path) |
| `plugins/strata-search.mjs`, `plugins/strata-models.mjs` | the app's web search registered with the Harness; helper agents on a named model |
| `plugins/strata-context.mjs`, `context-budget.mjs`, `context-fit.mjs` | measured context pressure, summaries that fit the window, the check before a message goes out |
| `agent-bridge.mjs` | the chat-to-session map, turns, approvals, Stop, status |
## Frames sent on `/api/chat/stream` for an agent turn
`queued`, `start` (`agent:true`), `reasoning`, `delta`, `agent-tool` (new: upserts a tool card by call id with
`queued`, `start` (`agent:true`), `reasoning`, `delta`, `agent-tool` (upserts a tool card by call id with
`running | proposed | done | denied | error`), `done` (usage, `finishReason`, `cancelled`), `error`, and `notice`
(why a chat is not using the agent). The chat UI renders them with its existing components.
@@ -57,7 +61,7 @@ The chat's Mode menu is real Harness behaviour. Each turn the page sends the cha
| Bypass | `strata-bypass` | `danger-full-access` | `never` | Full access without prompts, as before. |
- Manual relies on the gate, so a Manual turn does not start unless the bridge has confirmed the gate is loaded (`/strata-gate` command): without it Manual would be an open sandbox that nobody asks about.
- The gate listens to `tools/pre-execute` (the seam the Harness's own Auto-review layer uses) and returns an `ask` decision; the Harness's approval service then asks, runs the tool only after `allowed-once`, and records the answer. Read-only tools and a short allowlist of look-only shell commands (Get-ChildItem, git status, ...) run unasked; MCP and unknown tools ask.
- The gate listens to `tools/pre-execute` (the seam the Harness's own Auto-review layer uses) and returns an `ask` decision; the Harness's approval service then asks, runs the tool only after `allowed-once`, and records the answer. Read-only tools and a short allowlist of look-only shell commands (Get-ChildItem, git status, ...) run unasked, each only with the options listed for it in `strata-gate.mjs` (an unknown option, or one that writes, runs a program, sets something or never ends, asks); MCP and unknown tools ask.
- Web search and web pages (`web_search`, `web_fetch`) ask once per chat in Manual, not once per call. The gate asks through the approval service itself (`webDecision`: "The assistant wants to search the web for: ... Search words go to <service>." / "... open the web page ..."), remembers a yes for the chat (a helper agent shares its chat's answer, and two searches started together share one card), and on a no gives the model `WEB_REFUSAL` ("The person did not allow web access ... Go on without it and say so."). Plan and Bypass are unchanged. Settings -> Tools has a switch, "Let the assistant search the web without asking" (off by default, `webWithoutAsking` in `tools-settings.mjs`, passed to the gate as `config.web.skipAsk`), that skips the question in Manual. A scheduled task has nobody at the card, so its web call is answered no unless that switch is on. Tests: `tests/web-ask.test.mjs`.
- The mode is read once per turn; an unknown or missing value is Manual.
@@ -70,7 +74,7 @@ The chat's Mode menu is real Harness behaviour. Each turn the page sends the cha
What the person chose is saved by `tools-settings.mjs` (inside the host's settings file) and shown by `src/components/SettingsTools.tsx` (`GET/PUT /api/agent/tools`, `POST/PATCH/DELETE /api/agent/tools/custom[/id]`). A change reaches the agent from the next message: the profile carries a `toolsSignature`, and the bridge restarts the Harness (after the running turns end) when it differs.
- Tool groups (`tools-catalog.mjs`): each is one switch in plain words and is one or more preset plugins (`withoutDisabled`); `fetch` and `search` are the two halves of the Harness's `tool-web` (both off drops it). Goals off: `/goal` answers that goals are turned off.
- Web search needs no account. The default is DuckDuckGo's HTML result page (`../web-search-ddg.mjs`), with Bing's plain result page (`../web-search-bing.mjs`) as the second source. Manners, because DuckDuckGo refuses a program that asks fast (a real box: two searches in 1.2 s answered, the third got HTTP 202 and every DuckDuckGo page refused for many minutes): a browser User-Agent and the page's own request shape (Referer, Origin, Sec-Fetch-*), one request at a time per source, 2.5 to 4 s apart (random), at most 8 a minute, a 30 minute session cache per question, a cooldown after a refusal that doubles (1, 2, 4 ... 10 minutes, `Retry-After` honoured) during which that source gets no request, no lite-page follow-up after a refusal, one retry for a timeout or a 5xx, a 15 s limit. When every source refuses, the tool answers one plain sentence (`LIMITED_SENTENCE`: "Web search is limited right now. Try again in a minute, or add a search key (Exa, Perplexity or DeepSeek) in Settings -> Tools -> Web search.") plus an instruction to the model not to retry; the turn guard in `agent-bridge.mjs` (see Turn guard) stops a model that retries anyway. **The results come from DuckDuckGo or Bing**; they are not ranked or filtered here. The person may switch to Exa, Perplexity or DeepSeek with their own key (`../web-search-keyed.mjs`; DeepSeek uses the Harness's own provider): the key is saved in the settings file, reaches the agent only as the environment variable `STRATA_SEARCH_KEY`, never the profile file and never the screen (it only learns `keySet`). A keyed service without its key falls back to DuckDuckGo. The profile pins `web.searchProvider` to the chosen id so two usable backends cannot make the choice ambiguous.
- Web search needs no account. The default is DuckDuckGo's HTML result page (`../web-search-ddg.mjs`), with Bing's plain result page (`../web-search-bing.mjs`) as the second source. Manners, because DuckDuckGo refuses a program that asks fast: a browser User-Agent and the page's own request shape (Referer, Origin, Sec-Fetch-*), one request at a time per source, 2.5 to 4 s apart (random), at most 8 a minute, a 30 minute session cache per question, a cooldown after a refusal that doubles (1, 2, 4 ... 10 minutes, `Retry-After` honoured) during which that source gets no request, no lite-page follow-up after a refusal, one retry for a timeout or a 5xx, a 15 s limit. When every source refuses, the tool answers one plain sentence (`LIMITED_SENTENCE`: "Web search is limited right now. Try again in a minute, or add a search key (Exa, Perplexity or DeepSeek) in Settings -> Tools -> Web search.") plus an instruction to the model not to retry; the turn guard in `agent-bridge.mjs` (see Turn guard) stops a model that retries anyway. **The results come from DuckDuckGo or Bing**; they are not ranked or filtered here. The person may switch to Exa, Perplexity or DeepSeek with their own key (`../web-search-keyed.mjs`; DeepSeek uses the Harness's own provider): the key is saved in the settings file, reaches the agent only as the environment variable `STRATA_SEARCH_KEY`, never the profile file and never the screen (it only learns `keySet`). A keyed service without its key falls back to DuckDuckGo. The profile pins `web.searchProvider` to the chosen id so two usable backends cannot make the choice ambiguous.
- The browser: Playwright's MCP server (`@playwright/mcp`, staged next to the Harness by `apps/desktop/scripts/stage-harness.mjs`) on the person's own Chrome, Edge or Chromium (`browser-probe.mjs`), `--isolated` so it never sees their profile. Tools appear as `mcp__browser__browser_*`. In Manual, opening a page, clicking, typing, uploading, running a script in the page and closing tabs ask (plain reasons: "Open the web page ...", "Click on the page: ..."); looking (snapshot, screenshot, console, network, waiting, listing tabs) does not. Plan refuses every connected tool. The screen says plainly when no browser, or no tool server, is found.
- The person's own tools: `instructions` (the tool returns their text; look-only, never asks), `command` (runs their command with the assistant's text in the `STRATA_INPUT` environment variable, never spliced into the command line; asks in Manual, refused in Plan) and `server` (a web address or a program, added as `mcp__custom-<name>__*`; asks in Manual). Instruction and command tools are served by `custom-tools-mcp.mjs` (stdio MCP) from a JSON file the settings module rewrites on every change, so a switch in Settings is seen without a restart.
- `strata-gate.mjs` implements the Manual/Plan rules above (`planDecision`, `browserDecision`, the look-only instruction server).
@@ -81,7 +85,7 @@ What the person chose is saved by `tools-settings.mjs` (inside the host's settin
- A helper's request for approval is the same card (state `proposed`, id `<helper session>:<call>`), answered with the same `/api/chat/approval`; a chat can only answer its own helpers.
- The run (the chat's one NDJSON response) stays open until the parent is idle, every helper has finished and reported back, and no armed goal is waiting; `done` is sent then.
- Stop: pauses an active goal, interrupts every helper (`subagents/interruptByParent`; `session/cancel` on a helper session is refused by the Harness), cancels the parent, waits; a report that would wake the parent afterwards is cancelled too.
- The number of helpers at once equals the engine's request slots (`maxActiveSubagents` in the profile); the Harness refuses more with a message the model reads.
- The number of helpers at once is the engine's request slots, with a floor of 4 for a local model and 8 for an API model (`maxActiveSubagents` in the profile). The Harness refuses more with a message the model reads.
## /goal
@@ -89,8 +93,15 @@ What the person chose is saved by `tools-settings.mjs` (inside the host's settin
## Thinking and the output budget
The Harness staging script (`apps/desktop/scripts/stage-harness.mjs`) applies Strata's guarded pi-ai 1.1.0 patch to its staged copy
(`harness-budget.mjs`, #3684): below 64K the context safety room is at most `min(4096, floor(contextWindow / 16))` and never more
than half the estimated remaining room, so an 8K window keeps output room after a tool result instead of ending with a one-token
cap. The caller's output cap and the one-token floor remain; windows of 64K or more keep the previous calculation. `BUNDLE.json.patches`
records the patch identity, the dependency version and the before/after SHA-256. Unknown dependency bytes stop staging for review.
Source and already-installed Harness copies are not changed.
- A local route is reasoning-capable (`reasoning: true`) with `compat.thinkingFormat: 'chat-template'`: the request carries `chat_template_kwargs {enable_thinking, reasoning_effort}`, which the Strata server reads (a top-level `enable_thinking` is ignored by it, which is why pi-ai's `qwen` format left the model thinking). The chat's Reasoning menu travels as `thinking` in the stream body; the bridge sends it with `session/selectModel` (`reasoningEffort`) whenever the model or the level changed; a level a route does not have is dropped (never an error). API routes keep the provider default (the menu is disabled for them in the UI).
- `localOutputBudget(context)` = context minus 4096 (it was context/4 capped at 8192, which cut big answers off at exactly 8192 tokens); pi-ai sends the window minus the prompt. App-loaded models get `fit_max_tokens: true` so the engine clamps instead of answering 400. Sampling is not set here: the Harness sends none, so the server's own defaults apply.
- `localOutputBudget(context)` is 30 percent of the window, at least 4096 and at most 32768 (9,830 tokens at 32K, 19,660 at 64K). The Harness's automatic compaction reads it as the room a reply reserves (the request header), and it is NOT the ceiling of a request: `plugins/strata-context.mjs` raises what the adapter sends to half the window, at most 64K tokens (`reply-cut.mjs` `replyCap`; pi-ai clamps it to the window minus the prompt), because a thinking model that writes a whole file in one call was cut inside the call at 19,660 and lost it (AGENT-LOST). A reply that is cut anyway is announced in the chat, the agent is asked to go on in smaller pieces (twice at most), and the file being written is streamed to the page as `agent-tool-draft` frames (name, size, lines, tail of the text). App-loaded models get `fit_max_tokens: true` so the engine clamps instead of answering 400. Sampling is not set here: the Harness sends none, so the server's own defaults apply.
## Tasks
@@ -108,7 +119,7 @@ Failures the person sees are plain sentences with a next step (`AgentUnavailable
## Helper agents on a named model (`plugins/strata-models.mjs`, `model-routes.mjs`)
The Harness's `subagent` tool takes an exact `provider` route key and model id, and the app had its model choice switched off, so every helper ran on the chat's model. The profile now enables the Harness's `subagent-model-selection-settings` with every route the app can reach (local instances and saved API models), and the `strata-models` plugin wraps the two tool definitions the Harness registers for each chat (`subagent`, `list_subagent_models`) in place:
The Harness's `subagent` tool takes an exact `provider` route key and model id. The profile enables the Harness's `subagent-model-selection-settings` with every route the app can reach (local instances and saved API models), and the `strata-models` plugin wraps the two tool definitions the Harness registers for each chat (`subagent`, `list_subagent_models`) in place:
- the model is found from its id, `provider/id`, the name in the model picker, `connection name`, or "local" (any letters). A close guess is not started: the tool answers "Did you mean ...?" and nothing runs until the call is made again with that model. Several fits are listed; nothing fitting lists the models that can be used and says they can be added in Models.
- the tool's own text says when to use it ("ask a subagent", "ask model X"), not to look for the model with shell commands, and not to wait with sleep. The system prompt (`preset.mjs`) says the same, so it reaches every model, local or hosted.
@@ -126,7 +137,7 @@ Pictures are on by default for every model that can read them: an API model read
## Turn guard (`turn-guard.mjs`)
A turn that keeps failing the same way is stopped by the app, never left to the model. Seen on a search-heavy `/plan`: every web search was refused, the model searched again and rewrote its plan after each one until the context reached 100%, then the turn ended with no answer and no message.
A turn that keeps failing the same way is stopped by the app, never left to the model.
- **Counted, from what the turn shows:** web searches that failed in a row (3), searches in one turn (10 in a plan turn, 25 otherwise), a plan turn that starts its numbered list from `1.` again (4 times), and a context of 93% or more (the Harness's own compaction did not keep up).
- **Stopped the way Stop stops** (`session/cancel`, helpers first) but ended as a normal turn (`done`, not cancelled, no error card), so the plan the model had written is still proposed. The plan card carries the reason (`plan.caution`); any other turn ends with one `delta` frame that has `own: true` and `key` (the English sentence, translated by the page; `GUARD_NOTES` are in the interface catalog in all eight languages). Each sentence says what happened and gives one next step.
@@ -134,9 +145,7 @@ A turn that keeps failing the same way is stopped by the app, never left to the
- **Never silent:** a turn that ends (`completed` or `max-tokens`) with no answer text gets "I ran out of room in this chat..." when the last request used 85% of the window or more, otherwise "I stopped without writing an answer..." (not when tool cards were shown, not after Stop, not after an error, which says why by itself). Goal runs have their own ending lines and are not guarded.
- Tests: `tests/agent-turn-guard.test.mjs` (counting, always; end to end with the real Harness, the scripted model's `searchloop` and `bloat` scripts and `searchMock: 'limited'` on the bridge, which runs the real search code on a pretend web that refuses, no network).
## Context: measured pressure, summaries that fit, the check before a message goes out (2026-10-22)
Found by the owner's hands-on test of the installed 0.2.1 (Gyro-S, 64K): a 118 KB log paste was 66,500 tokens to the engine and about 29,500 to the Harness (4 characters a token; logs and code are about 2); the engine refused it with a 400, the refusal stayed in the history, and then neither the next message nor `/compact` could go through, because the summary request carried the same history and was refused for its size too. `/compact` itself was never a command of the app: it went to the model as a question and the model answered "nothing to compact".
## Context: measured pressure, summaries that fit, the check before a message goes out
- **`plugins/strata-context.mjs`** (inserted by the profile, like the gate) wraps `tokenMeter.measure` and `llm.stream` in place, once each. Pressure = the last prompt size the model server reported for an answer, plus what changed since priced at the density that measurement showed (`context-budget.mjs`: `pressureNow`, `densityOf`); never lower than the Harness's own price. Node prices are scaled the same way, so the retained tail is in real tokens. A summary request (`purpose: 'compaction'`) that would not fit the window is asked for in pieces: a message bigger than a piece is cut for the request only, each piece builds on the summary before it, a piece the server refuses is halved and tried again. `/strata-context` (a Harness command, like `/strata-gate`) answers `{tokens, window}` for a session so the host can tell the person true figures.
- **`context-fit.mjs`**: before a message is sent the model server counts it (`POST /v1/messages/count_tokens`, its own tokenizer). `decideFit`: send; summarise first and then send (the message fits an otherwise empty chat); or never fits. The last one is not sent: the error frame has `code: 'CONTEXT_MESSAGE_TOO_LARGE'` and the page offers "Attach as a file" (`oversize.mjs`, `/api/chat/oversize`: the text is saved in the chat's folder, the assistant reads it in parts) or "Send a shortened preview". An engine that cannot count lets the message through; the Harness's own compact-and-retry is the net.
@@ -144,3 +153,15 @@ Found by the owner's hands-on test of the installed 0.2.1 (Gyro-S, 64K): a 118 K
- **Visible:** `compaction/start` shows "Summarizing earlier messages..." in the status line; `compaction/end` adds one plain line to the reply. The projector also sends a `context` frame after every model request, so the meter follows the running turn.
- **A refusal that is left** (`CONTEXT_WINDOW_EXCEEDED`) is a plain sentence with the engine's text under Details and the buttons "Summarize and continue" and "Start a new chat". "Try sending the message again" is for other errors only.
- Tests: `tests/agent-context.test.mjs` (arithmetic; the real Harness against the scripted worker with `window`, `charsPerToken` and `noCount`), `tests/oversize.test.mjs`, `tests/context-ui.test.mjs`. The run against a real engine is `scripts/context-e2e.mjs`.
## History / older measurements
- **Search refusals (2026-10):** on a real box two DuckDuckGo searches in 1.2 s answered, the third got HTTP 202, and every DuckDuckGo page refused for many minutes.
The pacing, cooldown and cache rules above come from that test.
- **Search-heavy `/plan` (2026-10):** every web search was refused, the model searched again and rewrote its plan after each one until the context reached 100%.
The turn ended with no answer and no message. The turn guard comes from that run.
- **Large log paste (2026-10-22, installed 0.2.1, Gyro-S, 64K):** a 118 KB log was 66,500 tokens to the engine and about 29,500 to the Harness
(4 characters a token; logs and code are about 2). The engine refused it with a 400. The refusal stayed in the history, so neither the next message nor `/compact` could
go through, because the summary request carried the same history. `/compact` was also not a command of the app yet: it went to the model as a question. This
produced `strata-context.mjs`, `context-fit.mjs` and the host's own `/compact`.
- **Output budget:** an earlier local budget of a quarter of the window, capped at 8192, cut big answers off at exactly 8192 tokens.
+150 -10
View File
@@ -26,10 +26,15 @@ import { buildProfile, routeFor, MODE_PRESETS, modeOf, effortOf, localOutputBudg
import { TurnProjector, parseArguments as parseArgs } from './events.mjs';
import { ChildProjector } from './children.mjs';
import { nearestBudget } from './llm-shape.mjs';
import { DeliverableTracker, ActivityGroups } from './deliverables.mjs';
import { DeliverableTracker, ActivityGroups, FolderClaims } from './deliverables.mjs';
import { SkillsUsed, skillInfoFrom } from './skills-used.mjs';
import { turnEngineSpeed } from './engine-speed.mjs';
import { TurnGuard, guardNote, GUARD_NOTES } from './turn-guard.mjs';
import { cutNotice, cutNudge } from './reply-cut.mjs';
/** How often one turn asks the agent to carry on after a reply the length limit cut (each time in smaller pieces). */
export const MAX_CUT_RETRIES = 2;
import { buildAnswer, parseQuestions } from './user-questions.mjs';
import { BASE_TOKENS, COUNT_ABOVE_CHARS, FIT_TEXT, countTokens, decideFit, fill, grouped } from './context-fit.mjs';
import { PlanStore, planPrompt, parsePlan, parsePoints, stepObjective, stepLine, firstSentence, stepSummary, planAsPrompt, editedSteps, planView, researchPrompt, researchWritePrompt, RESEARCH_NUDGE, parseSources } from './plan.mjs';
@@ -122,6 +127,8 @@ export function createAgentBridge({ dataDir, componentsDir = null, bundledDir =
const home = join(root, 'home');
const sessionsFile = join(root, 'sessions.json');
mkdirSync(root, { recursive: true });
/** Which chat's file tools wrote which file, for chats that share a folder (#5008). */
const folderClaims = new FolderClaims();
/**
* Where the turn's file tracker must never look (deliverables.mjs): the app, the engine and its components, Strata's own data, AppData
@@ -157,6 +164,8 @@ export function createAgentBridge({ dataDir, componentsDir = null, bundledDir =
const queues = new Map();
const queueView = chatId => (queues.get(chatId) ?? []).map(({ id, text, at, deliverAt }) => ({ id, text, at, deliverAt }));
const pending = new Map(); // `${sessionId}:${callId}` -> { eventId, sessionId, callId, at }
const questions = new Map(); // `${sessionId}:${callId}` -> the agent's own question waiting for the person: { eventId, sessionId, callId, items, at, clientId } (#4244)
const chatOfSession = sessionId => Object.keys(sessionMap).find(id => sessionMap[id]?.sessionId === sessionId) ?? null;
const idleWaiters = new Set();
const chat = id => { let c = chats.get(id); if (!c) { c = { id, turn: null, route: null, routeGeneration: -1 }; chats.set(id, c); } return c; };
@@ -275,10 +284,23 @@ export function createAgentBridge({ dataDir, componentsDir = null, bundledDir =
if (!r.callId) return;
pending.set(`${value.agentId}:${r.callId}`, { eventId: value.eventId, sessionId: value.agentId, callId: r.callId, tool: r.toolName, reason: r.reason ?? null, at: Date.now(), clientId: runtime.clientId });
} else if (value.type === 'waterfall' && value.event === 'user-questions/request') {
// The app has no surface for the agent's own questions yet. Declaring that nobody can answer makes
// the tool return an error the model reads and carries on from, instead of a turn that waits forever.
void runtime.gw.answerEvent({ clientId: runtime.clientId, eventId: value.eventId, outcome: 'next' }).catch(() => {});
// The agent of a chat that is running in front of the person asks the person: the question goes to the page (the question card above the message box) and the
// Harness waits for the answer. Anything else (a helper's question, a chat that is stopping, a request with nothing to show) is declared
// unanswerable, which makes the tool return an error the model reads and carries on from, instead of a turn that waits forever.
const r = value.request ?? {};
const chatId = chatOfSession(value.agentId);
const turn = chatId ? chats.get(chatId)?.turn : null;
const items = parseQuestions(r.questions);
// A goal or a plan step runs without anyone there: a question would stall it, so it is declined at once (the model reads "could not be answered" and goes on).
if (!turn || turn.finished || turn.stopping || turn.goalRun || turn.plan || !turn.announceQuestion || !items.length) {
void runtime.gw.answerEvent({ clientId: runtime.clientId, eventId: value.eventId, outcome: 'next' }).catch(() => {});
} else {
const callId = String(r.wait?.callId ?? value.eventId);
questions.set(`${value.agentId}:${callId}`, { eventId: value.eventId, sessionId: value.agentId, callId, items, at: Date.now(), clientId: runtime.clientId });
turn.announceQuestion({ type: 'agent-question', callId, questions: items });
}
} else if (value.type === 'cancel') {
for (const [k, v] of questions) if (v.eventId === value.eventId) { questions.delete(k); chats.get(chatOfSession(v.sessionId))?.turn?.announceQuestion?.({ type: 'agent-question-end', callId: v.callId }); }
for (const [k, v] of pending) if (v.eventId === value.eventId) pending.delete(k);
}
}
@@ -347,24 +369,27 @@ export function createAgentBridge({ dataDir, componentsDir = null, bundledDir =
let hideDeltas = !!researchTopic;
signal?.addEventListener('abort', () => { send = () => {}; attached = false; }, { once: true });
// What the assistant itself wrote in this run (not our own step lines): a plan turn reads its plan from it, a step reads its summary.
const written = { all: '', step: '' };
const written = { all: '', step: '', reasoning: '' };
// The frame that closed the turn, kept with the turn's text for a page that comes back after it ended (attach).
let terminal = null;
// The turn guard (turn-guard.mjs): counts failing searches, searches, plan rewrites and a nearly full context. Set up once the route is known.
let guard = null;
const raw = frame => { try { send(frame); } catch { send = () => {}; attached = false; } };
const raw = frame => { if (frame.type === 'done' || frame.type === 'error') terminal = frame; try { send(frame); } catch { send = () => {}; attached = false; } };
// What the turn made (files) and compact activity lines (notes-split-agent8.md): derived from the cards the turn already sends.
const tracker = new DeliverableTracker(workspacePath, trackerFolders());
const tracker = new DeliverableTracker(workspacePath, { ...trackerFolders(), claims: folderClaims, owner: chatId });
let toolsRan = false; // a turn that ran no tool wrote nothing, so there is nothing to look for at its end
try { tracker.snapshot(); } catch { /* a folder that cannot be read has no deliverables */ }
const groups = new ActivityGroups({ emit: raw });
const skills = new SkillsUsed({ emit: raw, info: skillInfoFrom(skillsDir()) });
const emit = frame => {
if (frame.type === 'reasoning' && frame.text && written.reasoning.length < 2_000_000) written.reasoning += frame.text;
if (frame.type === 'delta' && !frame.own) { written.all += frame.text; written.step += frame.text; if (guard) trip(guard.text(frame.text)); if (hideDeltas) return; }
if (frame.own) { const { own, ...rest } = frame; frame = rest; }
if (frame.type === 'delta' && frame.text) groups.text();
if (frame.type === 'agent-tool' && frame.tool) {
if (['done', 'error'].includes(frame.tool.state) && !['read', 'read_image', 'glob', 'grep', 'web_fetch', 'web_search'].includes(frame.tool.name)) toolsRan = true;
let made = null;
if (frame.tool.state === 'done' && (frame.tool.name === 'write' || frame.tool.name === 'edit')) { try { made = tracker.check(frame.tool.args?.file_path ?? frame.tool.args?.path); } catch { made = null; } }
if (frame.tool.state === 'done' && (frame.tool.name === 'write' || frame.tool.name === 'edit')) { try { made = tracker.check(frame.tool.args?.file_path ?? frame.tool.args?.path, { explicit: true }); } catch { made = null; } }
raw(frame);
if (made) raw(made);
if (guard) trip(guard.card(frame.tool));
@@ -443,6 +468,20 @@ export function createAgentBridge({ dataDir, componentsDir = null, bundledDir =
research: researchTopic ? { phase: 'gather', nudged: false, waitingStart: false } : null,
};
c.turn = turn;
if (workspacePath) folderClaims.begin(workspacePath, chatId);
/**
* A page that comes back (a reload, a new window) takes the running turn over again. Everything that was said while no page was there is
* sent as one catch-up: the start, the text and thinking so far (a `resync`), every tool card as it is now (the ones waiting for a yes
* included) and the questions waiting for an answer. After that the turn's frames flow to the new page as they did to the old one.
*/
turn.reattach = ({ onFrame, signal: again }) => {
send = onFrame; attached = true;
again?.addEventListener('abort', () => { send = () => {}; attached = false; }, { once: true });
raw({ type: 'start', model: route.name, contextMax: route.contextWindow, demo: false, remoteModel: route.kind === 'api', agent: true, mode: appliedMode, sessionId, requestId, skills: [], resumed: true, ...(planTask ? { planning: true } : {}) });
raw({ type: 'resync', content: hideDeltas ? '' : written.all, reasoning: written.reasoning });
for (const t of projector.tools.values()) if (t.card) raw({ type: 'agent-tool', tool: t.card });
for (const q of questions.values()) if (q.sessionId === sessionId) raw({ type: 'agent-question', callId: q.callId, questions: q.items });
};
/**
* A turn must always end. If the Harness sends nothing at all for a while (a tool that never returns, a model server that went
* quiet, a lost event) the turn is stopped and ended with a plain message instead of showing "thinking" forever. A running tool gets
@@ -471,6 +510,7 @@ export function createAgentBridge({ dataDir, componentsDir = null, bundledDir =
async function finishTurn(failure) {
if (turn.finished) return;
turn.finished = true;
for (const [k, v] of questions) if (v.sessionId === turn.sessionId) questions.delete(k);
clearTimeout(turn.settleTimer);
clearInterval(turn.watchdog);
turn.follow?.close();
@@ -479,12 +519,21 @@ export function createAgentBridge({ dataDir, componentsDir = null, bundledDir =
reporter.clear();
const extra = [];
try { if (toolsRan) for (const d of tracker.sweep()) { emit(d); groups.addFile(d); } } catch { /* never blocks the end of a turn */ }
if (workspacePath) folderClaims.end(workspacePath, chatId);
groups.end();
if (researchTopic && !failure && !turn.stopping) { const sources = parseSources(written.all); if (sources.length) extra.push({ type: 'research', topic: researchTopic, sources }); }
try { extra.push(...settlePlan(failure)); } catch { /* a plan frame must never break the end of a turn */ }
// The turn guard: a turn the app stopped, or one that ended with no words, always ends with one plain message (a plan card carries it itself).
let noteReason = turn.guard ?? null;
if (!noteReason && guard && !failure && !turn.stopping && !turn.goalRun) {
// A reply the length limit cut ends with a sentence that says so (and what was lost), never with silence (AGENT-LOST).
const cutEnd = !failure && !turn.stopping && projector.ended?.kind === 'max-tokens';
if (cutEnd && !noteReason) {
const note = cutNotice({ ...projector.cut(), retrying: false });
extra.push({ type: 'delta', own: true, text: `
${note.text}`, key: note.key, values: note.values, noteReason: 'cut' });
}
if (!noteReason && !cutEnd && guard && !failure && !turn.stopping && !turn.goalRun) {
noteReason = guard.silent({ ended: projector.ended, wrote: projector.emittedAny === true, acted: projector.tools.size > 0, contextTokens: projector.contextTokens });
}
if (noteReason && !extra.some(f => f.type === 'plan' && f.event === 'proposed')) { const note = guardNote(noteReason); if (note) extra.push(note); }
@@ -500,6 +549,8 @@ export function createAgentBridge({ dataDir, componentsDir = null, bundledDir =
}
for (const f of frames.slice(-1)) emit(f);
c.turn = null;
// What a page that was away needs to show the end of this turn (attach, below).
c.last = { requestId, sessionId, mode: appliedMode, model: route.name, contextMax: route.contextWindow, remoteModel: route.kind === 'api', content: hideDeltas ? '' : written.all.slice(0, 4_000_000), reasoning: written.reasoning, cards: [...projector.tools.values()].filter(t => t.card).map(t => t.card), terminal };
turn.ended.resolve({ reason: projector.ended, failure: failure ?? null });
notifyIdle();
// A turn that filled the context, or that the guard stopped, leaves a long history behind: summarise the older part now so the next message has room.
@@ -601,6 +652,7 @@ export function createAgentBridge({ dataDir, componentsDir = null, bundledDir =
case 'turn/start':
turn.parentOpen = true;
if (turn.research) turn.research.waitingStart = false;
turn.cutWaiting = false;
clearTimeout(turn.settleTimer);
// After Stop, a helper's report would start a new turn that nobody asked for.
if (turn.stopping) void runtime.gw.rpc('session/cancel', { request: { sessionId } }).catch(() => {});
@@ -752,6 +804,24 @@ export function createAgentBridge({ dataDir, componentsDir = null, bundledDir =
const current = await goalGet();
if (turn.finished || turn.parentOpen) return;
if (current && current.phase === 'active' && current.activation === 'armed') { turn.settleTimer = after(700, armSettle); return; }
// A reply the length limit cut (AGENT-LOST): the agent lost what it was writing. Say so in the chat and ask it to carry on in smaller pieces, twice at most.
if (turn.cutWaiting) { turn.settleTimer = after(300, armSettle); return; }
if (projector.ended?.kind === 'max-tokens' && !turn.goalRun && !turn.stopping && (turn.cuts ?? 0) < MAX_CUT_RETRIES) {
const cut = projector.cut();
turn.cuts = (turn.cuts ?? 0) + 1;
const note = cutNotice({ ...cut, retrying: true });
emit({ type: 'delta', own: true, text: `
${note.text}
`, key: note.key, values: note.values, noteReason: 'cut' });
for (const f of projector.clearCut()) emit(f);
turn.cutWaiting = true;
try { await runtime.gw.rpc('session/prompt', { request: { requestId: `cut-${randomUUID()}`, sessionId, mode: 'queue', content: [{ type: 'text', text: cutNudge(cut) }] } }); }
catch { turn.cutWaiting = false; finishTurn(null); return; }
turn.settleTimer = after(500, armSettle);
return;
}
// Research: gather (search and read pages), then write the answer in the fixed shape. A step that did not open pages is sent back once.
if (turn.research && !turn.stopping) {
const r = turn.research;
@@ -794,6 +864,7 @@ export function createAgentBridge({ dataDir, componentsDir = null, bundledDir =
}
turn.announceGoal = announceGoal;
turn.announceQueue = frame => emit(frame);
turn.announceQuestion = frame => emit(frame);
const snapshotTimer = {};
await Promise.race([turn.snapshot.promise, new Promise((_, rej) => { snapshotTimer.t = after(20_000, () => rej(new Error('the session stream did not open'))); })])
@@ -1096,6 +1167,47 @@ export function createAgentBridge({ dataDir, componentsDir = null, bundledDir =
delete sessionMap[chatId]; persist();
}
// -- the agent's own questions ------------------------------------------------------------------------------
/**
* The person's answer to a question the agent asked (#4244), or a skip. The answer is checked against the questions that were asked, then sent
* to the Harness as the result of the waiting `user-questions/request`; a skip declares the question unanswerable, as before, and the model
* carries on.
*/
async function answerQuestion({ chatId, callId, answers, skip = false }) {
const sessionId = sessionMap[chatId]?.sessionId;
const key = `${sessionId}:${callId}`;
const entry = questions.get(key);
if (!entry || !rt?.hp.running) throw new AgentUnavailable('That question was already answered, or the assistant has gone on.', 'question-gone');
const outcome = skip ? 'next' : buildAnswer(entry.items, answers);
await rt.gw.answerEvent({ clientId: entry.clientId ?? rt.clientId, eventId: entry.eventId, outcome });
questions.delete(key);
return { ok: true, skipped: !!skip };
}
// -- a message for a helper ---------------------------------------------------------------------------------
/**
* The person's own words for one helper (#4749), through the Harness's `subagents/prompt`: it delivers one browser-authored message to a
* continuable child through its live parent. "steer" reaches a working helper at its next step; "queue" gives a helper that has finished
* (or is between steps) a new turn. The Harness refuses a helper that cannot take messages (a one-shot one) and a parent that is not live;
* either comes back as a plain sentence.
*/
async function messageHelper({ chatId, helperId, text, delivery = 'queue' }) {
const clean = String(text ?? '').trim().slice(0, 8000);
if (!clean) throw new AgentUnavailable('Type a few words for the helper.', 'helper-empty');
if (!/^[\w.:@-]{1,160}$/.test(String(helperId ?? ''))) throw new AgentUnavailable('That helper is not known.', 'helper-unknown');
const parent = sessionMap[chatId]?.sessionId;
if (!parent || !rt?.hp.running) throw new AgentUnavailable('The assistant is not running in this chat, so it cannot pass a message on. Send a message in the chat first.', 'helper-idle');
try {
const receipt = await rt.gw.rpc('subagents/prompt', { request: { requestId: `helper-${randomUUID()}`, parentSessionId: parent, childSessionId: String(helperId), mode: 'continuable', delivery: delivery === 'steer' ? 'steer' : 'queue', content: [{ type: 'text', text: clean }] } });
return { ok: true, messageId: receipt?.messageId ?? null, delivery: delivery === 'steer' ? 'steer' : 'queue' };
} catch (e) {
log(`agent · message for a helper refused: ${String(e?.message ?? e).slice(0, 200)}`);
throw new AgentUnavailable(/continuable|one-shot|not.*(found|live|open)|unavailable|closed|ended/i.test(String(e?.message ?? '')) ? 'This helper cannot take a message any more. Ask the assistant in the chat to start a new one.' : 'The message could not be delivered right now. Try again in a moment.', 'helper-refused');
}
}
// -- messages typed while a goal runs ----------------------------------------------------------------------
/** Queue a message for the running goal. It is delivered between steps, or handed back to the page when the goal ends or pauses. */
@@ -1312,6 +1424,34 @@ export function createAgentBridge({ dataDir, componentsDir = null, bundledDir =
return { stopped: true, ended: result.reason?.kind ?? 'unknown', ms: Date.now() - t0 };
}
/** The turn running for a chat now, for a page that has just loaded; null when none. */
function turnOf(chatId) {
const t = chats.get(chatId)?.turn;
if (!t || t.finished) return null;
return { chatId, sessionId: t.sessionId, requestId: t.requestId, mode: t.mode, stopping: !!t.stopping, pending: pendingFor(chatId) };
}
/** Every chat with a turn running. */
function runningTurns() { return [...chats.keys()].map(turnOf).filter(Boolean); }
/**
* Take a turn over from a new page. A running turn: the catch-up, then the live frames until it ends or the page leaves again. A turn that
* ended while no page was there (`requestId` names it): its text, tool cards and closing frame once. null when there is nothing to take over.
*/
function attach({ chatId, requestId = null, onFrame, signal }) {
const c = chats.get(chatId);
const turn = c?.turn;
if (turn && !turn.finished && typeof turn.reattach === 'function') {
if (requestId && turn.requestId !== requestId) return null;
turn.reattach({ onFrame, signal });
return { ended: turn.ended.promise.then(() => undefined), live: true };
}
const last = c?.last;
if (!last || !requestId || last.requestId !== requestId) return null;
onFrame({ type: 'start', model: last.model, contextMax: last.contextMax, demo: false, remoteModel: last.remoteModel, agent: true, mode: last.mode, sessionId: last.sessionId, requestId: last.requestId, skills: [], resumed: true });
onFrame({ type: 'resync', content: last.content, reasoning: last.reasoning });
for (const card of last.cards) onFrame({ type: 'agent-tool', tool: card });
if (last.terminal) onFrame(last.terminal);
return { ended: Promise.resolve(), live: false };
}
function pendingFor(chatId) {
const sessionId = sessionMap[chatId]?.sessionId;
const helpers = chats.get(chatId)?.turn?.children;
@@ -1333,7 +1473,7 @@ export function createAgentBridge({ dataDir, componentsDir = null, bundledDir =
/** Let go of a chat's Harness session mapping (a task starts every run in a session of its own). */
function forget(chatId) { if (chats.get(chatId)?.turn) return; delete sessionMap[chatId]; chats.delete(chatId); persist(); }
return { compactCommand, goalState, goalControl, harnessDir, status, runTurn, goalCommand, planCommand, researchCommand, llmTarget, setPlanBriefing, resetSession, reopenPlan, skipStep, planGoal, planControl, planState, queueMessage, cancelQueued, takeQueue, forget, approve, setMode, stop, pendingFor, pendingAll, close, sessionOf: chatId => sessionMap[chatId]?.sessionId ?? null, workingFolder: chatId => sessionMap[chatId]?.cwd ?? null, isRunning: chatId => !!chats.get(chatId)?.turn, _debug: () => ({ rt, sessionMap }) };
return { compactCommand, messageHelper, answerQuestion, goalState, goalControl, harnessDir, status, turnOf, runningTurns, attach, runTurn, goalCommand, planCommand, researchCommand, llmTarget, setPlanBriefing, resetSession, reopenPlan, skipStep, planGoal, planControl, planState, queueMessage, cancelQueued, takeQueue, forget, approve, setMode, stop, pendingFor, pendingAll, close, sessionOf: chatId => sessionMap[chatId]?.sessionId ?? null, workingFolder: chatId => sessionMap[chatId]?.cwd ?? null, isRunning: chatId => !!chats.get(chatId)?.turn, _debug: () => ({ rt, sessionMap }) };
}
/** A goal that stopped by itself, in words. */
+12
View File
@@ -160,3 +160,15 @@ export function isContextOverflow(error) {
const text = [error.message, error.cause?.message, error.failure?.message, error.failure?.code].filter(Boolean).join(' ');
return /request exceeds the context window|leaves no room to answer|context[_ ]length|context window|too many tokens|maximum context/i.test(text);
}
/** The Harness asks for "concise English engineering prose", so a Chinese or Russian chat came back summarised in English (#4750). One sentence is added to the
* summary request's last message: write it in the language the person used. Paths, commands and identifiers stay exact. Messages are not changed in place. */
export const SUMMARY_LANGUAGE_RULE = 'Write the summary in the language the person used in this conversation (English only when they wrote English). The instruction above is in English; that does not change the language of the summary. Keep file paths, commands, error strings, identifiers and numbers exactly as they appear.';
export function withSummaryLanguage(messages) {
if (!Array.isArray(messages) || !messages.length) return messages;
const last = messages[messages.length - 1];
if (!last || last.role !== 'user') return messages;
const content = typeof last.content === 'string' ? last.content + ' ' + SUMMARY_LANGUAGE_RULE
: Array.isArray(last.content) ? [...last.content, { type: 'text', text: SUMMARY_LANGUAGE_RULE }] : null;
return content === null ? messages : [...messages.slice(0, -1), { ...last, content }];
}
+1 -1
View File
@@ -58,7 +58,7 @@ export async function countTokens({ baseUrl, key = '', model = '', text, fetchIm
} catch { return null; }
}
/** The plain sentences (also in the interface catalog in all eight languages). `{0}` slots are filled by the page. */
/** The plain sentences (also in the interface catalog in all nine languages). `{0}` slots are filled by the page. */
export const FIT_TEXT = Object.freeze({
tooLarge: 'This message is about {0} tokens long, and this model can read about {1} at a time. It is too large to send as it is.',
tooLargeNext: 'Attach it as a file for the assistant to read in parts, or send a shortened preview.',
@@ -11,6 +11,7 @@
* timeoutSec the time allowed, 1 to 3600 (default 120). A command that runs over is ended together with the programs it started.
*/
import { spawn, spawnSync } from 'node:child_process';
import { endTree as endTreeNow, ownGroup } from '../process-tree.mjs';
import { existsSync, statSync } from 'node:fs';
import { homedir } from 'node:os';
@@ -67,10 +68,7 @@ export const clampTimeoutSec = value => {
};
function endTree(child) {
try {
if (process.platform === 'win32' && child.pid) spawnSync('taskkill', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, timeout: 10000 });
else child.kill('SIGKILL');
} catch { /* gone */ }
endTreeNow(child, 'SIGKILL'); // the command's whole process group on Linux and macOS (it is started as its own group below), taskkill /T on Windows
}
/**
@@ -85,7 +83,7 @@ export function runCommand({ command, input = '', cwd, callCwd, shell = 'auto',
const limit = clampTimeoutSec(timeoutSec);
let child;
try {
child = spawn(plan.file, plan.args, { shell: plan.shell, windowsHide: true, cwd: where.cwd, env: { ...process.env, STRATA_INPUT: String(input ?? '') }, stdio: ['ignore', 'pipe', 'pipe'] });
child = spawn(plan.file, plan.args, { shell: plan.shell, windowsHide: true, detached: ownGroup(), cwd: where.cwd, env: { ...process.env, STRATA_INPUT: String(input ?? '') }, stdio: ['ignore', 'pipe', 'pipe'] });
} catch (e) { return resolve({ ok: false, text: `The command could not start in ${plan.name}: ${e.message}` }); }
let text = '', cut = false, timedOut = false;
const take = chunk => { if (text.length < OUT_MAX + 1) text += chunk.toString(); else cut = true; };
+51 -3
View File
@@ -49,6 +49,44 @@ export function isWithin(parent, child) {
return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
}
/**
* Who made which file when several chats work in one folder (#5008). A folder chosen for two chats is shared on purpose, but a
* chat's list of "files this turn made" is private to that turn: a file another chat wrote must not be shown as this chat's work.
* One registry is shared by the turns of a host. A turn `begin`s on its folder, `claim`s the files its own file tools wrote, and
* `end`s. Turns that were running at the same time in the same folder are `overlapped`: for them the end-of-turn sweep reports only
* files nobody else claimed AND that no overlapping turn could have written; since a script's output cannot be told apart, it is
* left out for an overlapped turn rather than given to the wrong chat.
*/
export class FolderClaims {
#active = new Map(); // folder key -> Set(owner)
#overlap = new Map(); // folder key + owner -> true
#claims = new Map(); // folder key -> Map(rel path -> owner)
#key(root) { return same(resolve(String(root))); }
begin(root, owner) {
const k = this.#key(root);
const set = this.#active.get(k) ?? new Set();
const others = [...set].filter(o => o !== owner);
if (others.length) { for (const o of [...others, owner]) this.#overlap.set(`${k}|${o}`, true); }
set.add(owner); this.#active.set(k, set);
}
end(root, owner) {
const k = this.#key(root);
const set = this.#active.get(k);
if (!set) return;
set.delete(owner); this.#overlap.delete(`${k}|${owner}`);
if (!set.size) { this.#active.delete(k); this.#claims.delete(k); }
}
claim(root, rel, owner) {
const k = this.#key(root);
if (!this.#active.has(k)) return;
const m = this.#claims.get(k) ?? new Map();
m.set(rel, owner); this.#claims.set(k, m);
}
ownerOf(root, rel) { return this.#claims.get(this.#key(root))?.get(rel) ?? null; }
/** True when another turn was running in this folder at any time during this owner's turn. */
overlapped(root, owner) { return this.#overlap.get(`${this.#key(root)}|${owner}`) === true; }
}
export class DeliverableTracker {
/**
* `exclude`: folders that are never searched when they lie inside the chat's folder (the app, the engine and its components, AppData,
@@ -56,8 +94,9 @@ export class DeliverableTracker {
* tracked at all. The turn only reports files the turn itself wrote: an explicit file-tool call, or (for scripts) a file whose modified
* or creation time is after the turn started.
*/
constructor(root, { now = Date.now, exclude = [], forbid = [] } = {}) {
constructor(root, { now = Date.now, exclude = [], forbid = [], claims = null, owner = null } = {}) {
this.root = root ? resolve(root) : null;
this.claims = claims; this.owner = owner;
this.now = now;
if (this.root && forbid.filter(Boolean).some(p => isWithin(p, this.root))) this.root = null;
// only the folders that lie inside the chat's folder matter for the search
@@ -104,13 +143,15 @@ export class DeliverableTracker {
}
/** The deliverable for one path now, compared with the snapshot (and the last report), or null when nothing changed. */
check(rawPath) {
check(rawPath, { explicit = false } = {}) {
if (!this.root) return null;
const full = resolve(this.root, String(rawPath ?? ''));
const rel = relative(this.root, full).split(sep).join('/');
if (!rel || rel.startsWith('..') || isAbsolute(rel) || rel.split('/').some(seg => SKIP_DIR.test(seg)) || this.#excluded(dirname(full))) return null;
let st; try { st = statSync(full); } catch { return null; }
if (!st.isFile()) return null;
// A file this chat's own file tool wrote is this chat's, whoever else works in the folder (#5008).
if (explicit && this.claims && this.owner) this.claims.claim(this.root, rel, this.owner);
const old = this.before.get(rel);
if (old && old.size === st.size && old.mtimeMs === st.mtimeMs) return null;
const type = fileType(rel);
@@ -139,7 +180,14 @@ export class DeliverableTracker {
// modified during the turn; or created during it (a copy that kept its old modified time), which is only certain when the first look saw the whole folder
const fresh = st.mtimeMs >= since || (!this.snapshotTruncated && (st.birthtimeMs || 0) >= since);
if (!fresh) return;
const d = this.check(relative(this.root, full)); if (d) out.push(d);
const rel = relative(this.root, full).split(sep).join('/');
if (this.claims && this.owner) {
// Another chat's file tool wrote it: not ours. Written by a script while another chat was working in the same folder: nobody can say whose it is.
const by = this.claims.ownerOf(this.root, rel);
if (by && by !== this.owner) return;
if (!by && this.claims.overlapped(this.root, this.owner)) return;
}
const d = this.check(rel); if (d) out.push(d);
});
return out;
}
+35 -8
View File
@@ -11,6 +11,7 @@ import { toolUpdate, TokenCount, fmtTokens } from './status.mjs';
import { askFor } from './ask.mjs';
import { FIT_TEXT, fill } from './context-fit.mjs';
import { isContextOverflow } from './context-budget.mjs';
import { ToolDraft, cutCalls } from './reply-cut.mjs';
const SHELL = new Set(['pwsh', 'bash', 'job_kill', 'job_output', 'job_list']);
const WRITE = new Set(['write', 'edit']);
@@ -222,7 +223,8 @@ export class TurnProjector {
this.asked = new Map(); // approval id -> callId
this.onStatus = null; // (update) => void: what the assistant is doing now (status.mjs); set by the bridge
this.reasoning = new TokenCount();
this.composing = new Map(); // callId -> { name, head, bytes }: a tool call the model is still writing
this.composing = new Map(); // callId -> ToolDraft: a tool call the model is still writing (still here at a cut reply = it was lost)
this.lastOutput = 0; // tokens the LAST model request produced
this.running = []; // callIds of tools that are running, oldest first
}
@@ -289,19 +291,20 @@ export class TurnProjector {
out.push({ type: 'reasoning', text: c.text });
} else if (c.type === 'tool-call-delta' && c.id) {
// The model is writing a tool call. For a big file this is the long silent part: nothing else arrives until it is finished.
const call = this.composing.get(c.id) ?? { name: c.name ?? '', head: '', bytes: 0 };
const call = this.composing.get(c.id) ?? new ToolDraft(c.id, c.name ?? '');
if (c.name) call.name = c.name;
const piece = String(c.argumentsDelta ?? '');
// Writing a tool call (a whole file, say) is decoding too: its tokens are in the engine's output count, so its time belongs in the speed.
this.#produced(f.attemptId, piece, out);
call.bytes += piece.length;
if (call.head.length < 4000) call.head += piece;
call.add(piece);
this.composing.set(c.id, call);
if (call.name && !NO_CARD.has(call.name)) {
const raw = (/"(?:file_path|path|filePath)"\s*:\s*"((?:[^"\\]|\\.)*)"/.exec(call.head) ?? /"url"\s*:\s*"((?:[^"\\]|\\.)*)"/.exec(call.head))?.[1];
let target = raw; try { if (raw !== undefined) target = JSON.parse('"' + raw + '"'); } catch { /* a half-written escape: show it as it is */ }
const target = call.target;
const args = target ? { file_path: target, url: target } : {};
this.#status(toolUpdate('preparing', 'preparing-tool', call.name, args, { bytes: call.bytes }, { args: { tool: call.name, verb: toolVerb(call.name, 'running'), target: target ?? '', bytes: call.bytes } }));
// The live data path of a file being written (AGENT-LOST): the name, a growing size and the tail of the text, at most five times a second.
const now = Date.now();
if (now - call.at >= 200) { call.at = now; out.push(call.frame({ scope: toolScope(call.name), verb: toolVerb(call.name, 'running') })); }
}
} else if (c.type === 'usage' && c.usage) {
this.usageSeen = true;
@@ -310,6 +313,7 @@ export class TurnProjector {
const prompt = (Number(c.usage.inputTokens) || 0) + (Number(c.usage.cacheReadTokens) || 0) + (Number(c.usage.cacheWriteTokens) || 0);
this.tokensIn += prompt;
this.tokensOut += Number(c.usage.outputTokens) || 0;
this.lastOutput = Number(c.usage.outputTokens) || 0;
this.contextTokens = prompt + (Number(c.usage.outputTokens) || 0);
out.push({ type: 'context', contextTokens: this.contextTokens, contextMax: this.window || undefined });
}
@@ -333,7 +337,9 @@ export class TurnProjector {
const args = parseArguments(d.arguments);
const card = { ...presentTool({ callId: d.callId, name: d.name, args }), state: 'running' };
this.tools.set(d.callId, { card, calledAt: e.time });
const draft = this.composing.get(d.callId);
this.composing.delete(d.callId);
if (draft && draft.bytes > 0 && !NO_CARD.has(d.name)) out.push(draft.frame({ done: true }));
this.running.push(d.callId);
this.#status(toolUpdate('tool', 'tool', d.name, args, {}, { args: { tool: d.name, verb: card.verb, target: card.target } }));
out.push({ type: 'agent-tool', tool: card });
@@ -419,8 +425,29 @@ export class TurnProjector {
return this.#card(callId, { state: 'denied', result: outcome === 'rejected' ? 'Denied. Nothing ran.' : outcome === 'cancelled' ? 'Cancelled. Nothing ran.' : 'No one could answer this request, so nothing ran.' });
}
/** A reply the model could not finish (the turn ended on the length limit): what it was writing, so nothing is lost without a word. */
cut() {
return { tokens: this.lastOutput, calls: cutCalls(this.composing.values()) };
}
/** The agent was asked to carry on: the calls it lost are not pending any more. */
clearCut() { const frames = this.#dropDrafts(); this.ended = null; return frames; }
/** Frames that take the writing cards of calls that will never be whole off the page (the reply was cut, or the turn ended). */
#dropDrafts() {
const frames = [...this.composing.values()].filter(d => d.bytes > 0 && !NO_CARD.has(d.name)).map(d => d.frame({ done: true, lost: true }));
this.composing.clear();
return frames;
}
/** The terminal frames for a finished turn. */
finish({ model, contextMax, engine = null } = {}) {
finish(options = {}) {
const cutInfo = this.ended?.kind === 'max-tokens' ? this.cut() : null;
const dropped = this.#dropDrafts();
return [...dropped, ...this.#finished(options, cutInfo)];
}
#finished({ model, contextMax, engine = null } = {}, cutInfo = null) {
const reason = this.ended ?? { kind: 'interrupted' };
const durationMs = Date.now() - this.startedAt;
const usage = this.usageSeen
@@ -441,7 +468,7 @@ export class TurnProjector {
case 'completed':
return [{ type: 'done', ...base, ...usage, finishReason: 'stop' }];
case 'max-tokens':
return [{ type: 'done', ...base, ...usage, finishReason: 'length' }];
return [{ type: 'done', ...base, ...usage, finishReason: 'length', cut: cutInfo }];
case 'aborted':
return [{ type: 'done', ...base, ...usage, finishReason: 'aborted', cancelled: true }];
case 'error':
@@ -16,9 +16,13 @@
* request the Harness marks `purpose: 'compaction'` is touched in `llm.stream`. Nothing in the Harness is rewritten: a Harness update
* that renames either leaves compaction as it was, and the test that runs the real Harness fails.
*/
import { densityOf, estimateMessage, estimateTools, isContextOverflow, MAX_DENSITY, pressureNow, promptTokensOf, splitMessages, summaryBudget, usageOfEvent, WINDOW_SLACK } from '../context-budget.mjs';
import { replyCap } from '../reply-cut.mjs';
import { densityOf, estimateMessage, estimateTools, isContextOverflow, MAX_DENSITY, pressureNow, promptTokensOf, splitMessages, summaryBudget, usageOfEvent, WINDOW_SLACK, withSummaryLanguage } from '../context-budget.mjs';
export const name = 'strata-context';
/** The routes the host writes for models on this computer (profile.mjs): `strata-local`, `strata-local-<id>`, and their `~s<budget>` copies. */
const isLocalRoute = provider => typeof provider === 'string' && provider.startsWith('strata-local');
export const inject = ['tokenMeter', 'llm'];
/** Real prompt measurements per session: { real, estimated } for the last answer the model server reported. */
@@ -74,6 +78,47 @@ export function apply(ctx) {
};
}
// 3. A reply that has room to finish (AGENT-LOST, 11-01). The output budget in the profile is what the Harness's compaction sets aside for a
// reply (30 percent of the window; it reads it from the request header), and the same number was also the ceiling of every request: a thinking
// model that wrote a 15K-token file after 9K tokens of thinking was cut inside the tool call at 19,660, the unfinished call was dropped and the
// turn ended with no file and no word. The header (and so compaction) stays as it is; the request that goes to the model server may produce up
// to half the window, at most 64K tokens (replyCap), and pi-ai still clamps it to what the window has left after the prompt. The agent loop
// dispatches through the adapter's `streamWithSnapshot`, after the loop has checked the request against its header, so that is where the ceiling
// is raised, once per adapter, the first time a local route is prepared. A Harness that renames it leaves the ceiling as it was (the test that
// runs the real Harness fails).
if (llm && typeof llm.prepareCall === 'function' && !wrapped.has(llm.prepareCall)) {
const original = llm.prepareCall;
const raised = new WeakSet();
const prepare = async function prepareCall(config, signal, configure) {
try { raiseReplyCeiling(config?.provider, raised); } catch (e) { log('warn', `could not raise the reply ceiling: ${e.message}`); }
return original.call(this, config, signal, configure);
};
wrapped.add(prepare);
llm.prepareCall = prepare;
}
function raiseReplyCeiling(provider, raised) {
if (!isLocalRoute(provider)) return;
const adapter = llm.adapters?.get?.(provider)?.adapter;
if (!adapter || raised.has(adapter) || typeof adapter.streamWithSnapshot !== 'function' || typeof adapter.modelOf !== 'function') return;
raised.add(adapter);
const stream = adapter.streamWithSnapshot;
adapter.streamWithSnapshot = function streamWithSnapshot(options, snapshot) {
let next = options;
try {
if (isLocalRoute(options?.provider)) {
const model = this.modelOf(snapshot, options.provider, options.model);
const asked = options.maxTokens ?? model.maxTokens ?? 0;
// Only the agent's own reply budget is raised: a summary or a title asks for far less and keeps what it asked.
if (model.contextWindow > 0 && asked >= Math.floor(model.contextWindow * 0.25)) {
const cap = replyCap(model.contextWindow, asked);
if (cap > asked) next = { ...options, maxTokens: cap };
}
}
} catch { next = options; }
return stream.call(this, next, snapshot);
};
}
// The host asks, with /strata-context, how much of the window the chat holds right now: the same measured number compaction decides on
// (JSON in the command's text: { tokens, window }). Before a message goes out, and after /compact, so the person is told true figures.
ctx.inject(['commands'], (commandCtx) => {
@@ -108,6 +153,7 @@ export function apply(ctx) {
const refusedForSize = finish => finish?.kind === 'error' && isContextOverflow(finish.failure ?? finish);
async function* summaryStream(options, original) {
options = { ...options, messages: withSummaryLanguage(options.messages) }; // #4750: the summary follows the conversation's language
let window = 0;
try { window = (await llm.resolveModelInfo(options.provider, options.model, options.signal))?.context?.contextWindow ?? 0; } catch { window = 0; }
const messages = options.messages ?? [];
+203 -33
View File
@@ -48,25 +48,196 @@ export const READ_ONLY_TOOLS = new Set([
const SHELL_TOOLS = new Set(['pwsh', 'bash']);
/** Commands that only look. Matched against the first word of every pipeline segment. */
const SAFE_COMMANDS = new Set([
// PowerShell
'get-childitem', 'gci', 'ls', 'dir', 'get-content', 'gc', 'cat', 'type', 'get-item', 'gi', 'get-location', 'pwd', 'gl',
'select-string', 'sls', 'measure-object', 'measure', 'select-object', 'select', 'where-object', 'where', 'sort-object', 'sort',
'format-table', 'ft', 'format-list', 'fl', 'out-string', 'test-path', 'resolve-path', 'get-filehash', 'get-date', 'get-command',
'get-process', 'get-itemproperty', 'write-output', 'echo', 'hostname', 'whoami', 'get-host', 'get-psdrive', 'get-help',
'group-object', 'group', 'convertto-json', 'convertfrom-json', 'select-xml', 'compare-object', 'get-unique', 'tee-object-none',
// POSIX
'head', 'tail', 'wc', 'grep', 'rg', 'egrep', 'fgrep', 'stat', 'file', 'du', 'df', 'uname', 'which', 'basename', 'dirname',
'realpath', 'readlink', 'sort', 'uniq', 'cut', 'tr', 'less', 'more', 'date', 'id', 'env-none', 'tree',
]);
/**
* Which shell commands run without a card in Manual mode, and with which options. Safe by construction (#3975): a command is on the list WITH the
* options that are known to only look. Every other option, long or short, abbreviated or attached, makes the command ask. So a write mode
* (`sort -o`, `tree -o`, `uniq in out`), a program-execution mode (`sort --compress-program`, `rg --pre`, `git grep -O`), a setting mode
* (`date -s`) or one that never ends (`tail -f`) cannot slip in, and a word added to the list later starts with no options at all.
*
* A spec says what one command accepts:
* f short options without a value (letters, so `-la` is `-l -a`) v short options that need a value (`-k2`, `-k 2`)
* vn the same, and the value must be a number o short options with an optional value that must be attached (`-Iseconds`)
* n a bare `-5` is allowed l long options (`--name`, `--name=value`), spelled in full
* lv long options that need a value (`--name=value`, `--name value`) ps PowerShell parameter names, lower case, spelled in full
* max how many operands (files, patterns) are allowed op a check for each operand
* post a last check from the options seen and the operand count
*/
const words = (text) => (text ? text.trim().split(/\s+/) : []);
const spec = ({ f = '', v = '', vn = '', o = '', n = false, l = '', lv = '', ps = '', max = Infinity, op = null, post = null }) =>
({ f, v, vn, o, n, l: new Set(words(l)), lv: new Set(words(lv)), ps: ps ? new Set([...words(ps), ...PS_COMMON]) : null, max, op, post });
/** Parameters every cmdlet has that only change how errors and warnings show. (-Debug asks a question and -OutVariable sets one: not listed.) */
const PS_COMMON = ['erroraction', 'ea', 'warningaction', 'wa', 'informationaction', 'ia', 'verbose', 'vb'];
const NUMBER = /^[+-]?\d+[A-Za-z]{0,3}$/;
/** What `ls`, `dir`, `gci` and `Get-ChildItem` take. */
const LS = { f: 'aAbBcdDFgGhHiklLmnNopqQrRsStuUvxX1CZ', v: 'ITw', ps: 'path literalpath filter include exclude recurse depth name force file directory hidden readonly system attributes',
l: 'all almost-all human-readable recursive directory classify reverse size inode dereference color group-directories-first sort time time-style block-size indicator-style si numeric-uid-gid no-group literal hide ignore quote-name escape author file-type full-time width tabsize' };
/** What `cat`, `type`, `gc` and `Get-Content` take. (-Wait never ends, so it is not here.) */
const CAT = { f: 'AbeEnstTuv', ps: 'path literalpath totalcount tail head raw encoding readcount force filter include exclude delimiter stream asbytestream',
l: 'show-all number-nonblank show-ends number squeeze-blank show-tabs show-nonprinting' };
const MEASURE = { ps: 'property sum average maximum minimum line word character ignorewhitespace inputobject standarddeviation allstats' };
const SELECT = { ps: 'first last skip skiplast property expandproperty unique index excludeproperty inputobject' };
const WHERE = { ps: 'property value inputobject eq ne gt lt ge le like notlike match notmatch contains notcontains in notin is isnot ceq cne cgt clt cge cle clike cnotlike cmatch cnotmatch ccontains cnotcontains cin cnotin' };
const GROUP = { ps: 'property noelement ashashtable asstring casesensitive inputobject culture' };
const FT = { ps: 'property autosize wrap hidetableheaders groupby view force expand repeatheader displayerror showerror inputobject' };
const FL = { ps: 'property view groupby expand force displayerror showerror inputobject' };
const SLS = { ps: 'pattern path literalpath simplematch casesensitive context list notmatch quiet allmatches include exclude encoding raw inputobject culture noemphasis' };
const GETITEM = { ps: 'path literalpath name filter include exclude force stream' };
const GETLOC = { ps: 'psprovider psdrive stack stackname' };
const ECHO = { f: 'neE', ps: 'inputobject noenumerate' };
/** The GNU/BSD grep family. */
const GREP = { f: 'aiIlLcnvwHhEFGPqrRsTUyzZbxo', v: 'efdD', vn: 'ABCm', n: true,
l: 'extended-regexp fixed-strings basic-regexp perl-regexp ignore-case no-ignore-case word-regexp line-regexp invert-match count color colour files-with-matches files-without-match max-count only-matching quiet silent no-messages byte-offset with-filename no-filename label line-buffered initial-tab null after-context before-context context text recursive dereference-recursive include exclude exclude-dir binary-files line-number null-data no-group-separator group-separator regexp file directories devices binary' };
/** Commands that only look, each with the options that are known to be harmless. Matched against the first word of every pipeline segment. */
const SPECS = new Map();
const add = (names, s) => { for (const name of words(names)) SPECS.set(name, spec(s)); };
// PowerShell
add('get-childitem gci', { ps: LS.ps });
add('ls dir', LS);
add('get-content gc', { ps: CAT.ps });
add('cat type', CAT);
add('get-item gi get-itemproperty', GETITEM);
add('get-location gl', GETLOC);
add('pwd', { ...GETLOC, f: 'LP' });
add('select-string sls', SLS);
add('measure-object measure', MEASURE);
add('select-object select', SELECT);
add('where-object where', WHERE);
add('sort-object', { ps: 'property descending unique top bottom casesensitive culture stable inputobject' });
add('format-table ft', FT);
add('format-list fl', FL);
add('out-string', { ps: 'stream width inputobject' });
add('test-path', { ps: 'path literalpath pathtype isvalid filter include exclude olderthan newerthan' });
add('resolve-path', { ps: 'path literalpath relative relativebasepath force' });
add('get-filehash', { ps: 'path literalpath algorithm' });
add('get-date', { ps: 'format uformat date asutc displayhint year month day hour minute second millisecond' });
add('get-command', { ps: 'name commandtype module all syntax listimported totalcount noun verb fullyqualifiedmodule parametername parametertype' });
add('get-process', { ps: 'name id includeusername module fileversioninfo inputobject' });
add('write-output', { ps: ECHO.ps });
add('echo', ECHO);
add('get-host', { ps: '' });
add('get-psdrive', { ps: 'name psprovider scope literalname' });
add('get-help', { ps: 'name full detailed examples parameter category component functionality role' });
add('group-object group', GROUP);
add('convertto-json', { ps: 'depth compress enumsasstrings inputobject asarray escapehandling' });
add('convertfrom-json', { ps: 'ashashtable depth inputobject noenumerate' });
add('select-xml', { ps: 'xml path literalpath xpath content namespace' });
add('compare-object', { ps: 'referenceobject differenceobject property includeequal excludedifferent passthru syncwindow casesensitive culture' });
add('get-unique', { ps: 'inputobject asstring caseinsensitive onetype' });
// POSIX. `sort` and `date` take no PowerShell names here: `sort -o FILE` writes, `date -s` sets the clock, and a PowerShell-looking `-Second` or `-Top`
// must not be read by a real sort or date as one of those. (Sort-Object and Get-Date above are the PowerShell forms.)
add('head', { f: 'qvz', vn: 'nc', n: true, l: 'quiet silent verbose zero-terminated lines bytes' });
add('tail', { f: 'qvz', vn: 'nc', n: true, l: 'quiet silent verbose zero-terminated lines bytes' }); // no -f, -F, --follow, --retry, --pid, -s: those never end
add('wc', { f: 'lwcmL', l: 'lines words bytes chars max-line-length' });
add('grep egrep fgrep', GREP);
add('rg', { // no --pre, --pre-glob, --hostname-bin (they run a program) and no -z/--search-zip (it runs decompressors)
f: 'iIsSnNHlcwFxvUoLaquhVpbP', v: 'efgtTr', vn: 'ABCjdMm',
l: 'ignore-case no-ignore-case smart-case case-sensitive fixed-strings word-regexp line-regexp invert-match count count-matches files-with-matches files-without-match files type-list hidden no-ignore no-ignore-vcs no-ignore-parent no-ignore-dot no-ignore-global no-ignore-messages follow line-number no-line-number with-filename no-filename column heading no-heading null only-matching quiet text multiline multiline-dotall pcre2 no-messages json stats trim unrestricted binary no-config vimgrep pretty byte-offset passthru crlf one-file-system no-unicode regexp file glob iglob type type-not max-count max-depth maxdepth context after-context before-context color replace threads sort sortr max-filesize max-columns path-separator encoding engine ignore-file glob-case-insensitive no-glob-case-insensitive' });
add('stat', { f: 'LtxlnqrsFf', v: 'c', l: 'dereference file-system terse format printf' });
add('file', { f: 'bhikLnNrvI', l: 'brief dereference no-dereference mime mime-type mime-encoding keep-going no-pad no-buffer raw' }); // no -C, -m, -f, -P (they write or read other files), no -z/-s/-p (they run decompressors, read devices, reset times)
add('du', { f: 'abchHklmsxLPSD0', v: 'tB', vn: 'd', l: 'all apparent-size bytes total dereference-args human-readable si summarize one-file-system dereference no-dereference separate-dirs count-links inodes null max-depth threshold block-size time time-style exclude' });
add('df', { f: 'ahHiklmPTv', v: 'txB', l: 'all human-readable si inodes local portability print-type total sync no-sync output block-size type exclude-type' });
add('uname', { f: 'asnrvmpio', max: 0, l: 'all kernel-name nodename kernel-release kernel-version machine processor hardware-platform operating-system' });
add('which', { f: 'as', l: 'all' });
add('basename', { f: 'az', v: 's', l: 'multiple zero suffix' });
add('dirname', { f: 'z', l: 'zero' });
add('realpath', { f: 'emsqzLP', l: 'canonicalize-existing canonicalize-missing no-symlinks quiet zero strip logical physical relative-to relative-base' });
add('readlink', { f: 'femnqsvz', l: 'canonicalize canonicalize-existing canonicalize-missing no-newline quiet silent verbose zero' });
add('sort', { f: 'bdfghiMnRrsuVzmcC', v: 'kt', l: 'ignore-leading-blanks dictionary-order ignore-case general-numeric-sort human-numeric-sort ignore-nonprinting month-sort numeric-sort random-sort reverse stable unique version-sort zero-terminated check merge sort field-separator key' });
// uniq's second operand is its OUTPUT file, so it takes at most one.
add('uniq', { f: 'cdDuiz', vn: 'fsw', max: 1, l: 'count repeated all-repeated unique ignore-case skip-fields skip-chars check-chars zero-terminated group' });
add('cut', { f: 'snz', v: 'bcfd', l: 'complement only-delimited zero-terminated bytes characters fields delimiter output-delimiter' });
add('tr', { f: 'cCdst', max: 2 });
add('date', { f: 'uR', o: 'I', v: 'dr', lv: 'date reference', l: 'utc universal rfc-email rfc-2822 iso-8601 rfc-3339',
op: (word) => word.startsWith('+') }); // a bare operand like 1026120026 SETS the clock; only +FORMAT is a format
add('id', { f: 'ugGnrazZ', l: 'user group groups name real context zero' });
add('hostname', { f: 'sfiIdaAyv', max: 0 }); // `hostname NAME` renames the machine
add('whoami', { l: 'help version', op: (word) => /^\/(all|user|groups|priv|upn|fqdn|logonid|nh)$/i.test(word) });
add('tree', { f: 'adfilpsugDFqNQrtvxCnASUchJX', v: 'LPIHT', vn: '', l: 'help version filelimit charset sort timefmt noreport dirsfirst filesfirst prune du si gitignore matchdirs ignore-case inodes device nolinks' }); // no -o, no -R
// `less` and `more` are not here: they wait for keys, and `less` can run commands.
/** The names that run without a card (for the tests). */
export const readOnlyCommandNames = () => [...SPECS.keys()];
/** git subcommands that only read, with the options they may take. */
const GIT_VIEW = 'oneline graph decorate no-decorate all branches tags remotes stat shortstat numstat dirstat name-only name-status patch no-patch raw summary compact-summary abbrev-commit no-abbrev-commit abbrev reverse follow no-merges merges first-parent full-history simplify-by-decoration simplify-merges ancestry-path left-right left-only right-only cherry-pick cherry-mark cherry topo-order date-order author-date-order relative-date all-match invert-grep regexp-ignore-case extended-regexp fixed-strings perl-regexp no-color color color-words word-diff word-diff-regex minimal patience histogram diff-algorithm check exit-code quiet cached staged merge-base no-index no-ext-diff no-textconv find-renames find-copies find-copies-harder no-renames ignore-space-change ignore-all-space ignore-space-at-eol ignore-blank-lines ignore-cr-at-eol function-context show-function unified inter-hunk-context src-prefix dst-prefix no-prefix full-index binary text source use-mailmap mailmap no-mailmap log-size notes no-notes show-signature diff-merges boundary children parents no-walk sparse date format pretty author committer grep since until after before max-count skip min-parents max-parents diff-filter encoding stat-width stat-name-width stat-graph-width stat-count count';
const LISTING = ['l', 'list', 'contains', 'no-contains', 'merged', 'no-merged', 'points-at'];
const GIT = new Map(Object.entries({
status: spec({ f: 'sbvz', o: 'u', l: 'short branch porcelain long ignored untracked-files ignore-submodules renames no-renames find-renames show-stash ahead-behind no-ahead-behind column no-column verbose null' }),
log: spec({ f: 'pswbMCizuacmrtqEFPR', vn: 'nU', v: 'SGL', n: true, l: GIT_VIEW }),
diff: spec({ f: 'pswbMCizuacmrtqEFPR', vn: 'nU', v: 'SGL', n: true, l: GIT_VIEW }),
show: spec({ f: 'pswbMCizuacmrtqEFPR', vn: 'nU', v: 'SGL', n: true, l: GIT_VIEW }),
'rev-list': spec({ f: 'pswbMCizuacmrtqEFPR', vn: 'nU', v: 'SGL', n: true, l: GIT_VIEW }),
shortlog: spec({ f: 'sneciw', n: false, l: `${GIT_VIEW} summary numbered email committer group` }),
// Listing forms only. A name with no list option would CREATE a branch or a tag, and the options that change things are not here.
branch: spec({ f: 'avri', l: 'list all remotes verbose show-current ignore-case contains no-contains merged no-merged points-at sort format abbrev no-abbrev color no-color column no-column omit-empty',
post: (seen, count) => count === 0 || LISTING.some(name => seen.has(name)) }),
tag: spec({ f: 'li', o: 'n', l: 'list sort contains no-contains merged no-merged points-at format column no-column ignore-case omit-empty color no-color',
post: (seen, count) => count === 0 || LISTING.some(name => seen.has(name)) }),
remote: spec({ f: 'v', l: 'verbose', max: 0 }),
// `git config KEY` reads and `git config KEY VALUE` writes, so a key is only taken with --get; no --edit, --add, --unset, --file.
config: spec({ f: 'lz', l: 'list get get-all get-regexp show-origin show-scope local global system worktree bool int bool-or-int path expiry-date type null name-only includes no-includes', lv: 'default',
post: (seen, count) => (count > 0 ? ['get', 'get-all', 'get-regexp'].some(name => seen.has(name)) : seen.has('list') || seen.has('l')) }),
'rev-parse': spec({ f: 'q', l: 'show-toplevel git-dir git-common-dir absolute-git-dir abbrev-ref symbolic symbolic-full-name short verify is-inside-work-tree is-inside-git-dir is-bare-repository is-shallow-repository show-prefix show-cdup show-superproject-working-tree all branches tags remotes quiet revs-only no-revs flags no-flags local-env-vars show-object-format path-format', lv: 'default git-path' }),
'ls-files': spec({ f: 'cdmoiskuzvtfD', l: 'cached deleted modified others ignored stage directory unmerged killed no-empty-directory error-unmatch exclude-standard exclude full-name eol deduplicate recurse-submodules sparse abbrev resolve-undo format' }),
'ls-tree': spec({ f: 'rtdlz', l: 'name-only name-status long full-name full-tree object-only abbrev format' }),
blame: spec({ f: 'wMCespltbfnc', v: 'L', l: 'porcelain line-porcelain incremental show-email show-name show-number date root abbrev' }),
describe: spec({ l: 'all tags always long dirty broken contains abbrev match exclude first-parent exact-match candidates' }),
'cat-file': spec({ f: 'tsep', l: 'batch batch-check batch-all-objects allow-unknown-type buffer unordered follow-symlinks mailmap use-mailmap' }), // no --textconv, --filters
// No -O / --open-files-in-pager: they run a program on the matches.
grep: spec({ f: 'aiIlLcnvwHhEFGPqpWrzUo', v: 'e', vn: 'ABCm', n: true, l: 'no-index untracked cached no-exclude-standard exclude-standard recurse-submodules text ignore-case word-regexp invert-match full-name extended-regexp basic-regexp perl-regexp fixed-strings line-number column files-with-matches name-only files-without-match count null only-matching quiet no-color color break heading show-function function-context all-match and or not max-depth max-count threads after-context before-context context recursive no-recursive' }),
}));
/**
* One command's words against its spec: every option has to be one the spec knows (anything else is not understood, so it asks).
* @param {ReturnType<typeof spec>} sp
* @param {{t:string,q:boolean}[]} args the words after the command; q marks a quoted text, which is data and never an option
*/
function argumentsAreSafe(sp, args) {
const seen = new Set();
let operands = 0, ended = false;
const operand = (arg) => { operands++; return operands <= sp.max && (!sp.op || sp.op(arg.t, arg.q)); };
const valueOf = (i, numeric) => { // the next word as an option's value; it can be a quoted text, a number, or a word that is not an option
const arg = args[i + 1];
if (!arg) return false;
return numeric ? NUMBER.test(arg.t) : arg.q || !arg.t.startsWith('-') || NUMBER.test(arg.t);
};
for (let i = 0; i < args.length; i++) {
const { t, q } = args[i];
if (q || ended || t === '-' || t[0] !== '-') { if (!operand(args[i])) return false; continue; }
if (t === '--') { ended = true; continue; }
if (t.startsWith('--')) {
const eq = t.indexOf('=');
const name = eq < 0 ? t.slice(2) : t.slice(2, eq);
if (sp.l.has(name)) { seen.add(name); continue; }
if (sp.lv.has(name)) { seen.add(name); if (eq < 0) { if (!valueOf(i, false)) return false; i++; } continue; }
return false;
}
if (sp.ps) {
const named = /^-([A-Za-z][A-Za-z0-9]*)(?::|$)/.exec(t);
if (named && sp.ps.has(named[1].toLowerCase())) { seen.add(named[1].toLowerCase()); continue; }
}
if (sp.n && /^-\d+$/.test(t)) continue;
for (let j = 1; j < t.length; j++) {
const c = t[j];
if (sp.f.includes(c)) { seen.add(c); continue; }
if (sp.o.includes(c)) { seen.add(c); break; }
const numeric = sp.vn.includes(c);
if (!numeric && !sp.v.includes(c)) return false;
seen.add(c);
const rest = t.slice(j + 1);
if (rest) { if (numeric && !NUMBER.test(rest)) return false; } else { if (!valueOf(i, numeric)) return false; i++; }
break;
}
}
return sp.post ? sp.post(seen, operands) : true;
}
/** git subcommands that only read. */
const SAFE_GIT = new Set(['status', 'log', 'diff', 'show', 'branch', 'rev-parse', 'ls-files', 'blame', 'describe', 'remote', 'tag', 'shortlog', 'rev-list', 'cat-file', 'grep', 'ls-tree', 'config']);
const SAFE_VERSION = /^(node|python|python3|py|npm|pnpm|git|dotnet|cargo|rustc|go|java|pip|pip3)\s+(--version|-v|-version|version)$/i;
/** Anything that can chain, redirect, expand or start something else is not read-only. */
const UNSAFE_SYNTAX = /[;&<>`${}()]|\r|\n|--%|\bout-file\b|\bset-content\b|\badd-content\b|\binvoke-|\biex\b|\bstart-|\bnew-|\bremove-|\bcopy-|\bmove-|\brename-|\bclear-|\bset-|\bpush-|\bexpand-|\bsave-|\binstall-|\bcurl\b|\bwget\b|\bsudo\b|\bsh\b|\bbash\b|\bpwsh\b|\bpowershell\b|\bcmd\b|-exec\b|-delete\b|-execdir\b/i;
/** Anything that can chain, redirect, expand or start something else is not read-only. (`sh` as a word, not inside an option like `du -sh`.) */
const UNSAFE_SYNTAX = /[;&<>`${}()]|\r|\n|--%|\bout-file\b|\bset-content\b|\badd-content\b|\binvoke-|\biex\b|\bstart-|\bnew-|\bremove-|\bcopy-|\bmove-|\brename-|\bclear-|\bset-|\bpush-|\bexpand-|\bsave-|\binstall-|\bcurl\b|\bwget\b|\bsudo\b|(?<![-\w])sh(?![-\w])|\bbash\b|\bpwsh\b|\bpowershell\b|\bcmd\b|-exec\b|-delete\b|-execdir\b/i;
/**
* The command with every quoted text replaced by a plain stand-in, or null when it cannot be read safely. What is inside quotes is data
@@ -74,9 +245,10 @@ const UNSAFE_SYNTAX = /[;&<>`${}()]|\r|\n|--%|\bout-file\b|\bset-content\b|\badd
* inside double quotes a $ or a backtick expands (a variable, a sub-command), and an unterminated quote means the text was not understood.
* Either one gives null, and the command asks as before.
* @param {string} text
* @param {string[]} [quoted] when given, receives the text inside each quote, in order (the checks on operands read it)
* @returns {string|null}
*/
export function maskQuotes(text) {
export function maskQuotes(text, quoted) {
let out = '';
for (let i = 0; i < text.length; i++) {
const c = text[i];
@@ -91,13 +263,14 @@ export function maskQuotes(text) {
const before = text[i - 1], after = text[end + 1];
if ((before !== undefined && !/[\s|]/.test(before)) || (after !== undefined && !/[\s|]/.test(after)) || text[i + 1] === '-') return null;
out += '"_"';
quoted?.push(text.slice(i + 1, end));
i = end;
}
return out;
}
/**
* Whether a shell command only reads. Conservative by design: one unrecognised word and the answer is no.
* Whether a shell command only reads. Conservative by design: one unrecognised word or option and the answer is no.
* Quoted text is data and is looked at after it has been set aside (maskQuotes): Get-ChildItem "C:\Users\x\Q&A" only lists.
* @param {string} command
* @returns {boolean}
@@ -106,24 +279,21 @@ export function isReadOnlyShell(command) {
const text = String(command ?? '').trim();
if (!text || text.length > 600) return false;
if (SAFE_VERSION.test(text)) return true;
const masked = maskQuotes(text);
const quoted = [];
const masked = maskQuotes(text, quoted);
if (masked === null || UNSAFE_SYNTAX.test(masked)) return false;
const segments = masked.split('|').map(s => s.trim());
return segments.every((segment) => {
if (!segment) return false;
const words = segment.split(/\s+/);
const first = words[0].toLowerCase().replace(/^\.\\/, '');
let next = 0;
const tokens = masked.split('|').map(segment => segment.trim()).map(segment => words(segment).map(w => (w === '"_"' ? { t: quoted[next++] ?? '', q: true } : { t: w, q: false })));
return tokens.every((segment) => {
if (!segment.length || segment[0].q) return false;
const first = segment[0].t.toLowerCase().replace(/^\.\\/, '');
if (first === 'git') {
// `git config` and `git tag`/`branch`/`remote` can write when given a value; only the bare listing forms are safe.
const sub = (words[1] ?? '').toLowerCase();
if (!SAFE_GIT.has(sub)) return false;
if (['config', 'tag', 'branch', 'remote'].includes(sub)) {
const rest = words.slice(2).filter(w => !w.startsWith('-') || /^(-l|-a|-v|-vv|--list|--all|--show-current|--get|--get-all)$/i.test(w));
return rest.length === 0 || (sub === 'config' && words[2] === '--get') || (sub === 'remote' && words[2] === '-v');
}
return !words.some(w => /^--(output|ext-diff|textconv)\b/i.test(w));
const sub = segment[1];
const sp = sub && !sub.q ? GIT.get(sub.t) : null;
return !!sp && argumentsAreSafe(sp, segment.slice(2));
}
return SAFE_COMMANDS.has(first) && !words.some(w => /^-(delete|exec|execdir|fprint|fls|ok)$/i.test(w));
const sp = SPECS.get(first);
return !!sp && argumentsAreSafe(sp, segment.slice(1));
});
}
+23 -4
View File
@@ -11,8 +11,8 @@
* reminder tools would run behind its back.
* - `web_search` removed (tool-web with `search:false`): every search backend the Harness has needs a service key
* the app does not have, so the tool could only fail. `web_fetch` stays. (`searchProvider` can switch it back on.)
* - `tool-ask-user` removed: the app has no place to show the agent's own questions, so the tool could only fail;
* the agent asks in plain text instead.
* - `tool-ask-user` stays (it is the group "Questions for you" in Settings -> Tools): the app shows the agent's own questions in a card above
* the message box and sends the answer back (#4244). Switched off, the tool is not offered and the agent asks in plain text.
* - the `planning` group (Harness plan mode and `exit_plan_mode`) removed: the app's Plan mode is the permission
* preset (nothing can change); the Harness's plan mode needs an interactive review the app does not have.
* - `tool-bash` / `tool-pwsh`: exactly one, by operating system (the Harness chooses the same at load).
@@ -29,7 +29,7 @@ export const STANDARD_PLUGIN_IDS = [
'tool-ask-user', 'tool-todo', 'tool-web', 'present', 'tool-plugin-manager',
];
/** What we leave out of it. */
export const REMOVED_PLUGIN_IDS = ['tool-schedule', 'tool-ask-user', 'planning'];
export const REMOVED_PLUGIN_IDS = ['tool-schedule', 'planning'];
/** Plugins removed from inside the `delegation` group (the group stays: helper agents are a feature). */
export const REMOVED_DELEGATION_IDS = [];
@@ -48,6 +48,24 @@ export const AGENT_PROMPT = [
'Tokens prefixed with @ are files or folders the person pointed at. A failed or refused action changed nothing; say so, and do not claim work you did not do.',
].join('\n\n');
/**
* The one sentence that says which computer this is. The Harness's own runtime context is off (see `includeRuntimeContext` below), so without it
* a model on Windows spent its first steps on `ls`, `uname` and `wc` before trying PowerShell commands (and some ran out of steps first).
*/
export const OS_LINES = {
win32: 'This computer runs Windows and the shell tool is PowerShell: uname, wc and grep do not exist here; use Get-ChildItem, Get-Content, Select-String and Measure-Object. There is no need to check which system this is.',
darwin: 'This computer runs macOS and the shell tool is a Unix shell. There is no need to check which system this is.',
linux: 'This computer runs Linux and the shell tool is a Unix shell. There is no need to check which system this is.',
};
/** AGENT_PROMPT with the sentence for `platform` after its first line. */
export function agentPromptFor(platform = process.platform) {
const line = OS_LINES[platform];
if (!line) return AGENT_PROMPT;
const [first, ...rest] = AGENT_PROMPT.split('\n\n');
return [first, line, ...rest].join('\n\n');
}
/**
* @param {{platform?: string, search?: boolean}} [options]
* @returns {object} the `config` of the `preset-standard` row
@@ -55,7 +73,7 @@ export const AGENT_PROMPT = [
export function standardPreset({ platform = process.platform, search = false, off = new Set(), skillsDir = null, compaction = [] } = {}) {
const windows = platform === 'win32';
const plugins = [
{ id: 'persona', name: '@deepseek-ai/dsh-persona', config: { prefix: AGENT_PROMPT, complete: true, includeRuntimeContext: false } },
{ id: 'persona', name: '@deepseek-ai/dsh-persona', config: { prefix: agentPromptFor(platform), complete: true, includeRuntimeContext: false } },
{ id: 'agent-instructions', name: '@deepseek-ai/dsh-agent-instructions', config: { maxBytes: 65536 } },
{ id: 'time-context', name: '@deepseek-ai/dsh-time-context' },
windows
@@ -88,6 +106,7 @@ export function standardPreset({ platform = process.platform, search = false, of
{ id: 'tool-workflow', name: '@deepseek-ai/dsh-tool-workflow' },
],
},
{ id: 'tool-ask-user', name: '@deepseek-ai/dsh-tool-ask-user' },
{ id: 'tool-todo', name: '@deepseek-ai/dsh-tool-todo', config: { allowParallelInProgress: true } },
{ id: 'tool-web', name: '@deepseek-ai/dsh-tool-web', config: { fetch: !off.has('fetch'), search: search && !off.has('search'), searchTimeoutMs: 60000 } },
{ id: 'present', name: '@deepseek-ai/dsh-tool-present' },
+123
View File
@@ -0,0 +1,123 @@
/**
* A reply the model could not finish (AGENT-LOST, 11-01): pure helpers, no I/O.
*
* A model reply has a ceiling (the "output budget"). A thinking model that then writes a whole file in one tool call can spend all of it
* before the call is closed. The Harness drops a tool call that was not closed (its arguments are half a JSON text), the turn ends with
* the reason `max-tokens`, and until now the person saw nothing: no file, no card, no sentence. Here:
*
* - `replyCap(window)` how much one reply may produce (the request's ceiling), much larger than the budget the Harness reserves
* for compaction (profile.mjs `localOutputBudget`): the Harness still clamps it to what the window has left.
* - `ToolDraft` what a tool call that is being written looks like so far: its name, the file it is for, a size, a tail of the text.
* - `cutNotice(...)` the plain sentence for the chat, and `cutNudge(...)` the message that lets the agent carry on in smaller pieces.
*/
/** Half the window, at most 64K tokens, and never less than the budget compaction reserves. The Harness clamps it to the room left (window minus prompt). */
export function replyCap(window, reserved = 0) {
const w = Number.isFinite(window) && window > 0 ? Math.floor(window) : 32768;
return Math.max(Math.floor(reserved) || 0, Math.min(Math.floor(w / 2), 65536));
}
const ESCAPES = { n: '\n', t: '\t', r: '', '"': '"', '\\': '\\', '/': '/', b: '', f: '' };
/** A tail of a JSON string body, read back to text: `\n` becomes a newline, `\"` a quote. A half-written escape at the end is dropped. */
export function unescapeJsonTail(raw) {
let text = String(raw ?? '');
const cut = /\\(u[0-9a-fA-F]{0,3})?$/.exec(text);
if (cut) text = text.slice(0, cut.index);
return text.replace(/\\(u[0-9a-fA-F]{4}|[nrtbf"\\/])/g, (_, e) => (e[0] === 'u' ? String.fromCharCode(parseInt(e.slice(1), 16)) : ESCAPES[e] ?? e));
}
const TARGET = /"(?:file_path|path|filePath)"\s*:\s*"((?:[^"\\]|\\.)*)"/;
const URL_ARG = /"url"\s*:\s*"((?:[^"\\]|\\.)*)"/;
/** The long text argument of the common writing tools; the tail of the arguments is previewed after the first one of these. */
const BODY = /"(?:content|new_string|new_str|text|code|body|command|file_text)"\s*:\s*"/;
/** One tool call being written: the model's streamed arguments, kept small. */
export class ToolDraft {
constructor(id, name = '') {
this.id = id; this.name = name;
this.bytes = 0; this.lines = 0;
this.head = ''; // the first 4,000 characters: the body argument usually starts here
this.where = ''; // the file (or address) the call is for, once seen: a model may write it before or after the text
this.carry = ''; // the last 300 characters, so a name split between two pieces is still seen
this.tail = ''; // the last 2,400 characters: what is being written now
this.at = 0; // when a frame was last sent for it
}
add(piece) {
const text = String(piece ?? '');
if (!text) return;
this.bytes += text.length;
for (let i = text.indexOf('\\n'); i >= 0; i = text.indexOf('\\n', i + 2)) this.lines += 1;
if (this.head.length < 4000) this.head += text.slice(0, 4000 - this.head.length);
this.tail = (this.tail + text).slice(-2400);
if (!this.where) {
const seen = this.carry + text;
const raw = (TARGET.exec(seen) ?? URL_ARG.exec(seen))?.[1];
if (raw !== undefined) { try { this.where = JSON.parse(`"${raw}"`); } catch { this.where = unescapeJsonTail(raw); } }
this.carry = seen.slice(-300);
}
}
get target() { return this.where; }
/** The text of the body so far, only its last `max` characters; empty until the body argument has started. */
preview(max = 1600) {
const start = BODY.exec(this.head);
if (!start) return '';
const bodyAt = start.index + start[0].length; // where the body begins, counted in streamed characters
if (bodyAt >= this.bytes) return '';
const tailAt = this.bytes - this.tail.length; // where the kept tail begins
const body = unescapeJsonTail(this.tail.slice(Math.max(0, bodyAt - tailAt)));
return body.length > max ? body.slice(-max) : body;
}
/** The frame for the page. */
frame({ done = false, ...extra } = {}) {
return { type: 'agent-tool-draft', draft: { id: this.id, name: this.name, target: this.target, bytes: this.bytes, lines: this.lines, preview: done ? '' : this.preview(), ...extra, done } };
}
}
const kb = n => (n >= 10_000 ? `${Math.round(n / 1000)} KB` : `${(n / 1000).toFixed(1)} KB`);
/** Calls the model was still writing when its reply was cut: [{ name, target, bytes }]. */
export function cutCalls(drafts) {
return [...drafts].filter(d => d.bytes > 0).map(d => ({ name: d.name, target: d.target, bytes: d.bytes }));
}
const withTokens = tokens => (tokens > 0 ? ` (${Number(tokens).toLocaleString('en-US')} tokens)` : '');
/** The sentence for the person, with the catalog slots it needs: { text, key, values }. `again` is true when the agent was already asked to carry on. */
export function cutNotice({ calls = [], tokens = 0, again = false, retrying = false } = {}) {
const call = calls.find(c => c.target) ?? calls[0] ?? null;
const file = call?.target ? String(call.target).split(/[\\/]/).filter(Boolean).at(-1) : '';
const what = file || (/^(write|edit)$/.test(call?.name ?? '') ? 'a file' : call?.name);
if (call && retrying) {
const key = 'That reply reached its length limit{0} while writing {1} ({2}), so that write did not happen. I asked the assistant to look at what is on disk and write the rest in smaller parts.';
const values = [withTokens(tokens), what, kb(call.bytes)];
return { key, values, text: fill(key, values) };
}
if (call) {
const key = 'The reply reached its length limit{0} while writing {1} ({2}), so that write did not happen. Say “continue” and it will look at what is on disk and go on in smaller parts.';
const values = [withTokens(tokens), what, kb(call.bytes)];
return { key, values, text: fill(key, values) };
}
if (retrying) {
const key = 'That reply reached its length limit{0} and was cut off. I asked the assistant to carry on.';
return { key, values: [withTokens(tokens)], text: fill(key, [withTokens(tokens)]) };
}
const key = 'The reply reached its length limit{0} and was cut off. Say “continue” to go on.';
return { key, values: [withTokens(tokens)], text: fill(key, [withTokens(tokens)]) };
}
export function fill(template, values) { return String(template).replace(/\{(\d+)\}/g, (_, i) => String(values[Number(i)] ?? '')); }
/** The message sent to the assistant so it carries on. It is not shown as the person's message. */
export function cutNudge({ calls = [], tokens = 0 } = {}) {
const call = calls.find(c => c.target) ?? calls[0] ?? null;
if (!call) {
return 'Your last reply was cut off by the output limit before it was finished. Carry on from where it stopped without repeating what you already wrote, and keep your thinking short.';
}
const file = call.target || 'the file';
return `Your last reply was cut off by the output limit${tokens > 0 ? ` (${Number(tokens).toLocaleString('en-US')} tokens)` : ''} while you were writing ${file} in one tool call, so that call did not run and nothing was changed by it. First look at what is on disk now (list the folder, read the end of the file). Do not delete or start over a file that is already complete. If it is not complete, write what is missing in several smaller parts (about 150 lines or fewer each) with edits or appends, and read the end of the file back when you are done. Keep your thinking short.`;
}
+1
View File
@@ -16,6 +16,7 @@ export const TOOL_GROUPS = [
{ id: 'search', area: 'web', name: 'Search the web', about: 'Lets the assistant search the web when it needs fresh information.', asks: true, web: 'search' },
{ id: 'helpers', area: 'together', name: 'Helper assistants', about: 'Lets the assistant start helpers that work on parts of a task at the same time.', asks: false, plugins: ['delegation'] },
{ id: 'goals', area: 'together', name: 'Goals', about: 'Lets you give a goal with /goal. The assistant keeps working until it is done.', asks: false, plugins: ['command-goal', 'tool-goal'] },
{ id: 'questions', area: 'together', name: 'Questions for you', about: 'Lets the assistant stop and ask you a question, with answers to pick from, when it needs your decision.', asks: false, plugins: ['tool-ask-user'] },
{ id: 'todo', area: 'together', name: 'To-do list', about: 'Lets the assistant keep a checklist while it works on a big task.', asks: false, plugins: ['tool-todo'] },
{ id: 'skills', area: 'together', name: 'Skills', about: 'Lets the assistant use your saved skills when they fit the task.', asks: false, plugins: ['skill-filesystem', 'tool-skill'] },
{ id: 'present', area: 'together', name: 'Show results', about: 'Lets the assistant show a page or file in the preview next to the chat.', asks: false, plugins: ['present'] },
+1 -1
View File
@@ -10,7 +10,7 @@
* rewrites a plan turn wrote its numbered plan from "1." again this many times
* context the context is nearly full: the Harness compaction did not keep up
*
* What the person is told is `guardNote`: fixed English sentences that are also in the interface catalog in all eight languages (the page
* What the person is told is `guardNote`: fixed English sentences that are also in the interface catalog in all nine languages (the page
* translates a `delta` frame that carries `key`). Each says what happened and gives one next step.
*/
+39
View File
@@ -0,0 +1,39 @@
/**
* The agent's own questions (the Harness's `ask_user_question` tool, #4244). The Harness raises the `user-questions/request` event with
* `{ questions: [{ id, question, detail?, header?, options?: [{ label, description? }], multiSelect?, intent? }], wait?: { callId } }` and wants
* `{ answers: [{ id, selected: string[], custom? }] }` back (AskUserQuestionRequestEvent / AskUserQuestionAnswer of the pinned Harness).
* This file only reads the first into something the page can draw, and checks the second before it goes out.
*/
const text = (v, max) => (typeof v === 'string' ? v.trim().slice(0, max) : '');
/** The questions of a request, bounded and plain; an empty list when the request carries nothing that can be shown. */
export function parseQuestions(raw) {
const out = [];
for (const q of Array.isArray(raw) ? raw.slice(0, 6) : []) {
const id = text(q?.id, 120), question = text(q?.question, 800);
if (!id || !question) continue;
const options = (Array.isArray(q.options) ? q.options.slice(0, 12) : [])
.map(o => ({ label: text(o?.label, 200), description: text(o?.description, 400) || undefined })).filter(o => o.label);
out.push({ id, question, detail: text(q.detail, 800) || undefined, header: text(q.header, 80) || undefined, options, multiSelect: q.multiSelect === true });
}
return out;
}
/**
* The answer the person gave, checked against the questions that were asked: every question answered, only the offered labels (or the person's own
* words), one choice for a single-select question. Throws a plain Error with a code on the first thing that does not fit.
*/
export function buildAnswer(questions, given) {
const byId = new Map((Array.isArray(given) ? given : []).map(a => [String(a?.id ?? ''), a]));
const answers = [];
for (const q of questions) {
const a = byId.get(q.id);
const labels = new Set(q.options.map(o => o.label));
const selected = (Array.isArray(a?.selected) ? a.selected : []).map(s => String(s)).filter(s => labels.has(s));
const custom = text(a?.custom, 2000);
if (!selected.length && !custom) throw Object.assign(new Error('Answer every question, or skip them.'), { code: 'question-incomplete' });
if (!q.multiSelect && selected.length > 1) throw Object.assign(new Error('Choose one answer for each question.'), { code: 'question-invalid' });
answers.push({ id: q.id, selected: [...new Set(selected)], ...(custom ? { custom } : {}) });
}
return { answers };
}
+10
View File
@@ -7,6 +7,16 @@ const run=(command,args)=>promisify(execFile)(command,args,{env:externalEnvironm
const extensions=new Set(['.md','.txt','.pdf','.html','.htm','.png','.jpg','.jpeg','.webp','.gif','.svg','.csv','.ts','.tsx','.js','.jsx','.py','.json','.css','.rs','.go','.sh','.mp4','.webm']);
export async function openArtifact(body,{examplesDir,previewsDir,platform=process.platform,launch=run}) {
if(!['open','reveal'].includes(body.action))throw new Error('Choose Open or Show in file explorer.');
if(typeof body.url==='string') {
// #4721: a web address goes to the person's default browser (the app window drops window.open: WebView2 has no new-window handler). http and https only, no shell.
if(body.action!=='open')throw new Error('A web address can only be opened.');
let address;try{address=new URL(body.url);}catch{throw new Error('This is not a web address.');}
if(!['http:','https:'].includes(address.protocol)||body.url.length>4096)throw new Error('Only http and https addresses can be opened.');
if(platform==='darwin')await launch('open',[address.href]);
else if(platform==='win32')await launch('rundll32.exe',['url.dll,FileProtocolHandler',address.href]);
else await launch('xdg-open',[address.href]);
return {ok:true};
}
let file;
if(typeof body.path==='string') {
// Only public example assets can be addressed by path. Never accept host paths.
+107 -23
View File
@@ -67,40 +67,70 @@ export function nvidiaDriverVersion() {
/** Where the builds are published; the wrong-make sentence points here. */
export const RELEASES_URL = 'https://github.com/Niko1221/Strata/releases';
/** The PCI vendor ids (`10de`, `1002`, `8086`) of the Windows display adapters; empty off Windows or when the registry cannot be read. */
export function windowsDisplayVendors({ platform = process.platform, run = execFileSync } = {}) {
/** The PCI vendor ids of the display adapters Windows reports as present right now (PowerShell, Win32_VideoController); empty when that cannot be read. */
export function windowsPresentVendors({ platform = process.platform, run = execFileSync } = {}) {
const found = new Set();
if (platform !== 'win32') return found;
try {
const out = run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', 'Get-CimInstance Win32_VideoController | ForEach-Object { $_.PNPDeviceID }'], { encoding: 'utf8', timeout: 15000, windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] });
for (const match of String(out).matchAll(/VEN_([0-9A-F]{4})/gi)) found.add(match[1].toLowerCase());
} catch { /* no PowerShell or a limited account: the registry answer stands */ }
return found;
}
/**
* The PCI vendor ids (`10de`, `1002`, `8086`) of the Windows display adapters; empty off Windows or when the registry cannot be read.
* The registry keeps the entry of a card that was taken out (an NVIDIA card swapped for a Radeon, #3892): when NVIDIA and AMD are both
* listed, only the makes Windows reports as present now count, the same rule Strata Control uses (engine_component.windows_display_adapters).
*/
export function windowsDisplayVendors({ platform = process.platform, run = execFileSync, present = windowsPresentVendors } = {}) {
const found = new Set();
if (platform !== 'win32') return found;
try {
const out = run('reg', ['query', 'HKLM\\SYSTEM\\CurrentControlSet\\Control\\Class\\{4d36e968-e325-11ce-bfc1-08002be10318}', '/s', '/v', 'MatchingDeviceId'], { encoding: 'utf8', timeout: 8000, windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] });
for (const match of String(out).matchAll(/VEN_([0-9A-F]{4})/gi)) found.add(match[1].toLowerCase());
} catch { /* no registry access: the tools decide */ }
if (found.has('10de') && found.has('1002')) {
const now = present({ platform, run });
const kept = new Set([...found].filter(vendor => now.has(vendor)));
if (kept.size) return kept;
}
return found;
}
const BACKEND_OF_VENDOR = [['10de', 'cuda'], ['1002', 'hip'], ['8086', 'sycl']];
/**
* Every engine family this PC's graphics cards can run, in the order NVIDIA, AMD, Intel; STRATA_ENGINE_BACKEND wins. A PC with two
* makes lists both, so a build for either make is accepted (recommend, never force).
*/
export function gpuBackendsHere(env = process.env, { vendors = windowsDisplayVendors() } = {}) {
const forced = String(env.STRATA_ENGINE_BACKEND ?? '').trim().toLowerCase();
if (forced) return ['cuda', 'hip', 'sycl'].includes(forced) ? [forced] : [];
if (vendors.size) {
const list = BACKEND_OF_VENDOR.filter(([vendor]) => vendors.has(vendor)).map(([, backend]) => backend);
if (list.length) return list;
}
if (nvidiaDriverVersion()) return ['cuda'];
try {
const base = '/sys/class/drm';
const seen = new Set();
for (const card of readdirSync(base)) {
if (!/^card\d+$/.test(card)) continue;
try { seen.add(readFileSync(`${base}/${card}/device/vendor`, 'utf8').trim().toLowerCase()); } catch { /* no vendor file */ }
}
const list = [['0x10de', 'cuda'], ['0x1002', 'hip'], ['0x8086', 'sycl']].filter(([vendor]) => seen.has(vendor)).map(([, backend]) => backend);
if (list.length) return list;
} catch { /* not Linux */ }
return [];
}
/**
* The engine family this PC's graphics card needs ('cuda', 'hip', 'sycl'), or null when it cannot be told. The same order as Control
* (NVIDIA first, then AMD, then Intel); STRATA_ENGINE_BACKEND wins. Reads only the driver and (Linux) the PCI vendor of the display adapters.
*/
export function gpuBackendHere(env = process.env, { vendors = windowsDisplayVendors() } = {}) {
const forced = String(env.STRATA_ENGINE_BACKEND ?? '').trim().toLowerCase();
if (forced) return ['cuda', 'hip', 'sycl'].includes(forced) ? forced : null;
// Windows: the display adapters are the machine's own statement of which cards it has; a leftover nvidia-smi from a card that was
// taken out must not outvote them (#3688). Same order as Control: NVIDIA, then AMD, then Intel.
if (vendors.size) {
for (const [vendor, backend] of [['10de', 'cuda'], ['1002', 'hip'], ['8086', 'sycl']]) if (vendors.has(vendor)) return backend;
}
if (nvidiaDriverVersion()) return 'cuda';
try {
const base = '/sys/class/drm';
const vendors = new Set();
for (const card of readdirSync(base)) {
if (!/^card\d+$/.test(card)) continue;
try { vendors.add(readFileSync(`${base}/${card}/device/vendor`, 'utf8').trim().toLowerCase()); } catch { /* no vendor file */ }
}
for (const [vendor, backend] of [['0x10de', 'cuda'], ['0x1002', 'hip'], ['0x8086', 'sycl']]) if (vendors.has(vendor)) return backend;
} catch { /* not Linux */ }
return null;
export function gpuBackendHere(env = process.env, options = {}) {
return gpuBackendsHere(env, options)[0] ?? null;
}
const HIP_FAMILIES = [['RX 9000', ['gfx1200', 'gfx1201']], ['RX 7000', ['gfx1100', 'gfx1101', 'gfx1102']], ['RX 6000', ['gfx1030', 'gfx1031']], ['Ryzen AI Max', ['gfx1151']]];
@@ -147,10 +177,12 @@ export async function installBundledEngine({ bundleDir, components, platform = n
if (platform) packages = packages.filter(p => p.manifest.platform === platform);
if (!packages.length) return { action: 'none' };
// A build for another make of graphics card than this PC has can never run here: it is not unpacked, and the caller says so in plain words.
const wanted = backend === undefined ? gpuBackendHere() : backend;
// A PC with two makes (an AMD card for the model, an NVIDIA card for the desktop) accepts a build for either (#3892).
const wantedList = backend === undefined ? gpuBackendsHere() : (Array.isArray(backend) ? backend : backend ? [backend] : []);
const wanted = wantedList[0] ?? null;
if (wanted && ['cuda', 'hip', 'sycl'].includes(wanted)) {
const gpu = packages.filter(p => ['cuda', 'hip', 'sycl'].includes(p.manifest.backend));
const fits = packages.filter(p => !gpu.includes(p) || p.manifest.backend === wanted);
const fits = packages.filter(p => !gpu.includes(p) || wantedList.includes(p.manifest.backend));
if (gpu.length && !fits.length) {
const first = gpu.sort((a, b) => compareVersions(b.manifest.version, a.manifest.version))[0].manifest;
return { action: 'wrong-make', built: first.backend, here: wanted, notice: wrongMakeNotice(first, wanted) };
@@ -234,3 +266,55 @@ export async function installBundledEngine({ bundleDir, components, platform = n
return { action: 'refused', reason: error?.message ?? String(error) };
}
}
/**
* After an update from an installer that carried an engine (a per-GPU build), its `engine-bundle` folder is still in the install folder
* (about 570 MB) although the engine was copied to the engine store long ago. It is removed only when EVERY package in it exists complete in
* the store (every file there with the size and SHA-256 its manifest lists); otherwise it stays and one line says why.
* Returns { removed: bool, reason?: string, packages?: number }; never throws.
*/
export async function removeVerifiedBundle({ bundleDir, components, log = () => {} } = {}) {
try {
if (!bundleDir || !components || !existsSync(bundleDir)) return { removed: false, reason: 'no bundle' };
// Files inside a signed macOS app must stay intact after installation.
if (process.platform === 'darwin' && resolve(bundleDir).split(sep).some(part => part.toLowerCase().endsWith('.app'))) return keepBundle(log, 'macOS app bundles must remain unchanged after signing');
const packages = [];
for (const entry of await readdir(bundleDir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const folder = join(bundleDir, entry.name);
const manifest = await readManifest(folder);
packages.push({ folder, manifest, name: entry.name });
}
if (!packages.length) return { removed: false, reason: 'nothing in the bundle' };
const store = join(components, 'engine');
const stored = existsSync(store) ? (await readdir(store, { withFileTypes: true })).filter(e => e.isDirectory() && !e.name.startsWith('.')).map(e => join(store, e.name)) : [];
for (const pkg of packages) {
if (!pkg.manifest) return keepBundle(log, `${pkg.name} has no readable manifest`);
let found = false;
for (const candidate of stored) {
const theirs = await readManifest(candidate);
if (!theirs || theirs.version !== pkg.manifest.version || theirs.platform !== pkg.manifest.platform || theirs.backend !== pkg.manifest.backend
|| (theirs.variant ?? '') !== (pkg.manifest.variant ?? '') || (theirs.build_id ?? '') !== (pkg.manifest.build_id ?? '')) continue;
if (await packageIsWhole(candidate, pkg.manifest, { deep: true })) { found = true; break; }
}
if (!found) return keepBundle(log, `${pkg.name} is not (or not completely) in the engine store`);
}
for (const pkg of packages) await rm(pkg.folder, { recursive: true, force: true });
log(`engine bundle: the copy that came with the installer (${packages.map(p => p.name).join(', ')}) was removed; the same engine is complete in the engine store`);
return { removed: true, packages: packages.length };
} catch (error) {
return keepBundle(log, `could not be checked (${error?.message ?? error})`);
}
}
function keepBundle(log, why) {
log(`engine bundle: kept in the install folder (${why})`);
return { removed: false, reason: why };
}
/**
* What the host does once its engine step has settled: for an engine that is (or was) there, look at the install folder's leftover bundle.
* The lines go to the host's output (console.log), which the desktop shell copies to its log file (`[host] engine bundle: ...`).
*/
export function cleanupAfterInstall(settled, { bundleDir, components, log = line => console.log(line) } = {}) {
return Promise.resolve(settled).then(result => (['installed', 'kept', 'repaired'].includes(result?.action) ? removeVerifiedBundle({ bundleDir, components, log }) : null), () => null);
}
+1 -1
View File
@@ -153,7 +153,7 @@ export function parseCsv(text, delimiter = ',', maxRows = 500) {
export function previewFile(root, rawPath, sessionId) {
const file = resolveChatFile(root, rawPath);
const st = statSync(file); const kind = fileType(file);
const base = { kind, name: basename(file), path: file, size: st.size, url: pathToFileURL(file).href };
const base = { kind, name: basename(file), path: file, size: st.size, mtime: Math.round(st.mtimeMs), url: pathToFileURL(file).href };
const raw = `/api/files/raw?sessionId=${encodeURIComponent(sessionId)}&path=${encodeURIComponent(file)}`;
if (kind === 'image' || kind === 'pdf') return { ...base, rawUrl: raw, mime: mimeOf(file) };
if (kind === 'other') return { ...base, note: 'This kind of file has no preview. Open it in its own app.' };
+186
View File
@@ -0,0 +1,186 @@
/**
* The chats, saved by the host as files (owner decision 11-04: the browser's 5 MB storage limit is gone).
*
* <data folder>/chats/index.json { v: 1, chats: [{ id, rev, createdAt, updatedAt, bytes, title }] }
* <data folder>/chats/<id>.json { "v": 1, "rev": N, "savedAt": ISO, "session": { ...the chat exactly as the page sent it... } }
* <data folder>/chats/conflicts/ the version a later save displaced when two windows saved the same chat from an older copy
*
* - One file per chat. A save writes a temporary file next to it, flushes it and renames it over the chat: a crash or a full disk
* in the middle leaves the older file whole. Temporary files left by a crash are removed at the next start.
* - `rev` counts the saves of a chat. A save that names a `baseRev` older than the stored one (a second window saved first) is still
* written, but the version it displaces is kept in conflicts/ first, so nothing is lost.
* - The index is a list for fast start. If it is missing or does not match the files it is rebuilt from the files; a file that cannot
* be read is reported and left exactly as it is.
* - Nothing is cut: the chat is stored byte for byte as received (a text splice, no re-serialising).
* - Owner-only: the folder is created private (POSIX 0700 and files 0600; on Windows the folder gets a protected ACL that its files
* inherit), the same level as the rest of the profile.
*/
import { mkdirSync, readdirSync, readFileSync, statSync, unlinkSync } from 'node:fs';
import { open, readFile, rename, unlink, mkdir, copyFile, readdir } from 'node:fs/promises';
import { join } from 'node:path';
export const CHAT_FORMAT = 1;
const ID = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/;
const RESERVED = /^(con|prn|aux|nul|com\d|lpt\d|index|import|conflicts)(\.|$)/i;
const KEEP_CONFLICTS = 5;
export class ChatStoreError extends Error {
constructor(message, status, code) { super(message); this.status = status; this.code = code; }
}
/** The disk operations, replaceable in tests to make a disk full, a permission failure or a crash in the middle of a write. */
export const realIo = {
async writeFile(path, text) {
const handle = await open(path, 'w', 0o600);
try { await handle.writeFile(text, 'utf8'); await handle.sync(); } finally { await handle.close(); }
},
rename, unlink, copyFile, readFile, readdir, mkdir,
};
export const validChatId = id => typeof id === 'string' && ID.test(id) && !RESERVED.test(id);
/** Map a failed disk operation to what the page is told: a full disk and a refused write are different words for the same "not saved". */
export function storeFailure(error) {
if (error instanceof ChatStoreError) return error;
const code = error?.code;
if (code === 'ENOSPC' || code === 'EDQUOT' || code === 'EFBIG') return new ChatStoreError('The disk is full.', 507, 'disk-full');
if (code === 'EACCES' || code === 'EPERM' || code === 'EROFS') return new ChatStoreError('Strata may not write its chat folder.', 403, 'permission');
return new ChatStoreError(error?.message ?? 'The chat could not be written.', 500, code ?? 'write-failed');
}
export function createChatStore({ dir, io = realIo, privateDir = () => {}, now = () => new Date() }) {
const root = join(dir, 'chats');
const conflictsDir = join(root, 'conflicts');
mkdirSync(root, { recursive: true, mode: 0o700 });
try { privateDir(root); } catch { /* the folder is inside the profile; the tighter ACL is best effort */ }
let tmpCount = 0;
let index = new Map(); // id -> entry
const damaged = new Set();
const fileOf = id => join(root, `${id}.json`);
const bytesOf = text => Buffer.byteLength(text, 'utf8');
const entryOf = (id, rev, session, bytes) => ({ id, rev, createdAt: session?.createdAt ?? null, updatedAt: session?.updatedAt ?? null, bytes, title: typeof session?.title === 'string' ? session.title.slice(0, 200) : '' });
// A chain, so saves of one host never overlap: the file, the index and the conflicts folder change one save at a time.
let chain = Promise.resolve();
const queue = work => { const run = chain.then(work, work); chain = run.catch(() => {}); return run; };
function loadSync() {
// Temporary files from a write that never finished.
let names = [];
try { names = readdirSync(root); } catch { /* empty */ }
for (const name of names) if (/\.tmp-\d+-\d+$/.test(name)) { try { unlinkSync(join(root, name)); } catch { /* left */ } }
const files = names.filter(n => n.endsWith('.json') && n !== 'index.json' && !/\.tmp-/.test(n)).map(n => n.slice(0, -5)).filter(validChatId);
let saved = null;
try { const parsed = JSON.parse(readFileSync(join(root, 'index.json'), 'utf8')); if (parsed?.v === CHAT_FORMAT && Array.isArray(parsed.chats)) saved = parsed.chats; } catch { /* rebuilt below */ }
const map = new Map();
if (saved) for (const e of saved) if (e && validChatId(e.id) && files.includes(e.id)) map.set(e.id, e);
// Files the index does not know (a crash before the index was written, an index lost): read from the files.
for (const id of files) {
if (map.has(id)) continue;
try {
const parsed = JSON.parse(readFileSync(fileOf(id), 'utf8'));
if (parsed?.v !== CHAT_FORMAT || !parsed.session || parsed.session.id !== id) throw new Error('not a chat file');
map.set(id, entryOf(id, Number(parsed.rev) || 1, parsed.session, statSync(fileOf(id)).size));
} catch { damaged.add(id); }
}
index = map;
}
loadSync();
async function writeAtomic(path, text) {
const temp = `${path}.tmp-${process.pid}-${++tmpCount}`;
try {
await io.writeFile(temp, text);
for (let attempt = 0; ; attempt++) {
try { await io.rename(temp, path); break; }
catch (e) { if (attempt >= 5 || !['EPERM', 'EBUSY', 'EACCES'].includes(e?.code)) throw e; await new Promise(r => setTimeout(r, 40 * (attempt + 1))); } // an antivirus scan holds the file for a moment on Windows
}
} catch (e) {
try { await io.unlink(temp); } catch { /* nothing to remove */ }
throw e;
}
}
async function writeIndex() {
const list = [...index.values()];
try { await writeAtomic(join(root, 'index.json'), JSON.stringify({ v: CHAT_FORMAT, savedAt: now().toISOString(), chats: list })); }
catch { /* only a list: it is rebuilt from the files when it does not match */ }
}
async function keepDisplaced(id, rev) {
try {
await io.mkdir(conflictsDir, { recursive: true });
await io.copyFile(fileOf(id), join(conflictsDir, `${id}.rev${rev}.json`));
const mine = (await io.readdir(conflictsDir)).filter(n => n.startsWith(`${id}.rev`)).sort((a, b) => Number(a.match(/rev(\d+)/)?.[1]) - Number(b.match(/rev(\d+)/)?.[1]));
for (const old of mine.slice(0, Math.max(0, mine.length - KEEP_CONFLICTS))) { try { await io.unlink(join(conflictsDir, old)); } catch { /* left */ } }
} catch (e) {
throw storeFailure(e); // not saved rather than saved over the only copy of the other window's work
}
}
return {
root,
/** The chats, newest first. */
list() {
const chats = [...index.values()].sort((a, b) => String(b.createdAt ?? '').localeCompare(String(a.createdAt ?? '')) || a.id.localeCompare(b.id));
return { v: CHAT_FORMAT, chats, damaged: [...damaged] };
},
has: id => index.has(id),
rev: id => index.get(id)?.rev ?? 0,
/** The stored file text of a chat ({"v","rev","savedAt","session"}), or null. */
async read(id) {
if (!validChatId(id) || !index.has(id)) return null;
try { return await io.readFile(fileOf(id), 'utf8'); }
catch (e) { if (e?.code === 'ENOENT') { index.delete(id); return null; } throw storeFailure(e); }
},
/**
* Save one chat. `sessionText` is the chat's JSON as the page wrote it. Resolves { id, rev, bytes, conflict }.
* Rejects with a ChatStoreError (status 400 for a chat that is not a chat, 507 disk full, 403 refused, 500 other): nothing was changed.
*/
save(id, sessionText, { baseRev = null } = {}) {
if (!validChatId(id)) return Promise.reject(new ChatStoreError('That is not a chat id.', 400, 'bad-id'));
let session;
try { session = JSON.parse(sessionText); } catch { return Promise.reject(new ChatStoreError('The chat must be JSON.', 400, 'bad-json')); }
if (!session || typeof session !== 'object' || Array.isArray(session) || session.id !== id) return Promise.reject(new ChatStoreError('The chat does not match its id.', 400, 'bad-chat'));
const body = sessionText.trim();
return queue(async () => {
const current = index.get(id);
const rev = (current?.rev ?? 0) + 1;
let conflict = false;
if (current && baseRev !== null && Number(baseRev) !== current.rev) { conflict = true; await keepDisplaced(id, current.rev); }
const text = `{"v":${CHAT_FORMAT},"rev":${rev},"savedAt":${JSON.stringify(now().toISOString())},"session":${body}}`;
try { await writeAtomic(fileOf(id), text); } catch (e) { throw storeFailure(e); }
const bytes = bytesOf(text);
index.set(id, entryOf(id, rev, session, bytes));
damaged.delete(id);
await writeIndex();
return { id, rev, bytes, conflict };
});
},
/** Delete a chat (the person deleted it). A chat that is not there is already gone. */
remove(id) {
if (!validChatId(id)) return Promise.reject(new ChatStoreError('That is not a chat id.', 400, 'bad-id'));
return queue(async () => {
try { await io.unlink(fileOf(id)); } catch (e) { if (e?.code !== 'ENOENT') throw storeFailure(e); }
const had = index.delete(id);
await writeIndex();
return { id, removed: had };
});
},
/** Chats from the browser's old storage: every one the host does not have yet is saved; the ones it has are left alone. */
async importMany(sessions) {
const imported = [], skipped = [], failed = [];
for (const session of Array.isArray(sessions) ? sessions : []) {
const id = session?.id;
if (!validChatId(id)) { failed.push({ id: String(id ?? ''), error: 'not a chat id' }); continue; }
if (index.has(id)) { skipped.push(id); continue; }
try { await this.save(id, JSON.stringify(session)); imported.push(id); }
catch (e) { failed.push({ id, error: e.message, code: e.code }); if (e.code === 'disk-full') break; }
}
return { imported, skipped, failed };
},
/** Wait until every save queued so far is on disk (tests, shutdown). */
idle: () => chain,
};
}
+78 -11
View File
@@ -74,6 +74,17 @@ export function findControl({ env = process.env, platform = process.platform, ex
return control;
}
/**
* The environment of the server the host starts from a source checkout (#3851): the key travels in the run configuration, so a
* STRATA_API_KEY the host itself was started with is removed. Setting it to an empty string made serve/server.py stop with
* "an API key was given but it is empty".
*/
export function sourceServerEnvironment(env = process.env) {
const next = { ...env, PYTHONUNBUFFERED: '1', PYTHONUTF8: '1', PYTHONIOENCODING: 'utf-8' };
delete next.STRATA_API_KEY;
return next;
}
/** The environment Control runs in: nothing from the user's Python, and acquiring forbidden. */
export function controlEnvironment(control, { env = process.env, components = componentsDirectory({ env }), acquire = false } = {}) {
const next = { ...env };
@@ -110,10 +121,18 @@ export class ControlError extends Error {
}
}
/** Run one CLI command of Control. Resolves { code, json, text, stderr }; rejects only if it cannot start or times out. */
export function runCli(control, args, { env = process.env, timeoutMs = 60_000, spawnProcess = spawn, components, acquire = false, onLine = null, signal = null } = {}) {
/**
* Small Control programs that are a module of strata_control and not a `strata` command. They run on Control's interpreter with Control's folder first on the
* path (the folder is the working directory), so they work whatever the interpreter does with the script folder.
*/
const BOOT = {
pictures: 'import os,sys;sys.path.insert(0,os.getcwd());from strata_control import pictures;sys.exit(pictures.main(sys.argv[1:]))',
};
/** Run one CLI command of Control (or, with `boot`, one of its modules). Resolves { code, json, text, stderr }; rejects only if it cannot start or times out. */
export function runCli(control, args, { env = process.env, timeoutMs = 60_000, spawnProcess = spawn, components, acquire = false, onLine = null, signal = null, boot = null } = {}) {
return new Promise((resolveRun, reject) => {
const child = spawnProcess(control.python, [control.cli, ...args], {
const child = spawnProcess(control.python, boot ? ['-c', BOOT[boot], ...args] : [control.cli, ...args], {
cwd: control.dir, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'],
env: controlEnvironment(control, { env, acquire, ...(components ? { components } : {}) }),
});
@@ -174,20 +193,44 @@ export function createControl({ control = findControl(), env = process.env, run
if (result.json?.ok) return result.json;
throw failure(result, 'The update check failed.');
},
async update({ feed = env.STRATA_UPDATE_FEED ?? null, engine = true, control: withControl = true, appVersion = env.STRATA_APP_VERSION ?? null, force = false, onEvent = null, signal = null } = {}) {
async update({ feed = env.STRATA_UPDATE_FEED ?? null, engine = true, control: withControl = true, appVersion = env.STRATA_APP_VERSION ?? null, force = false, stopModel = false, onEvent = null, signal = null } = {}) {
const args = ['update', '--output', 'jsonl', ...feedArgs(feed), ...(engine ? [] : ['--no-engine']), ...(withControl ? [] : ['--no-control']),
...(appVersion ? ['--app-version', appVersion] : []), ...(force ? ['--force'] : [])];
...(appVersion ? ['--app-version', appVersion] : []), ...(force ? ['--force'] : []), ...(stopModel ? ['--stop-model'] : [])];
const result = await call(args, { timeoutMs: 3 * 3600_000, acquire: true, onLine: onEvent ?? (() => {}), signal });
if (result.json?.ok) return result.json;
const failed = result.json?.results?.find?.(r => r.status === 'failed');
if (failed) throw Object.assign(new ControlError(failed.error, { exitCode: failed.exit_code ?? result.code, output: result.json }), { code: result.code === 7 ? 'engine_serving' : 'update_failed' });
throw failure(result, 'The update failed.');
},
/** Install an engine package the person downloaded and chose (the offline path): no network; Control checks the package's own signature. */
async installEngineFile(path, { onEvent = null, signal = null } = {}) {
const result = await call(['components', 'install-file', path, '--output', 'jsonl'], { timeoutMs: 3600_000, onLine: onEvent ?? (() => {}), signal });
if (result.json?.ok) return { ok: true, results: [result.json] };
throw failure(result, 'This file could not be installed.');
},
async rollbackEngine() {
const result = await call(['components', 'rollback', '--output', 'json'], { timeoutMs: 120_000 });
if (result.json?.ok) return result.json;
throw failure(result, 'The engine could not be rolled back.');
},
/** The engines installed on this computer, by family (`strata components engines`): the one list the Terminal prints too. Reads files only. */
async engines() {
const result = await call(['components', 'engines', '--output', 'json'], { timeoutMs: 60_000 });
if (result.json?.ok) return result.json;
throw failure(result, 'The installed engines could not be read.');
},
/** Switch to an installed engine (`strata components use`): nothing is downloaded. `root` is only for a DeepSeek engine that lives in more than one place. */
async useEngine({ family, folder }) {
const result = await call(['components', 'use', folder, '--family', family, '--output', 'json'], { timeoutMs: 600_000 });
if (result.json?.ok) return result.json;
throw failure(result, 'The engine could not be switched.');
},
/** Delete an installed engine (`strata components remove`); Control refuses the one in use. */
async removeEngine({ family, folder }) {
const result = await call(['components', 'remove', folder, '--family', family, '--output', 'json'], { timeoutMs: 600_000 });
if (result.json?.ok) return result.json;
throw failure(result, 'The engine could not be deleted.');
},
async engine({ backend = null } = {}) {
const result = await call(['components', 'engine', '--output', 'json', ...(backend ? ['--backend', backend] : [])], { timeoutMs: 30_000 });
if (result.json) return result.json; // exit 5 still carries the report and the notice
@@ -245,21 +288,45 @@ export function createControl({ control = findControl(), env = process.env, run
throw Object.assign(failure(result, 'The partial copies could not be removed.'), { code: result.json?.error?.code ?? 'leave_failed', details: result.json?.error?.details ?? '' });
},
/**
* The DFlash2 drafter for a model's run config and the drafting choice (`strata drafting plan`): { needed, gb, line, note, drafter }. Control decides
* (the table of strata_control/drafting.py); the app never reads it. Nothing is downloaded.
* The drafter for a model's run config and the drafting choice (`strata drafting plan`): { needed, gb, line, note, drafter, split, mtp_needed, mtp_gb }. Control decides
* (the table of strata_control/drafting.py and the all-cards split engine's rule, strata_control/split_engine.py: the same answer as the Terminal's); the app never reads
* it. Nothing is downloaded. `noSplit` asks as if the split engine will not take the start (only after the draft layer for the split could not be downloaded).
*/
async draftingPlan({ configPath = null, mode = null, family = null, dataDir = null } = {}) {
const result = await call(['drafting', 'plan', ...(configPath ? ['--config', configPath] : []), ...(mode ? ['--mode', mode] : []), ...(family ? ['--family', family] : []), ...(dataDir ? ['--data-dir', dataDir] : []), '--output', 'json'], { timeoutMs: 30_000 });
async draftingPlan({ configPath = null, mode = null, family = null, dataDir = null, noSplit = false, unloading = false } = {}) {
const result = await call(['drafting', 'plan', ...(configPath ? ['--config', configPath] : []), ...(mode ? ['--mode', mode] : []), ...(family ? ['--family', family] : []), ...(dataDir ? ['--data-dir', dataDir] : []), ...(noSplit ? ['--no-split'] : []), ...(unloading ? ['--engines-unloading'] : []), '--output', 'json'], { timeoutMs: 30_000 });
if (result.json?.ok) return result.json;
throw failure(result, 'The drafter could not be checked.');
},
/** Fetch and pack the drafter when the choice would use it and it is not here (`strata drafting fetch --if-needed`); `onEvent` gets {step, done, total}. */
async draftingFetch({ configPath, mode = null, onEvent = null, signal = null } = {}) {
const args = ['drafting', 'fetch', '--config', configPath, ...(mode ? ['--mode', mode] : []), '--if-needed', '--output', 'jsonl'];
async draftingFetch({ configPath, mode = null, noSplit = false, unloading = false, onEvent = null, signal = null } = {}) {
const args = ['drafting', 'fetch', '--config', configPath, ...(mode ? ['--mode', mode] : []), ...(noSplit ? ['--no-split'] : []), ...(unloading ? ['--engines-unloading'] : []), '--if-needed', '--output', 'jsonl'];
const result = await call(args, { timeoutMs: 3 * 3600_000, acquire: true, onLine: onEvent ?? (() => {}), signal });
if (result.json?.ok) return result.json;
throw Object.assign(new ControlError(result.json?.error?.message ?? (result.stderr.split(/\r?\n/).filter(Boolean).pop() || 'The DFlash2 drafter could not be downloaded.'), { exitCode: result.code, output: result.json }), { code: 'drafting_failed' });
},
/**
* The all-cards split engine for the run config the app is about to start (`strata split-engine apply`, the step `strata launch` does itself): Control checks the rule, the
* engine package and the draft layer on this computer and writes the split engine's run config to `outPath` when the answer is yes. Resolves { split, line, warn, reasons, cards, written };
* `configPath` is never changed, nothing is downloaded.
*/
async splitEngine({ configPath, outPath } = {}) {
const result = await call(['split-engine', 'apply', '--config', configPath, '--out', outPath, '--output', 'json'], { timeoutMs: 60_000 });
if (result.json?.ok) return result.json;
throw failure(result, 'The all-cards split engine could not be checked.');
},
/**
* The picture files of an installed model that has none (strata_control/pictures.py): found or downloaded once (about 0.9 GB, shared), and the run
* config's `vision` section worked out for this engine. Nothing else of the model is touched and nothing is written to its run config: the caller does.
* Resolves { mmproj, vision, fetched, note, data_dir }; a failure is a ControlError whose `code` is offline | disk | failed | checksum | unsupported | no_reader.
* `onEvent` gets {step, done, total}. This is an explicit step that may reach the network (`acquire`), only for the Load the person pressed.
*/
async picturesFetch({ configPath, engineDir, vision = 'yes', dataDir = null, family = null, hardware = null, onEvent = null, signal = null } = {}) {
const args = ['fetch', '--config', configPath, '--engine-dir', engineDir, '--vision', vision, ...(dataDir ? ['--data-dir', dataDir] : []), ...(family ? ['--family', family] : []),
...(hardware?.vram_gb != null ? ['--vram-gb', String(hardware.vram_gb)] : []), ...(hardware?.make ? ['--make', String(hardware.make)] : []), ...(hardware?.arch != null ? ['--arch', String(hardware.arch)] : [])];
const result = await call(args, { timeoutMs: 3 * 3600_000, acquire: true, boot: 'pictures', onLine: onEvent ?? (() => {}), signal });
if (result.json?.ok) return result.json;
throw Object.assign(new ControlError(result.json?.error?.message ?? (result.stderr.split(/\r?\n/).filter(Boolean).pop() || 'The picture files could not be downloaded.'), { exitCode: result.code, output: result.json }), { code: result.json?.error?.code ?? 'failed' });
},
async saveProfile(name, configPath) {
const listed = await call(['profile', 'list', '--output', 'json'], { timeoutMs: 20_000 });
const known = (listed.json?.profiles ?? []).some(p => p.id === name);
+34 -14
View File
@@ -1,6 +1,7 @@
/** Physical topology and GPU sensors; unavailable values stay null. */
import { readFile, readdir, realpath } from 'node:fs/promises';
import { probe, round } from './util.mjs';
import { windowsGpuReading } from './windows-gpu.mjs';
/** Unified GPUs share one system memory pool; dedicated GPUs each have their own. */
export function gpuMemoryTotals(devices, readings) {
@@ -39,13 +40,15 @@ export function parseNvidiaDevices(raw) {
return [{id:uuid,index:Number(index),sensorIndex:Number(index),busId:bus?.toLowerCase(),vendor:'nvidia',name,vramGb:round((number(memory)??0)/1024,1),unified:false,backend:'cuda',driver}];
});
}
/** A percentage reading; "[N/A]" and out-of-range values are no reading. */
const percentOf = v => { const n=number(v); return n!==null&&n>=0&&n<=100?n:null; };
export function parseNvidiaReadings(raw) {
return raw.split('\n').flatMap(line=>{
const [id,temp,load,memory,power,limit,gen,maxGen,width]=line.split(',').map(s=>s.trim());
const [id,temp,load,memory,power,limit,gen,maxGen,width,fan,clock]=line.split(',').map(s=>s.trim());
if (!id?.startsWith('GPU-')) return [];
const t=number(temp),u=number(load),m=number(memory);
const positive = v => { const n=number(v); return n!==null&&n>=0?n:null; };
return [{id,temperatureC:t!==null&&t>=0&&t<=150?t:null,utilizationPct:u!==null&&u>=0&&u<=100?u:null,memoryUsedGb:m!==null&&m>=0?round(m/1024,2):null,powerW:positive(power),powerLimitW:positive(limit),pcieGen:positive(gen),pcieGenMax:positive(maxGen),pcieWidth:positive(width),pcieRxMbs:null}];
return [{id,temperatureC:t!==null&&t>=0&&t<=150?t:null,utilizationPct:u!==null&&u>=0&&u<=100?u:null,memoryUsedGb:m!==null&&m>=0?round(m/1024,2):null,powerW:positive(power),powerLimitW:positive(limit),pcieGen:positive(gen),pcieGenMax:positive(maxGen),pcieWidth:positive(width),pcieRxMbs:null,fanPct:percentOf(fan),clockMhz:positive(clock)}];
});
}
export function parsePcieThroughput(raw) {
@@ -59,6 +62,18 @@ export function parsePcieThroughput(raw) {
}
async function read(path) { try {return (await readFile(path,'utf8')).trim();} catch {return '';} }
async function list(path) {try{return await readdir(path);}catch{return [];}}
/** A server's remote-console chip (ASPEED), a virtual display or a framebuffer is not a graphics card: no maker we know and no memory of its own. */
export function isComputeAdapter(vendor,vramBytes) { return vendor!=='unknown'||vramBytes>0; }
/**
* The temperature of an amdgpu card from its hwmon inputs [{label, milli}]: the edge sensor (what nvtop and rocm-smi show), else the
* junction, else the first one that has a plausible value. Cards such as the Instinct line have no edge sensor. 0 is no reading.
*/
export function pickGpuTemperature(inputs) {
const ok=inputs.filter(i=>i.milli!==null&&i.milli>0&&i.milli<=150000);
const first=re=>ok.find(i=>re.test(i.label??''));
const chosen=first(/edge|^gpu/i)??ok.find(i=>!i.label)??first(/junction|hotspot/i)??ok.find(i=>!/mem/i.test(i.label??''));
return chosen?chosen.milli/1000:null;
}
export async function detectDrmGpus() {
const result=[];
for(const card of (await list('/sys/class/drm')).filter(n=>/^card\d+$/.test(n))) {
@@ -68,39 +83,44 @@ export async function detectDrmGpus() {
const vendor={'0x1002':'amd','0x8086':'intel','0x10de':'nvidia'}[vendorId]??'unknown';
const name=await probe('lspci',['-s',busId]);
const bytes=number(await read(`${device}/mem_info_vram_total`));
if(!isComputeAdapter(vendor,bytes))continue;
result.push({id:`drm:${busId}`,index:result.length,busId,vendor,name:name.replace(/^[^ ]+\s+[^:]+:\s*/,'')||`${vendor.toUpperCase()} GPU`,vramGb:bytes===null?0:round(bytes/1024**3,1),unified:vendor==='intel'&&!(bytes>0),backend:'vulkan',sensorPath:device});
}
return result;
}
export async function readGpuTelemetry(gpus) {
/** `transfer: false` skips the PCIe throughput sample (a second nvidia-smi); it only means something while a model runs. */
export async function readGpuTelemetry(gpus, { transfer: wantTransfer = true, run = probe } = {}) {
const now=new Date().toISOString();
const hasNvidia=gpus.some(g=>g.vendor==='nvidia');
let raw=hasNvidia?await probe('nvidia-smi',['--query-gpu=uuid,temperature.gpu,utilization.gpu,memory.used,power.draw,power.limit,pcie.link.gen.current,pcie.link.gen.max,pcie.link.width.current','--format=csv,noheader,nounits'],2000):'';
let raw=hasNvidia?await run('nvidia-smi',['--query-gpu=uuid,temperature.gpu,utilization.gpu,memory.used,power.draw,power.limit,pcie.link.gen.current,pcie.link.gen.max,pcie.link.width.current,fan.speed,clocks.current.graphics','--format=csv,noheader,nounits'],2000):'';
// Older drivers can reject a field; retain their basic sensors in that case.
if(hasNvidia&&!raw)raw=await probe('nvidia-smi',['--query-gpu=uuid,temperature.gpu,utilization.gpu,memory.used','--format=csv,noheader,nounits'],2000);
if(hasNvidia&&!raw)raw=await run('nvidia-smi',['--query-gpu=uuid,temperature.gpu,utilization.gpu,memory.used','--format=csv,noheader,nounits'],2000);
const nvidia=new Map(parseNvidiaReadings(raw).map(r=>[r.id,r]));
const transfer=hasNvidia?parsePcieThroughput(await probe('nvidia-smi',['dmon','-s','t','-c','1'],1500)):new Map();
const transfer=hasNvidia&&wantTransfer?parsePcieThroughput(await run('nvidia-smi',['dmon','-s','t','-c','1'],1500)):new Map();
return Promise.all(gpus.map(async gpu=>{
let reading=nvidia.get(gpu.id)??{id:gpu.id??`${gpu.vendor}:${gpu.index}`,temperatureC:null,utilizationPct:null,memoryUsedGb:null,powerW:null,powerLimitW:null,pcieGen:null,pcieGenMax:null,pcieWidth:null,pcieRxMbs:null};
if(gpu.vendor==='nvidia')reading={...reading,pcieRxMbs:transfer.get(gpu.sensorIndex??gpu.index)??null};
if(gpu.luid&&!nvidia.has(gpu.id)) {
const counters=windowsGpuReading(gpu.luid);
if(counters)reading={...reading,utilizationPct:counters.utilizationPct,memoryUsedGb:round(counters.memoryUsedGb,2),temperatureC:counters.temperatureC??null};
}
if(gpu.sensorPath&&!nvidia.has(gpu.id)) {
let temperature=null,power=null,powerLimit=null;
let temperature=null,power=null,powerLimit=null,fanPwm=null,clockHz=null;
for(const hwmon of await list(`${gpu.sensorPath}/hwmon`)) {
const folder=`${gpu.sensorPath}/hwmon/${hwmon}`,inputs=(await list(folder)).filter(n=>/^temp\d+_input$/.test(n));
power ??= number(await read(`${folder}/power1_average`)) ?? number(await read(`${folder}/power1_input`));
powerLimit ??= number(await read(`${folder}/power1_cap`));
for(const input of inputs) {
const label=await read(`${folder}/${input.replace('_input','_label')}`);
if(label&&!/edge|gpu/i.test(label))continue;
const milli=number(await read(`${folder}/${input}`));
if(milli!==null&&milli>=0&&milli<=150000){temperature=milli/1000;break;}
}
fanPwm ??= number(await read(`${folder}/pwm1`)); // 0-255, the fan's duty as the amdgpu driver reports it
clockHz ??= number(await read(`${folder}/freq1_input`)); // the graphics clock in Hz
const sensors=[];
for(const input of inputs)sensors.push({label:await read(`${folder}/${input.replace('_input','_label')}`),milli:number(await read(`${folder}/${input}`))});
temperature=pickGpuTemperature(sensors);
if(temperature!==null)break;
}
const utilization=number(await read(`${gpu.sensorPath}/gpu_busy_percent`)),bytes=number(await read(`${gpu.sensorPath}/mem_info_vram_used`));
const gen=path=>read(path).then(raw=>({2.5:1,5:2,8:3,16:4,32:5,64:6}[Number.parseFloat(raw)]??null));
const [pcieGen,pcieGenMax,width]=await Promise.all([gen(`${gpu.sensorPath}/current_link_speed`),gen(`${gpu.sensorPath}/max_link_speed`),read(`${gpu.sensorPath}/current_link_width`)]);
reading={...reading,temperatureC:temperature,utilizationPct:utilization!==null&&utilization>=0&&utilization<=100?utilization:null,memoryUsedGb:bytes!==null&&bytes>=0?round(bytes/1024**3,2):null,powerW:power!==null&&power>=0?power/1e6:null,powerLimitW:powerLimit!==null&&powerLimit>=0?powerLimit/1e6:null,pcieGen,pcieGenMax,pcieWidth:number(width)};
reading={...reading,fanPct:fanPwm!==null&&fanPwm>=0&&fanPwm<=255?Math.round(fanPwm/255*100):null,clockMhz:clockHz!==null&&clockHz>0?Math.round(clockHz/1e6):null,temperatureC:temperature,utilizationPct:utilization!==null&&utilization>=0&&utilization<=100?utilization:null,memoryUsedGb:bytes!==null&&bytes>=0?round(bytes/1024**3,2):null,powerW:power!==null&&power>=0?power/1e6:null,powerLimitW:powerLimit!==null&&powerLimit>=0?powerLimit/1e6:null,pcieGen,pcieGenMax,pcieWidth:number(width)};
}
return {...reading,sampledAt:now,source:Object.entries(reading).some(([key,value])=>key!=='id'&&typeof value==='number')?'sensor':'none'};
}));
+20
View File
@@ -0,0 +1,20 @@
// DeepSeek-V4.1-Flash's own context limit, read from its pack (the app offers sizes up to it: src/lib/deepseek-context.ts).
// Project Maya v1.1.28 (#442, @needmorevram, MIT) did the same in its dashboard. strata_control/dsv41_context.py reads the same file for setup and the Terminal.
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
/** The pack's own limit: `max_position_embeddings` in <pack>/ds4.txt. Null when there is no pack, no file or no such line (the choices then stay as before). */
export function packContextLimit(pack) {
if (!pack) return null;
try {
const match = /^max_position_embeddings\s+(\d+)\s*$/m.exec(readFileSync(join(pack, 'ds4.txt'), 'utf8'));
const value = match ? Number(match[1]) : 0;
return Number.isSafeInteger(value) && value >= 1024 ? value : null;
} catch { return null; }
}
/** The limit of the pack a DeepSeek run config names (`--pack DIR` in its arguments). */
export function configContextLimit(config) {
const args = (config?.args ?? []).map(String), at = args.indexOf('--pack');
return at >= 0 && at + 1 < args.length ? packContextLimit(args[at + 1]) : null;
}
+14
View File
@@ -0,0 +1,14 @@
/**
* GPU energy since the host started, in Wh (#3628): the power reading held for the time since the last tick, added up.
* A long gap (the PC slept, the host was paused) counts for at most 5 s, so a sleep never adds a made-up hour of use.
*/
const meter = { wh: 0, at: 0 };
export function addEnergy(powerW, at) {
const dt = meter.at ? Math.min((at - meter.at) / 1000, 5) : 0;
meter.at = at;
if (typeof powerW === 'number' && Number.isFinite(powerW) && powerW >= 0 && dt > 0) meter.wh += powerW * dt / 3600;
return meter.wh;
}
export const energyWh = () => meter.wh;
+42
View File
@@ -0,0 +1,42 @@
import { plainEngineError } from './update-messages.mjs';
/**
* Settings > Engine: the engines installed on this computer, switched and deleted through Control (`strata components engines | use | remove`),
* the same code the Terminal runs. This module only asks, and turns Control's answer into plain words; it never touches an engine folder itself.
* Nothing here downloads: a switch uses what is installed.
*/
const FAMILIES = ['qwen', 'deepseek'];
/** { status, body } for GET /api/engines. Without Control (a source checkout) the list is not available and the page says so. */
export async function listEngines(control) {
if (typeof control?.engines !== 'function') return { status: 200, body: { available: false, families: [], total_bytes: 0 } };
try {
const listing = await control.engines();
return { status: 200, body: { available: true, families: listing.families ?? [], total_bytes: listing.total_bytes ?? 0, serving: listing.serving === true } };
} catch (error) {
return { status: 500, body: { error: "The installed engines couldn't be read.", details: String(error?.message ?? error) } };
}
}
/**
* { status, body } for POST /api/engines/use and /api/engines/remove. `modelsRunning()` says whether the app has a model loaded: a switch then
* answers at once with the one next step (stop it first); a delete of an engine nothing uses is allowed, Control refuses the one in use.
*/
export async function changeEngine(control, action, body, { modelsRunning = () => false } = {}) {
if (typeof control?.useEngine !== 'function') return { status: 501, body: { error: 'Engines can be switched and deleted in the installed Strata app.' } };
const family = body?.family, folder = body?.folder;
if (!FAMILIES.includes(family) || typeof folder !== 'string' || !/^[0-9A-Za-z][\w.+-]*$/.test(folder)) {
return { status: 400, body: { error: 'Choose an engine from the list.' } };
}
if (action === 'use' && modelsRunning()) {
return { status: 409, body: { error: 'A model is running, so nothing was changed. Unload the model in Models, then try again.' } };
}
try {
const result = action === 'use' ? await control.useEngine({ family, folder }) : await control.removeEngine({ family, folder });
return { status: 200, body: { ok: true, ...result } };
} catch (error) {
const plain = plainEngineError(error, action === 'use' ? 'switch' : 'delete');
return { status: plain.message.startsWith('A model is running') ? 409 : 400, body: { error: plain.message, details: plain.details } };
}
}
+6 -3
View File
@@ -43,12 +43,15 @@ export function createEngineStep({ control, signal = null, onChange = () => {} }
return { ...part };
}
const version = engine.offered?.version ?? null;
set({ state: 'downloading', version, name: `Engine ${version ?? ''}`.trim(), total_gb: round((engine.download_bytes ?? 0) / GB), message: 'Downloading the engine for this PC…' });
// An engine already works here: say what is about to change and that it can be undone (#4823), not only "downloading".
const before = engine.installed?.version ?? null;
const change = before && version && before !== version ? `Updating the engine from ${before} to ${version}. The engine you have now is kept, and Settings can go back to it.` : null;
set({ state: 'downloading', version, name: `Engine ${version ?? ''}`.trim(), total_gb: round((engine.download_bytes ?? 0) / GB), message: change ?? 'Downloading the engine for this PC…' });
const result = await control.update({
engine: true, control: false, signal,
onEvent: event => {
if (event.event === 'progress') {
set({ state: 'downloading', percent: event.percent ?? 0, done_gb: round((event.done ?? 0) / GB), total_gb: round((event.total ?? 0) / GB), message: `Downloading the engine for this PC: ${event.percent ?? 0}%` });
set({ state: 'downloading', percent: event.percent ?? 0, done_gb: round((event.done ?? 0) / GB), total_gb: round((event.total ?? 0) / GB), message: `${change ? `Updating the engine from ${before} to ${version}` : 'Downloading the engine for this PC'}: ${event.percent ?? 0}%` });
} else if (event.event === 'phase') {
const state = { verify: 'verifying', test: 'testing', switch: 'switching' }[event.phase];
if (state) set({ state, message: PHASE_TEXT[event.phase] ?? part.message });
@@ -58,7 +61,7 @@ export function createEngineStep({ control, signal = null, onChange = () => {} }
},
});
const done = (result.results ?? []).find(r => r.component === 'engine');
set({ state: 'done', percent: 100, done_gb: part.total_gb, version: done?.installed_after?.version ?? version, message: 'The engine for this PC is ready.' });
set({ state: 'done', percent: 100, done_gb: part.total_gb, version: done?.installed_after?.version ?? version, message: change ? `The engine was updated from ${before} to ${version}. Settings can go back to the one you had.` : 'The engine for this PC is ready.' });
} catch (error) {
const plain = plainUpdateError(error, 'engine');
note = `Your model is ready. The engine for this PC couldn't be downloaded. ${plain.next}`;
+13 -3
View File
@@ -25,7 +25,12 @@ export function windowsFolderScript(prompt) {
'$dialog.ShowNewFolderButton = $true',
// READY tells the host the window is about to open; a host that never sees it gives up and asks for the path in words (QA F06).
'[Console]::Out.WriteLine("READY"); [Console]::Out.Flush()',
'if ($dialog.ShowDialog($owner) -eq "OK") { [Console]::Out.Write($dialog.SelectedPath) }',
// The dialog starts at the Desktop folder. Where that folder does not exist (it was deleted, a OneDrive move left a dead path, the profile was
// redirected) ShowDialog throws "Unable to retrieve the root folder" and no folder could be chosen at all: start at This PC instead, and when even
// that fails say NODIALOG so the app asks for the path.
'$answer = $null',
'try { $answer = $dialog.ShowDialog($owner) } catch { try { $dialog.RootFolder = [System.Environment+SpecialFolder]::MyComputer; $answer = $dialog.ShowDialog($owner) } catch { [Console]::Out.WriteLine("NODIALOG"); [Console]::Out.Flush() } }',
'if ($answer -eq "OK") { [Console]::Out.Write($dialog.SelectedPath) }',
'$owner.Close()',
].join('; ');
}
@@ -62,7 +67,10 @@ export function watchedWindowsDialog(shell, args, { readyMs = 30_000, openMs = 2
const later = () => { clearTimeout(timer); ready = true; setTimeout(() => end(resolve, { stdout: '' }), openMs).unref?.(); };
child.stdout?.on('data', chunk => { stdout += chunk; if (!ready && /^READY\s*$/m.test(stdout)) { clearTimeout(timer); later(); } });
child.on('error', error => end(reject, error));
child.on('close', code => { if (settled) return; settled = true; clearTimeout(timer); code === 0 ? resolve({ stdout }) : reject(Object.assign(new Error('exit ' + code), { code })); });
child.on('close', code => { if (settled) return; settled = true; clearTimeout(timer);
// PowerShell ended before its window opened (security software ended it, #3831): the person types the path instead.
if (!ready) return resolve({ noDialog: true });
code === 0 ? resolve({ stdout }) : reject(Object.assign(new Error('exit ' + code), { code })); });
});
}
@@ -85,11 +93,13 @@ export async function chooseFolder(prompt, options = {}) {
try {
const done = windowsDialog ? await windowsDialog(shell, windowsFolderArgs(prompt)) : await run(shell, windowsFolderArgs(prompt));
if (done.noDialog) return { unavailable: true, failed: true, reason: 'The folder window did not open. Type the folder path instead.' };
if (/^NODIALOG\s*$/m.test(String(done.stdout ?? ''))) return { unavailable: true, failed: true, reason: 'The folder window could not open. Type the folder path instead.' };
const answer = answerOf(done.stdout).trim();
return answer ? { path: cleanChosenPath(answer) } : { cancelled: true };
} catch (error) {
if (missing(error)) return { unavailable: true, reason: 'Windows PowerShell was not found.' };
throw new Error('The folder chooser could not open.');
// The window opened or tried to and ended with an error: the person is asked for the path instead of being left with an error and no way on.
return { unavailable: true, failed: true, reason: 'The folder window could not open. Type the folder path instead.', detail: `exit ${error?.code ?? '?'}` };
}
}
return chooseLinuxFolder(prompt, { run, portal });
+7 -5
View File
@@ -12,6 +12,7 @@ import { CONTEXT_TOKENS, VARIANTS, footprint } from './catalog.mjs';
import { MODELS_DIR, diskFreeGb, diskTotalGb } from './store.mjs';
import { probe, round } from './util.mjs';
import { utf8Script } from './powershell-utf8.mjs';
import { DX_ADAPTERS_SCRIPT, parseDxAdapters, windowsOtherGpus } from './windows-gpu.mjs';
let cached = null;
let cachedAt = 0;
@@ -187,11 +188,12 @@ async function detectGpus(platform, totalGb) {
}
if(platform==='win32') {
const raw=await probe('powershell.exe',['-NoProfile','-NonInteractive','-Command',utf8Script('Get-CimInstance Win32_VideoController | Select-Object Name,PNPDeviceID | ConvertTo-Json -Compress')]);
try {const parsed=JSON.parse(raw);for(const row of Array.isArray(parsed)?parsed:[parsed]) {
if(!row?.Name||(/nvidia/i.test(row.Name)&&gpus.some(g=>g.vendor==='nvidia')))continue;
const vendor=/amd|radeon/i.test(row.Name)?'amd':/intel/i.test(row.Name)?'intel':'unknown';
gpus.push({id:String(row.PNPDeviceID),index:gpus.length,vendor,name:row.Name,vramGb:0,unified:false,backend:'vulkan'});
}}catch{}
try {
const parsed=JSON.parse(raw);
const rows=(Array.isArray(parsed)?parsed:[parsed]).filter(row=>row?.Name&&!(/nvidia/i.test(row.Name)&&gpus.some(g=>g.vendor==='nvidia')));
const dx=rows.length?parseDxAdapters(await probe('powershell.exe',['-NoProfile','-NonInteractive','-Command',DX_ADAPTERS_SCRIPT],8000)):[];
for(const card of windowsOtherGpus(rows,dx))gpus.push({...card,index:gpus.length,unified:false,backend:'vulkan'});
}catch{}
}
gpus.forEach((g,i)=>{g.index=i;});
+168 -19
View File
@@ -32,7 +32,8 @@ import { findBrowser, findPlaywrightMcp } from './agent/browser-probe.mjs';
*/
import http from 'node:http';
import { homedir } from 'node:os';
import { cpSync, createReadStream, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
import { statfsSync, cpSync, createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
import { pipeline } from 'node:stream/promises';
import { spawn } from 'node:child_process';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
@@ -45,12 +46,15 @@ import * as engine from './engine.mjs';
import * as monitor from './monitor.mjs';
import * as serving from './serving.mjs';
import * as store from './store.mjs';
import { createChatStore, storeFailure } from './chat-store.mjs';
import { makePrivateDir } from './private-file.mjs';
import * as supervisor from './supervisor.mjs';
import * as engineInstall from './engine-install.mjs';
import { detectBackend, currentBackend, streamChat, inferenceLine } from './inference.mjs';
import { CONTEXT_TOKENS, VARIANTS, localModels, variant } from './catalog.mjs';
import { browseLocalModels, inspectLocalModel } from './local-models.mjs';
import { EventHub, error, json, readBody, readRaw, uid } from './util.mjs';
import { keepStreamAlive } from './stream-beat.mjs';
import { EventHub, badRequest, error, json, readBody, readRaw, requestUrl, sameSecret, uid } from './util.mjs';
import { createUploads } from './chat-uploads.mjs';
import { createNativeDrops } from './native-drop.mjs';
import { createVisionProbe, effectiveVision, readsPicturesByDefault } from './vision-probe.mjs';
@@ -61,7 +65,7 @@ import { createForkTimeskip } from './timeskip-fork.mjs';
import { prepareOversized } from './oversize.mjs';
import { createChatFolders, chatRootOf, openFile, resolveChatFile, previewFile, rawFile, createAttachments, withAttachments } from './chat-files.mjs';
import { createLlmProxy, samplingFromExpert } from './agent/llm-shape.mjs';
import { checkUpdates, checkComponentUpdates } from './updates.mjs';
import { checkUpdates, checkComponentUpdates, engineFirstRun } from './updates.mjs';
import { plainNoModel } from './no-model-text.mjs';
import { createAppUi } from './app-ui.mjs';
import { appVersionFrom, appVersionToShow } from './app-version.mjs';
@@ -69,8 +73,11 @@ import { createImageModels } from './image-models.mjs';
import { createRuntimeModels } from './runtime-models.mjs';
import { createNativeControl } from './native-control.mjs';
import { nativeUnloadPlan } from './storage-check.mjs';
import { installBundledEngine } from './bundled-engine.mjs';
import { installBundledEngine, cleanupAfterInstall } from './bundled-engine.mjs';
import { systemLocale } from './region.mjs';
import { createShellNotifier, DEFAULT_PORT, parsePortNotice, readShellPrefs, writeShellPrefs } from './shell-prefs.mjs';
import { findControl, createControl, componentsDirectory } from './control-runtime.mjs';
import { listEngines, changeEngine } from './engine-library.mjs';
import { forbidden as acquisitionForbidden } from './acquisition.mjs';
import { randomBytes, timingSafeEqual } from 'node:crypto';
import { nativeGateway } from './native-gateway.mjs';
@@ -114,7 +121,9 @@ const WORKSPACE_NAME = process.env.STRATA_WORKSPACE ? basename(WORKSPACE) : 'Str
const insideApp = path => { const rel = relative(ROOT, resolve(path)); return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel)); };
const hub = new EventHub();
const modelProviders = createModelProviders({ directory: store.DATA_DIR, changed: value => hub.send('providers', withPictureAnswers(value)) });
/** The chats, as files in the profile's data folder (chat-store.mjs). */
const chatStore = createChatStore({ dir: store.DATA_DIR, privateDir: makePrivateDir });
const modelProviders = createModelProviders({ directory: store.DATA_DIR, knownThinks: (connection, modelId) => visionProbe.knownThinks(connection, modelId), changed: value => hub.send('providers', withPictureAnswers(value)) });
const imageModels = createImageModels({ dataDir: store.DATA_DIR, modelsDir: () => store.MODELS_DIR, real: models.REAL, changed: value => hub.send('image-models', value) });
/* The installed app ships Strata Control (python + strata_control) next to the host; a source checkout has none and keeps its older paths. */
const CONTROL = findControl();
@@ -126,10 +135,26 @@ const appUi = createAppUi({ shutdown: () => shutdown() });
/* The app's version has one source: the desktop shell passes it (STRATA_APP_VERSION, from apps/desktop/src-tauri/Cargo.toml). Outside the app it is read from the same file in a checkout, else 'dev'. */
function appVersion() { return appVersionFrom({ env: process.env, here: HERE }); }
const appUpdater = appUi.updater;
const shellNotifier = createShellNotifier();
/** What the page needs for the desktop settings: whether this is the desktop app, the port in use, the saved choices and a busy-port notice. */
function shellState() {
const saved = readShellPrefs();
const desktop = !!shellNotifier;
return {
desktop,
tray: desktop && process.platform !== 'linux',
port: PORT, defaultPort: DEFAULT_PORT,
pinnedPort: saved.port || null, minimizeToTray: saved.tray,
portNotice: parsePortNotice(process.env.STRATA_PORT_NOTICE),
};
}
/* The engine that ships inside the installer is put in the components folder when this PC has none (or an older one); no network, hashes checked. The model list waits for it. */
const bundledEngine = control && !process.env.STRATA_NO_BUNDLED_ENGINE
? installBundledEngine({ bundleDir: process.env.STRATA_ENGINE_BUNDLE_DIR || resolve(HERE, '..', '..', 'engine-bundle'), components: componentsDirectory(), log: line => console.log(line) })
: Promise.resolve({ action: 'none' });
const engineBundleDir = process.env.STRATA_ENGINE_BUNDLE_DIR || resolve(HERE, '..', '..', 'engine-bundle');
/* An update from a per-GPU installer leaves its engine-bundle behind; it goes only when the same engine is complete in the store (sizes and hashes). */
cleanupAfterInstall(bundledEngine, { bundleDir: engineBundleDir, components: componentsDirectory() });
let bundledEngineSettled = !(control && !process.env.STRATA_NO_BUNDLED_ENGINE);
bundledEngine.then(() => { bundledEngineSettled = true; }, () => { bundledEngineSettled = true; });
/* A build made for another make of graphics card is not unpacked; Control's answer then carries the plain sentence (with the right download) instead of its own. */
@@ -220,6 +245,7 @@ models.init(hub);
engine.init(hub);
engineInstall.init(hub);
monitor.init(hub, boot);
monitor.setLiveProbe(() => native.hasLive());
serving.init(hub, { port: PORT, host: HOST });
runtimeModels=createRuntimeModels({hardware:()=>hardware.cachedHardware()??boot,library:()=>models.library(),requests:()=>serving.state().requests,real:models.REAL,changed:value=>{
if(!models.REAL){const primary=value.instances.find(i=>i.id===value.defaultId);if(store.get().loaded!==(primary?.variantId??null))store.update({loaded:primary?.variantId??null});}
@@ -553,17 +579,18 @@ function clientName(req) {
return 'Local client';
}
async function runRemoteTurn({ selection, message, history, images = [], onFrame, signal, workspaceId }) {
async function runRemoteTurn({ selection, message, history, images = [], thinking, onFrame, signal, workspaceId }) {
const controller = new AbortController();
const requestSignal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
let entry;
const started = Date.now();
try {
const { connection, model } = modelProviders.resolve(selection);
if (connection.protocol === 'openai-chat' && model.reasoning == null) await visionProbe.check(connection, model.id);
const appliedSkills = selectSkills(skillState, message, workspaceId);
entry = serving.open({ client: 'Strata chat', endpoint: connection.protocol === 'anthropic' ? '/messages' : connection.protocol === 'openai-responses' ? '/responses' : '/chat/completions', model: `${connection.name} · ${model.name}`, cancel: () => controller.abort(new DOMException('Cancelled by the user', 'AbortError')) });
serving.mark(entry.id, { state: 'generating' });
const result = await modelProviders.stream({ selection, message, history, images, signal: requestSignal, systemPrompt: ['You are Strata, a helpful assistant. Answer clearly. Do not claim to have read files or performed actions unless their results are included in the conversation.', skillInstructions(appliedSkills)].filter(Boolean).join('\n\n'), onFrame: frame => {
const result = await modelProviders.stream({ selection, message, history, images, thinking, signal: requestSignal, systemPrompt: ['You are Strata, a helpful assistant. Answer clearly. Do not claim to have read files or performed actions unless their results are included in the conversation.', skillInstructions(appliedSkills)].filter(Boolean).join('\n\n'), onFrame: frame => {
if (frame.type === 'start') noteSkillUse(appliedSkills);
if (frame.type === 'done') serving.mark(entry.id, { tokensIn: frame.tokensIn, tokensOut: frame.tokensOut, usageSource: frame.usageSource });
onFrame({ ...frame, requestId: entry.id, model: model.name, contextMax: model.contextMax, remoteModel: true, ...(frame.type === 'start' ? { skills: appliedSkills.map(s => s.name) } : {}) });
@@ -626,7 +653,7 @@ async function runAgentTurn({ res, controller, chatId, body, message, workspaceI
}
async function runTurn({ message, history = [], onFrame, signal, client, workspaceId = null, sessionId = null, remoteModel = null, runtimeModelId = undefined, nativeModelId, images=[], useTools=false, thinking='medium', thinkingBudget=0 }) {
if (remoteModel) return runRemoteTurn({ selection: remoteModel, message, history, images, onFrame, signal, workspaceId });
if (remoteModel) return runRemoteTurn({ selection: remoteModel, message, history, images, thinking, onFrame, signal, workspaceId });
if(native.hasLive()||nativeModelId){
let instance;try{instance=native.resolveInstance(nativeModelId??native.state().defaultId);}catch(e){const message=plainNoModel(e.message);onFrame({type:'error',message});return {error:message};}const started=Date.now(),cancel=new AbortController();
const entry=serving.open({client:client??'Strata chat',endpoint:'/chat/completions',model:instance.apiModelId,instanceId:instance.id,gpuIds:instance.gpus.map(String),cancel:()=>cancel.abort()});
@@ -921,7 +948,9 @@ const taskTimer = setInterval(() => tasks.tick(), 15000);
taskTimer.unref();
// The share and tunnel listeners take a free port (the next ones above the host's own) instead of a fixed PORT+1 / PORT+2 that Docker, WSL or Hyper-V may hold or reserve.
const sharePort = await findFreePort(Number(process.env.STRATA_SHARE_PORT ?? PORT + 1), '0.0.0.0', { skip: [PORT] });
// The probe listens on the loopback address only: a probe on 0.0.0.0 made Windows Defender Firewall ask "Allow Node.js to communicate on these networks?" on the
// first start of every new install path (QA 0213). Reserved or taken ports are caught on any address; the share listener itself binds 0.0.0.0 only when the person turns sharing on.
const sharePort = await findFreePort(Number(process.env.STRATA_SHARE_PORT ?? PORT + 1), '127.0.0.1', { skip: [PORT] });
const localShare = createLocalShare({ handle: openai, hasKey: () => !!store.get().serving?.apiKey, port: sharePort, changed: () => serving.setSharing(localShare.state()) });
const tunnelPort = await findFreePort(Number(process.env.STRATA_TUNNEL_PORT ?? PORT + 2), '127.0.0.1', { skip: [PORT, sharePort] });
const tunnelOrigin = createLocalShare({ handle: openai, hasKey: () => !!store.get().serving?.apiKey, host: '127.0.0.1', port: tunnelPort });
@@ -932,7 +961,8 @@ await cloudflare.inspect();
/* --------------------------------------------------------------- routing --- */
const server = http.createServer(async (req, res) => {
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
const url = requestUrl(req);
if (!url) return badRequest(res); // #4397: a bad Host or request target is a 400, never an unhandled rejection
const path = url.pathname;
const method = req.method ?? 'GET';
@@ -963,12 +993,55 @@ const server = http.createServer(async (req, res) => {
return await staticFile(req, res, path);
} catch (err) {
console.error('[strata]', err);
if (res.headersSent) { res.end(); return; } // a stream already began: it ends here (a second writeHead would throw out of this handler and end the process)
error(res, 500, err.message ?? 'Internal error');
}
});
async function api(req, res, path, method, url) {
if(path==='/api/fonts'&&method==='GET')return json(res,200,await systemFonts());
// --- chats: one file each in the data folder -----------------------------
// Not held back by a model folder move: saving a chat has nothing to do with the models. A page must name itself (x-strata-client), which
// a web page from another site cannot do without the browser asking the host first.
const chatsRoute = /^\/api\/chats(?:\/([^/]+))?$/.exec(path);
if (chatsRoute) {
if (!req.headers['x-strata-client']) return error(res, 403, 'Not allowed.');
let id = null;
try { id = chatsRoute[1] ? decodeURIComponent(chatsRoute[1]) : null; } catch { return error(res, 400, 'That is not a chat id.', { code: 'bad-id' }); }
const from = typeof req.headers['x-strata-window'] === 'string' ? req.headers['x-strata-window'].slice(0, 64) : null;
try {
if (!id && method === 'GET') return json(res, 200, chatStore.list());
if (id === 'import' && method === 'POST') {
const body = JSON.parse((await readRaw(req, 512 * 1024 * 1024)).toString('utf8') || '{}');
const result = await chatStore.importMany(body.sessions);
for (const imported of result.imported) hub.send('chats', { id: imported, rev: chatStore.rev(imported), from });
return json(res, 200, result);
}
if (id && method === 'GET') {
const text = await chatStore.read(id);
if (text === null) return error(res, 404, 'No such chat.', { code: 'not-found' });
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'content-length': Buffer.byteLength(text), 'cache-control': 'no-store' });
res.end(text);
return undefined;
}
if (id && method === 'PUT') {
const text = (await readRaw(req, 256 * 1024 * 1024)).toString('utf8');
const base = req.headers['x-strata-base-rev'];
const saved = await chatStore.save(id, text, { baseRev: typeof base === 'string' && /^\d+$/.test(base) ? Number(base) : null });
hub.send('chats', { id, rev: saved.rev, from });
return json(res, 200, saved);
}
if (id && method === 'DELETE') {
const removed = await chatStore.remove(id);
hub.send('chats', { id, deleted: true, from });
return json(res, 200, removed);
}
} catch (e) {
const failure = e instanceof SyntaxError ? { status: 400, message: 'The request must be JSON.', code: 'bad-json' } : storeFailure(e);
return error(res, failure.status, failure.message, { code: failure.code });
}
return error(res, 405, 'Method not allowed.');
}
if (storageChanging && !['GET', 'OPTIONS'].includes(method)) return error(res, 409, 'Wait for the model folder check to finish.');
// --- events -------------------------------------------------------------
if (path === '/api/events') return hub.add(res);
@@ -989,6 +1062,7 @@ async function api(req, res, path, method, url) {
if(path==='/api/native/models-folder/leave'&&method==='POST')return json(res,200,await native.leaveInterruptedMove());
if(path==='/api/native/plan'&&method==='POST')return json(res,200,await native.plan(await readBody(req)));
if(path==='/api/native/operations'&&method==='POST'){if(storageChanging)return error(res,409,'Wait for the model folder check to finish.');const body=await readBody(req);return json(res,202,native.operation(body.kind,body.args));}
if(path==='/api/native/engine-restart/dismiss'&&method==='POST')return json(res,200,native.dismissRestart());
if(path==='/api/native/logs'&&method==='GET')return json(res,200,{logs:native.logs()});
if(path==='/api/native/logs/save'&&method==='POST'){
/* Diagnostics to a file: the engine messages the app kept, failed operations, and the tail of each model's server log under Control.
@@ -1022,11 +1096,11 @@ async function api(req, res, path, method, url) {
if(nativeOperation&&method==='POST')return json(res,200,await native.cancel(nativeOperation[1]));
const nativeInstance=/^\/api\/native\/instances\/([a-z0-9-]+)\/(unload|default)$/.exec(path);
if(nativeInstance&&method==='POST')return json(res,200,nativeInstance[2]==='unload'?await native.unload(nativeInstance[1]):native.setDefault(nativeInstance[1]));
const nativeSettings=/^\/api\/native\/models\/([a-z0-9_-]+)\/settings$/.exec(path);
const nativeSettings=/^\/api\/native\/models\/([a-z0-9_.-]+)\/settings$/.exec(path);
/* Expert mode shows what will actually run: the saved tuning's values come with the settings (null when none applies or unreadable). */
if(nativeSettings&&method==='GET'){const settings=native.settings(nativeSettings[1]);let tuned=null;try{const t=await native.tuneEffective({model:nativeSettings[1],resourceProfile:url.searchParams.get('resourceProfile')||undefined,vision:url.searchParams.get('vision')||undefined});tuned=t.applied?t:null;}catch{tuned=null;}return json(res,200,{...settings,tuned});}
if(nativeSettings&&method==='PATCH')return json(res,200,native.save(nativeSettings[1],await readBody(req)));
const nativeSettingsFile=/^\/api\/native\/models\/([a-z0-9_-]+)\/settings-file$/.exec(path);
const nativeSettingsFile=/^\/api\/native\/models\/([a-z0-9_.-]+)\/settings-file$/.exec(path);
if(nativeSettingsFile&&method==='GET')return json(res,200,native.settingsFile(nativeSettingsFile[1]));
if(nativeSettingsFile&&method==='POST')return json(res,200,await native.openSettingsFile(nativeSettingsFile[1]));
@@ -1172,9 +1246,7 @@ async function api(req, res, path, method, url) {
}
// One revision attempt for a job this host started: the app's assistant works inside the candidate folder.
if (path === '/api/internal/timeskip-revise' && method === 'POST') {
const given = Buffer.from(String(req.headers['x-strata-internal'] ?? ''));
const want = Buffer.from(INTERNAL_SECRET);
if (given.length !== want.length || !timingSafeEqual(given, want)) return error(res, 403, 'Not allowed.');
if (!sameSecret(req.headers['x-strata-internal'], INTERNAL_SECRET)) return error(res, 403, 'Not allowed.');
const body = await readBody(req);
const cwd = String(body.cwd ?? '');
// Only the job's own candidate folders may be worked in.
@@ -1284,8 +1356,7 @@ async function api(req, res, path, method, url) {
// The shell (and nothing else) holds the secret that lets it end the host or tell it which window is in use.
const fromShell = () => {
const token = process.env.STRATA_SHUTDOWN_TOKEN ?? '';
const given = String(req.headers['x-strata-shutdown'] ?? '');
return token.length >= 32 && given.length === token.length && timingSafeEqual(Buffer.from(given), Buffer.from(token));
return token.length >= 32 && sameSecret(req.headers['x-strata-shutdown'], token);
};
// Which window the person is using: the page shows "Quit Strata" in browser mode only.
if (path === '/api/app/mode' && method === 'GET') return json(res, 200, appUi.state());
@@ -1327,6 +1398,36 @@ async function api(req, res, path, method, url) {
}
if (path === '/api/setup' && method === 'GET') return json(res, 200, setupState());
if (path === '/api/engine/first-run' && method === 'GET') {
if (!control?.updateCheck) return json(res, 200, { status: 'unavailable', gpus: [] });
const info = await engineFirstRun({ control, gpus: hardware.cachedHardware()?.gpus ?? [] });
try { let at = componentsDirectory(); while (at && !existsSync(at) && dirname(at) !== at) at = dirname(at); const f = statfsSync(at); info.freeBytes = Number(f.bavail) * Number(f.bsize); } catch { /* unknown: no warning */ }
return json(res, 200, info);
}
if (path === '/api/engines' && method === 'GET') {
const { status, body } = await listEngines(control);
return json(res, status, body);
}
if ((path === '/api/engines/use' || path === '/api/engines/remove') && method === 'POST') {
const { status, body } = await changeEngine(control, path.endsWith('/use') ? 'use' : 'remove', await readBody(req), { modelsRunning: () => native.state().instances.length > 0 });
if (status === 200) Promise.resolve(native.refresh?.(true)).catch(() => { /* the next poll reads it */ });
return json(res, status, body);
}
if (path === '/api/engine/job' && method === 'GET') {
// The newest engine download / file install, read without waiting for the native snapshot to connect (the first-run card needs it before that).
const kinds = ['update-engine', 'install-engine-file'];
return json(res, 200, { job: native.state().jobs.filter(j => kinds.includes(j.kind)).slice(-1)[0] ?? null });
}
if (path === '/api/engine/from-file' && method === 'POST') {
// The person chose an engine package in the page (the offline path). The bytes come here; Control checks and installs them.
const name = String(req.headers['x-file-name'] ?? 'engine.tar.gz').replace(/[^A-Za-z0-9._-]/g, '_').slice(-120);
if (!/\.(tar\.gz|tgz|tar\.xz|zip)$/i.test(name)) return error(res, 400, 'This is not a Strata engine package. Choose the .tar.gz or .zip file from the release page.');
const folder = join(store.DATA_DIR, 'incoming-engine');
mkdirSync(folder, { recursive: true, mode: 0o700 });
const target = join(folder, name);
await pipeline(req, createWriteStream(target, { mode: 0o600 }));
return json(res, 202, native.operation('install-engine-file', { path: target }));
}
if (path === '/api/updates/check' && method === 'GET' && control?.updateCheck) return json(res, 200, await checkComponentUpdates({ control, appUpdater, appVersion: process.env.STRATA_APP_VERSION || null }));
if (path === '/api/updates/check' && method === 'GET') return json(res, 200, await checkUpdates({ appVersion:appVersionToShow(setupState().app.version,native.state().version), engineVersion:native.state().available?(native.state().installed?.engine?.version??null):(await engine.config()).engine.version }));
@@ -1561,7 +1662,7 @@ async function api(req, res, path, method, url) {
if (cancelMatch && method === 'POST') return json(res, 200, serving.cancel(cancelMatch[1]));
// --- monitor ------------------------------------------------------------
if (path === '/api/monitor') return json(res, 200, monitor.snapshot());
if (path === '/api/monitor') { monitor.watch(); if (url.searchParams.get('drives') === '1') monitor.watchDrives(); return json(res, 200, monitor.snapshot()); }
// --- skills / workspace -------------------------------------------------
if (path === '/api/skills' && method === 'GET') {
@@ -1872,6 +1973,18 @@ async function api(req, res, path, method, url) {
try { return json(res, 200, agent.queueMessage({ chatId: String(body.sessionId ?? ''), text: body.message })); }
catch (e) { return error(res, e.code === 'queue-idle' ? 409 : 400, e.message); }
}
// The person's answer to a question the agent asked, or a skip (#4244).
if (path === '/api/chat/answer' && method === 'POST') {
const body = await readBody(req);
try { return json(res, 200, await agent.answerQuestion({ chatId: String(body.sessionId ?? ''), callId: String(body.callId ?? ''), answers: body.answers, skip: body.skip === true })); }
catch (e) { return error(res, e.code === 'question-gone' ? 409 : 400, e.message); }
}
// A message for one helper (#4749).
if (path === '/api/chat/helper' && method === 'POST') {
const body = await readBody(req);
try { return json(res, 200, await agent.messageHelper({ chatId: String(body.sessionId ?? ''), helperId: body.helperId, text: body.message, delivery: body.delivery })); }
catch (e) { return error(res, e.code === 'helper-idle' || e.code === 'helper-refused' ? 409 : 400, e.message); }
}
if (path === '/api/chat/queue/cancel' && method === 'POST') {
const body = await readBody(req);
try { return json(res, 200, agent.cancelQueued({ chatId: String(body.sessionId ?? ''), id: String(body.id ?? '') })); }
@@ -1905,12 +2018,47 @@ async function api(req, res, path, method, url) {
try { return json(res, 200, await agent.approve({ chatId: b.sessionId, callId: b.toolId, decision: b.decision })); }
catch (e) { return error(res, e.code === 'approval-gone' ? 409 : 400, e.message); }
}
/* The desktop shell's own choices (#3402, #3477): the port it asks for, the tray, and a desktop notification through the shell. */
if (path === '/api/locale' && method === 'GET') return json(res, 200, systemLocale());
if (path === '/api/shell' && method === 'GET') return json(res, 200, shellState());
if (path === '/api/shell/prefs' && method === 'POST') {
const b = await readBody(req);
try { writeShellPrefs(b); } catch (e) { return error(res, 400, e.message); }
return json(res, 200, shellState());
}
if (path === '/api/shell/notify' && method === 'POST') {
const b = await readBody(req);
if (!shellNotifier || typeof b.title !== 'string') return json(res, 200, { shown: false });
try { return json(res, 200, await shellNotifier.notify({ title: b.title, body: String(b.body ?? '') })); } catch { return json(res, 200, { shown: false }); }
}
/* The person changed the chat's mode while a turn runs: it applies to the next tool call, goal and helpers included. */
if (path === '/api/chat/mode' && method === 'POST') {
const b = await readBody(req);
if (typeof b.sessionId !== 'string' || !['manual', 'plan', 'bypass'].includes(b.mode)) return error(res, 400, 'sessionId and mode (manual, plan or bypass) are required');
try { return json(res, 200, await agent.setMode(b.sessionId, b.mode)); } catch (e) { return error(res, e.code === 'mode-unavailable' ? 409 : 502, e.message); }
}
/* A page that has just loaded asks which chats have a turn running (a reload does not stop a turn), and takes one over again: the catch-up
of what was said while no page was there, then the live frames, as on /api/chat/stream. A turn that ended while the page was away
(the page names it by requestId) is sent once, whole. 404 when there is nothing to take over. */
if (path === '/api/chat/active' && method === 'GET') return json(res, 200, { turns: agent.runningTurns() });
if (path === '/api/chat/attach' && method === 'POST') {
const b = await readBody(req);
if (typeof b.sessionId !== 'string' || !b.sessionId) return error(res, 400, 'sessionId is required');
const controller = new AbortController();
res.on('close', () => { if (!res.writableEnded) controller.abort(); });
let started = false;
const write = frame => {
if (res.writableEnded || res.destroyed) return;
if (!started) { started = true; res.writeHead(200, { 'content-type': 'application/x-ndjson; charset=utf-8', 'cache-control': 'no-cache, no-transform', connection: 'keep-alive' }); keepStreamAlive(res); }
res.write(`${JSON.stringify(frame)}
`);
};
const attached = agent.attach({ chatId: b.sessionId, requestId: typeof b.requestId === 'string' ? b.requestId : null, onFrame: write, signal: controller.signal });
if (!attached) return error(res, 404, 'There is no turn to take over.', { code: 'no-turn' });
await Promise.race([attached.ended, new Promise(r => controller.signal.addEventListener('abort', r, { once: true }))]);
if (!res.writableEnded) res.end();
return undefined;
}
/* Stop one chat's turn. Resolves once the turn has really ended, not when the cancel was admitted. */
if (path === '/api/chat/stop' && method === 'POST') {
const b = await readBody(req);
@@ -1940,6 +2088,7 @@ async function api(req, res, path, method, url) {
'cache-control': 'no-cache, no-transform',
connection: 'keep-alive',
});
keepStreamAlive(res); // #4878: a quiet turn stays open; the timer ends with the response
const controller = new AbortController();
res.on('close', () => { if (!res.writableEnded) controller.abort(); });
@@ -2156,7 +2305,7 @@ async function staticFile(req, res, path) {
if (!existsSync(DIST)) {
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
res.end(
`<!doctype html><meta charset="utf-8"><title>Strata host</title>
`<!doctype html><meta charset="utf-8"><title>Strata</title>
<body style="font:16px/1.6 -apple-system,system-ui;background:#0e1113;color:#eef1f3;padding:48px">
<h1 style="margin:0 0 8px">Strata host is running</h1>
<p style="color:#8e979f">No built web app found at <code>${DIST}</code>.</p>
+1 -1
View File
@@ -4,7 +4,7 @@ import { createHash } from 'node:crypto';
import { basename, dirname, extname, isAbsolute, join } from 'node:path';
import { homedir } from 'node:os';
import { readGGUF } from './gguf.mjs';
const ARCHITECTURES=new Set(['qwen2','qwen2moe','qwen3','qwen3moe','qwen3next','qwen35','qwen35moe','llama','gemma','gemma2','gemma3','phi3']);
const ARCHITECTURES=new Set(['qwen2','qwen2moe','qwen3','qwen3moe','qwen3next','qwen35','qwen35moe','qwen4exp','llama','gemma','gemma2','gemma3','phi3']);
const QUANTS={0:'F32',1:'F16',2:'Q4_0',3:'Q4_1',7:'Q8_0',8:'Q5_0',9:'Q5_1',10:'Q2_K',11:'Q3_K_S',12:'Q3_K_M',13:'Q3_K_L',14:'Q4_K_S',15:'Q4_K_M',16:'Q5_K_S',17:'Q5_K_M',18:'Q6_K',19:'IQ2_XXS',20:'IQ2_XS',21:'Q2_K_S',22:'IQ3_XS',23:'IQ3_XXS',24:'IQ1_S',25:'IQ4_NL',26:'IQ3_S',27:'IQ3_M',28:'IQ2_S',29:'IQ2_M',30:'IQ4_XS',31:'IQ1_M',32:'BF16'};
const clean=value=>String(value??'').replace(/[\x00-\x1f\x7f]/g,' ').trim().slice(0,160);
function fullPath(input){if(typeof input!=='string'||!input.trim()||input.includes('\0'))throw new Error('Choose a model file or enter its full path.');const path=input.trim().replace(/^~(?=$|[\\/])/,homedir());if(!isAbsolute(path))throw new Error('Enter a full path.');try{return realpathSync(path);}catch{throw new Error('This file or folder is unavailable. Check its location and permissions.');}}
+19 -2
View File
@@ -3,6 +3,7 @@ import http from 'node:http';
import os from 'node:os';
import { apiPath } from './api-paths.mjs';
import { listenProblem } from './ports.mjs';
import { badRequest, requestUrl } from './util.mjs';
/** Sharing without a key is allowed (owner decision, 0.2.2): the person is told once, in plain words, and the app keeps offering to make a key. */
export const NO_KEY_WARNING = 'No API key is set, so any device on this network can use your model. Create an API key to protect it.';
export function createLocalShare({ handle, hasKey, changed = () => {}, host = '0.0.0.0', port = 8792 }) {
@@ -13,9 +14,25 @@ export function createLocalShare({ handle, hasKey, changed = () => {}, host = '0
if (active) return state();
if (listener) throw new Error('Sharing is already starting.');
listener = http.createServer(async (req, res) => {
const path = apiPath(new URL(req.url ?? '/', 'http://localhost').pathname);
const url = requestUrl({ url: req.url, headers: {} }); // the Host header is not read here; a request target that is not a path is a 400 (#4397)
if (!url) return badRequest(res);
const path = apiPath(url.pathname);
const method = req.method ?? 'GET';
// #3208: with an API key set, a browser page of any site can call the shared API - the key decides, not the
// origin. The browser's preflight (OPTIONS) never carries the key (the spec says so), so it is answered here,
// before the path check and the key check; the real request that follows still goes through both. Without a
// key no CORS headers go out, as before: the listener stays "a device on this network" (SECURITY.md), and a
// web page of another site on the internet cannot read the model through it.
if (hasKey()) {
res.setHeader('access-control-allow-origin', '*');
res.setHeader('access-control-allow-methods', 'GET, POST, OPTIONS');
// the headers the preflight asks for (SDKs add their own; Anthropic browser clients send anthropic-version)
res.setHeader('access-control-allow-headers', String(req.headers['access-control-request-headers'] ?? '') || 'authorization, content-type, x-api-key, x-strata-client, anthropic-version, anthropic-beta');
res.setHeader('access-control-max-age', '600');
if (method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
}
if (!['/v1/models','/v1/chat/completions','/v1/messages'].includes(path)) { res.writeHead(404); res.end('Not found'); return; }
try { await handle(req, res, path, req.method ?? 'GET'); }
try { await handle(req, res, path, method); }
catch { if (!res.headersSent) res.writeHead(500); res.end('Request failed'); }
});
try { await new Promise((resolve,reject) => { listener.once('error',reject); listener.listen(port,host,resolve); }); }
+3 -3
View File
@@ -5,15 +5,15 @@
*/
import { basename } from 'node:path';
const PART_WORDS = { engine: 'the engine program', 'tokenizer/vocab.json': 'the tokenizer', 'tokenizer/merges.txt': 'the tokenizer', 'tokenizer/token_type.json': 'the tokenizer' };
const PART_WORDS = { engine: 'the engine program', 'component:strata-engine': 'the engine program', 'tokenizer/vocab.json': 'the tokenizer', 'tokenizer/merges.txt': 'the tokenizer', 'tokenizer/token_type.json': 'the tokenizer' };
/** Short readable names, no duplicates, most useful first. */
export function missingNames(controlParts, installerFiles) {
const names = [];
const add = text => { const t = String(text ?? '').trim(); if (t && !names.includes(t)) names.push(t); };
for (const file of Array.isArray(installerFiles) ? installerFiles : []) add(basename(String(file).split(String.fromCharCode(92)).join('/')));
for (const file of Array.isArray(installerFiles) ? installerFiles : []) { const name = basename(String(file).split(String.fromCharCode(92)).join('/')); add(PART_WORDS[name] ?? name); }
for (const item of Array.isArray(controlParts) ? controlParts : []) {
add(PART_WORDS[item?.part] ?? item?.file ?? item?.part);
add(PART_WORDS[item?.part] ?? PART_WORDS[item?.file] ?? item?.file ?? item?.part);
}
return names;
}
+9 -2
View File
@@ -1,5 +1,6 @@
/** Saved API connections. Secrets never leave the host's private credentials file. */
import { modelReadsPictures, modelSupportsReasoning } from './vision-probe.mjs';
import { isOpenRouter, OPENROUTER_EFFORTS } from './agent/profile.mjs';
import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync } from 'node:fs';
import { join } from 'node:path';
import { randomUUID } from 'node:crypto';
@@ -49,7 +50,7 @@ export function normalizeBaseUrl(value) {
const publicConnection = ({ apiKey, ...connection }) => ({ ...connection, apiKeySet: !!apiKey });
const numeric = value => typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null;
export function createModelProviders({ directory, changed = () => {}, fetchImpl = fetch }) {
export function createModelProviders({ directory, changed = () => {}, fetchImpl = fetch, knownThinks = () => null }) {
mkdirSync(directory, { recursive: true });
const file = join(directory, 'model-providers.json');
let connections = [];
@@ -142,7 +143,7 @@ export function createModelProviders({ directory, changed = () => {}, fetchImpl
const models = [...new Map(rows.filter(m => text(m.id, 256)).map(m => [m.id, { id: text(m.id, 256), name: text(m.display_name ?? m.name) || text(m.id, 256), contextMax: numeric(m.context_length ?? m.max_context_length ?? m.context_window), maxOutput: numeric(m.max_output_tokens ?? m.max_completion_tokens ?? m.top_provider?.max_completion_tokens), vision: modelReadsPictures(m), reasoning: modelSupportsReasoning(m) }])).values()];
return { models: models.sort((a, b) => a.name.localeCompare(b.name)) };
}
async function stream({ selection, message, history = [], systemPrompt = '', images = [], signal, onFrame }) {
async function stream({ selection, message, history = [], systemPrompt = '', images = [], thinking, signal, onFrame }) {
const { connection, model } = resolve(selection);
const started = Date.now();
const messages = history.filter(m => ['user', 'assistant'].includes(m.role) && typeof m.content === 'string').slice(-40).map(m => ({ role: m.role, content: m.content }));
@@ -155,6 +156,12 @@ export function createModelProviders({ directory, changed = () => {}, fetchImpl
: protocol === 'openai-responses'
? { model: model.id, input: messages, instructions: systemPrompt, stream: true, store: false }
: { model: model.id, messages: [...(systemPrompt ? [{ role: 'system', content: systemPrompt }] : []), ...messages], stream: true, ...(['openai', 'deepseek', 'openrouter', 'google', 'moonshotai'].includes(connection.provider) ? { stream_options: { include_usage: true } } : {}) };
// Match the native composer's effort names. Unknown support and other protocols keep provider defaults.
const choice = typeof thinking === 'string' ? thinking.toLowerCase() : '';
if (protocol === 'openai-chat' && (model.reasoning ?? knownThinks(connection, model.id)) === true && ['off', 'low', 'medium', 'high', 'max'].includes(choice)) {
if (isOpenRouter(connection)) payload.reasoning = { effort: OPENROUTER_EFFORTS[choice] };
else payload.reasoning_effort = ({ off: 'none', max: 'high' })[choice] ?? choice;
}
const requestSignal = signal ? AbortSignal.any([signal, AbortSignal.timeout(10 * 60 * 1000)]) : AbortSignal.timeout(10 * 60 * 1000);
const res = await request(connection, protocol === 'anthropic' ? 'messages' : protocol === 'openai-responses' ? 'responses' : 'chat/completions', { method: 'POST', body: JSON.stringify(payload), signal: requestSignal });
if (!res.body) fail('The model API returned an empty response.');
Loaded 100 of 1713 files, more files were not shown because too many files have changed in this diff. Show more