Compare commits

...
943 Commits
Author SHA1 Message Date
Niko1221 9de04cd44e Control 0.1.46.3: version bump for the Terminal/Control-only update (engine packages unchanged) 2026-10-20 23:56:06 +02:00
Niko1221 31ca248fbe Progress words: a file being repaired after a wrong SHA-256 says so in plain words
Python only (strata_control/progress_words.py, test).
2026-10-20 23:55:59 +02:00
Niko1221 63214a5655 Downloads: every ranged answer is checked, a file with a wrong SHA-256 is repaired piece by piece instead of deleted
A 39.8 GB shard came out with the wrong SHA-256 on 0.2.1 and was deleted, 40 GB of download lost.

- every ranged answer must be a 206 with exactly the Content-Range asked for (start, end and total), a fitting
  Content-Length and the same ETag as the first answer; a changed file part-way stops the download and drops the .part
- the raw unbuffered write is looped until every byte is down (its return value was ignored)
- a .part records the address and ETag it came from (.part.src); one from another mirror or revision is dropped, not mixed
- a parallel download always has a piece list before the first byte is written; if none can be kept it uses one stream,
  so holes in the sparse file are never read as a finished prefix
- the one-stream resume accepts a 206 only at the byte asked for; another range restarts from zero
- a finished file with a pinned hash but no finish mark is checked, not marked; the rename waits for a virus scanner
- a SHA-256 mismatch re-reads the file from the server piece by piece, rewrites only the blocks that differ and checks
  again ("Some parts of the file were damaged; downloading only those parts again"); deleted only if still wrong
- update archives (strata_control/transfer.py): a resumed answer is appended only when its Content-Range starts at the byte asked for
- tests: tests/terminal/test_download_integrity.py (a local server that answers 200 for ranges, wrong ranges and totals,
  cut bodies, another ETag, 503s, flipped bytes)

Python only (setup.py, strata_control/transfer.py, tests).
2026-10-20 23:55:59 +02:00
Niko1221 86620837b2 Updates: an AMD card with an unknown gfx never gets the newest HIP build by version; read the gfx from the Windows display adapters, choose by GPU name, keep the installed variant 2026-10-20 23:55:36 +02:00
Niko1221 cbfd5b926b Merge main (0.2.1.2 engine hotfix) into the 0.2.1.3 Terminal update 2026-10-20 19:12:29 +02:00
Niko1221 5b47f889b0 Test: the calibrate setup test's data_folder stand-in takes the extra arguments 2026-10-20 17:26:14 +02:00
Niko1221 5db632e72a Test: engine minimums are checked against the tree's CMake version, not a fixed 0.1.45 2026-10-20 17:26:14 +02:00
Niko1221 f5c4047e78 Test: the unsloth setup tests answer every disk-space question themselves 2026-10-20 17:26:14 +02:00
Niko1221 dea6920b81 hip-rdna3 on Windows: Strix Halo (gfx1151) keeps the expert arena unregistered by default; strata.exe finds rocm\bin for --version (delay-loaded ROCm math DLLs, bundled by package_windows.py) 2026-10-20 17:09:46 +02:00
Niko1221 9e24f4ab32 Control 0.1.46.2: version bump for the Terminal/Control-only update (engine packages unchanged) 2026-10-20 17:01:22 +02:00
Niko1221 dcbaa992d2 launch --plan: a part that is already on disk is not listed as a download, and a draft layer or picture reader found in the data folder is not counted 2026-10-20 17:00:31 +02:00
Niko1221 79cb59c574 Updater tests reset a signature verifier left behind by an earlier test file 2026-10-20 17:00:30 +02:00
Niko1221 ae456dfe5c iq_pack: an interrupted experts.bin write continues at the next layer (finished layers are flushed and recorded, the last kept one is checked against the GGUF) instead of starting from zero; says so 2026-10-20 17:00:30 +02:00
Niko1221 176e11a597 launch: a run by flags reuses the tuning result stored for the same model, choices and computer and says so; --retune measures again 2026-10-20 17:00:29 +02:00
Niko1221 5dc151d4d3 launch: setup step 6 (packing, converting) is no longer silent - the plain sentence for what it does is shown and a 'still working, N min so far' line comes every 30 s of silence; a cancel is seen at once 2026-10-20 17:00:29 +02:00
Niko1221 0dbea601a0 Download label: the total counts every shard and the picture reader, in the data folder setup is given (--data-dir), not the first shard only 2026-10-20 17:00:29 +02:00
Niko1221 3d5594d656 launch --plan: names the model your files are (title, size, picture setting, a copyable line with --model) instead of placeholders, and says what would be downloaded and how big (0 when everything is here, the draft layer and picture reader counted, engine mentioned) 2026-10-20 17:00:28 +02:00
Niko1221 284552fe26 Own model files: links to shards are accepted with or without --model, and a problem that applies to several files is said once (problem_words), in the Terminal, the screens and 'models import'; tests for items 2 and 3 2026-10-20 17:00:28 +02:00
Niko1221 c24f2a139f Own model files: a --model-path that is not there or holds no model is said in plain words before the engine or setup start (no 'missing /Qwen...gguf', no promise of a download), also in --plan and the screens 2026-10-20 17:00:28 +02:00
Niko1221 bd4c6bf1af Setup tests follow the merges: verify_sha256 takes own=, a tight disk margin warns and a real shortfall refuses 2026-10-20 17:00:27 +02:00
Niko1221 fa56c2b387 Start: a picture reader path from an older engine folder follows the installed package (engine update or other image no longer ends in server_exited) 2026-10-20 17:00:27 +02:00
Niko1221 cf34e915c4 Update rules: Control --version waits longer and tries twice, a busy card is not held against an engine (other failures expire after a day), pre-release Control handover, a wrong PC clock says so 2026-10-20 17:00:26 +02:00
Niko1221 14be49605f Models folder: a whole drive becomes D:\Strata (and D:\Strata\Models), the person's folder is written into the settings file Control reads (so the installer accepts it whatever it is called), no link where the disk cannot hold one, plain words for a folder that is gone or not writable; the Terminal remembers a chosen --data-dir; docs: moving models to another disk and what stays on C: 2026-10-20 17:00:26 +02:00
Niko1221 6b29adad1a strata launch without a real tty (Git Bash, mintty): say so and go on with plain questions instead of refusing 2026-10-20 17:00:26 +02:00
Niko1221 e728a132d7 POSIX owner-only checks: repair our own folders whose mode drifted, note drives without modes, and name the chown command for folders another account owns 2026-10-20 17:00:25 +02:00
Niko1221 32b9f75376 Tests: loopback.py is the one allowed opener for this computer's own servers 2026-10-20 17:00:25 +02:00
Niko1221 1972af6da1 Download and prepare steps say what they are: one set of plain sentences (draft layer download, packing experts, checking files, picture reader) for the app and the Terminal; the SHA-256 check shows its own figure instead of a download stuck at 99%; a step's heading is kept in view; the .part file no longer jumps the bar 2026-10-20 17:00:23 +02:00
Niko1221 7db513297d Launch: a tuning that found the standard settings already fastest counts as tuned (no 20-minute measurement at every start) 2026-10-20 17:00:08 +02:00
Niko1221 bf2926e05a Terminal and app checker: the folder holding the model is found above the picked one, links keep their model name, one plain wording for every refusal (file name + why) 2026-10-20 17:00:07 +02:00
Niko1221 4dee320dd5 setup --gguf-dir never deletes the person's own file: a size or SHA-256 mismatch stops with the file's name and why, and the file stays 2026-10-20 17:00:07 +02:00
Niko1221 c316a7c215 Fix: Windows runtime folder refused as not owner-only 2026-10-20 17:00:06 +02:00
Niko1221 73b68dfd88 Fix #3410: a Strata folder in the Trash is not remembered or offered as an install 2026-10-20 17:00:06 +02:00
ctr-137 ce938fb8c9 Fix #2971: a tool call no longer widens the web page on phones (from PR #3156) 2026-10-20 17:00:06 +02:00
Yash Raj Pandey fb2f387407 Fix #2496: an engine program that cannot be started is a 503 with the reason, not a dropped connection (from PR #2497) 2026-10-20 17:00:05 +02:00
xrip 9d2870d187 Fix #2289: Anthropic usage reports cache_creation_input_tokens, the prompt tokens read now and kept for the next request (from PR #2290) 2026-10-20 17:00:05 +02:00
xrip b611ec5f1e Fix #2141: honour llama-server's thinking_budget_tokens (from PR #2140) 2026-10-20 17:00:04 +02:00
MEsfits ddbbda4824 Fix #3275: calibrate a dual config whose gpu is the string 0,1 (from PR #3276; a single card given as a number keeps asking for that card) 2026-10-20 17:00:04 +02:00
Niko1221 386155fa25 Fix #3370: the vision work folder is made again when a temp cleaner removed it 2026-10-20 17:00:03 +02:00
Niko1221 ff64e08bba Fix #3403: the Mac setup installs cmake and ninja before it compiles 2026-10-20 17:00:03 +02:00
Niko1221 a569d80680 Fix #3187: calibrate checks the report folder before the sweep, not after it 2026-10-20 17:00:03 +02:00
Niko1221 461d0baee7 Fix #3188: strata_bench names the port it starts its own server on 2026-10-20 17:00:02 +02:00
Niko1221 ef5d8ab946 Fix #3302: owner-only file checks accept the built-in Administrator (SDDL alias LA) 2026-10-20 17:00:02 +02:00
Niko1221 bcda2e4b0d Fix #3298: --mtp with a depth number is not a folder in the model file checks 2026-10-20 17:00:02 +02:00
Niko1221 34d6c37df3 Disk space: stop only when the bytes do not fit, warn inside the working-room margin, a resumed download's partial file is not counted twice 2026-10-20 17:00:01 +02:00
Niko1221 f207907b14 Own model files: the checker in front of setup accepts what setup --gguf-dir accepted (Unsloth split sets with a metadata-only first shard, links, several sizes in a folder, a folder above the model folder, more than 128 files); Unsloth Q5/Q6/Q8 and Swift IQ3_S are in the table 2026-10-20 17:00:01 +02:00
Niko1221 7ebdd34356 Windows file in use: every state-file swap goes through atomic.replace (retry); a sharing violation no longer says run as yourself 2026-10-20 17:00:00 +02:00
Niko1221 aa44b9ce53 No engine for this PC: say it plainly (no graphics card Strata can use, or no engine for the card), no feed address as the next step 2026-10-20 17:00:00 +02:00
Niko1221 af189129a4 Calls to this computer's own servers skip the system proxy (tuner, tune session, launch self-check) 2026-10-20 17:00:00 +02:00
Niko1221 c6a0e739be Fix #3417: the hardware report counts physical cores, so the Work profile no longer asks for more CPU workers than the CPU has cores 2026-10-20 16:59:59 +02:00
Niko1221 e52668857c API key is optional beyond this computer: warn, never refuse
The server and strata serve start on 0.0.0.0 without a key and print one plain
warning. strata launch gets --no-api-key; the Access screen makes a key by
default and t turns it off. api_key "none" in a config means no key on
purpose. Cloudflare without a key gets a stronger warning. Docs updated.
2026-10-20 16:59:59 +02:00
Niko1221 c0c3a06ce4 Hand-over: a source checkout never hands over to an installed Control 2026-10-20 16:59:58 +02:00
Niko1221 f39d951904 Terminal hands over to the Control that strata update installed
The Terminal on PATH ignored components/control/active.json, so after
strata update the old Control kept running. strata.py now checks the
pointer first (stdlib only) and runs the newer, whole Control with its
own interpreter; never an older one, never outside the control folder,
no loop (STRATA_HANDOVER), one warning and a fallback if it is broken.
strata update says the next command already runs the new Control.
2026-10-20 16:59:58 +02:00
Niko1221 968fcb70b0 Drop the cached update notice when Control, engine or app version changed 2026-10-20 16:59:57 +02:00
Niko1221 9ae93f7b1f Control 0.1.46.1: version bump for the Windows long-path hotfix 2026-10-20 11:18:52 +02:00
Niko1221 9f13d1e78f Merge main (Docker commit fix) into the long-path hotfix 2026-10-20 11:15:54 +02:00
Niko1221 a0abdf7588 Engine install: survive Windows MAX_PATH (short staging folder, extended-length paths, plain file errors, deep-folder warning)
On a fresh Windows PC (long paths off) the AMD engine packages could not be unpacked: staging under .updates/staging/<12 hex> pushed the
longest hipBLASLt file past 259 characters, and the failure surfaced as 'local file operation failed (FileNotFoundError)'.

- staging is now <components>/.s/<4 hex>/engine/<folder>; Control stages the same way
- unpack, delete and verify go through extended-length paths (strata_control/longpath.py)
- OSError messages name the file and say so when the path is too long
- a plain warning (shown as a warn line by launch) when the install folder is too deep for the engine's own file loading
- tests: real 0.2.1 package path lengths for a 20 character user name
2026-10-20 11:15:20 +02:00
Niko1221 c093ac263f docker/build.sh: STRATA_BUILD_COMMIT overrides the commit it passes 2026-10-20 10:39:48 +02:00
Niko1221 436fbd0a6e Docker: pass the source commit into the terminal stage (no git in the image)
package_terminal.py reads STRATA_BUILD_COMMIT first and only then asks git, and no longer crashes when git is missing. build.sh passes the commit as a build arg, the Dockerfile turns it into that env.
2026-10-20 10:39:15 +02:00
Niko1221 5bda973a43 Docker: make docker/publish.sh executable (the image workflow failed with Permission denied) 2026-10-20 10:29:54 +02:00
Niko1221 90767252f4 Release notes 0.2.1: final two-card batch A/B result, drop TODOs 2026-10-20 09:40:48 +02:00
Niko1221 a7bdbfdabc Release notes 0.2.1: the #3237 drive probe is on by default (owner OK) 2026-10-20 06:27:42 +02:00
Niko1221 9944d2f7f1 Merge branch 'f-021-int' into f-021-rel 2026-10-20 05:56:29 +02:00
Niko1221 b42ec662c7 Release notes 0.2.1: the R9700 queue fix with the two-queue numbers 2026-10-20 05:56:26 +02:00
Niko1221 b35a9816c7 amd: open two GPU queues instead of five at start (keeps the fixed prompt speed, no decode loss on R9700) 2026-10-20 04:53:45 +02:00
Niko1221 d33bb832ca Release notes 0.2.1: engine 0.1.46 and feed sequence 102 (proposed), file-name and Docker TODOs resolved, tuning tables per package 2026-10-20 02:37:08 +02:00
Niko1221 3e7c5d8e1c build_hip_linux.sh: pick up lib/rocm_sysdeps when the ROCm tree has it (the gfx1151 halo package needed a patched copy in 0.2.0-beta) 2026-10-20 02:31:50 +02:00
Niko1221 894d1be1f4 Version bump for 0.2.1 (proposed): app 0.2.1, engine and Control 0.1.46
Cargo.toml and Cargo.lock, desktop and host package.json, PRODUCT_VERSION, CMake project and Metal STRATA_VERSION, build_hip_linux PKGVER default, About comment, tests that pin the number. tauri.conf.json has no version key (Tauri takes it from Cargo.toml). Download names in the READMEs, AMD_HIP.md and UPDATES.md follow.
2026-10-20 02:17:37 +02:00
Niko1221 93aa62ed64 Release notes 0.2.1: fill the iD, iE, zc and bimodal parts (measured defaults, fixes, known limits) 2026-10-20 02:14:44 +02:00
Niko1221 83027df587 Merge branch 'f-021-iD' into f-021-int 2026-10-20 02:13:21 +02:00
Niko1221 484e06b961 Linux file tier: a start-up drive probe overrides the room rule's file cache on a fast drive (#3237)
When the room rule picks the file cache but the cache holds under half of the read set, the engine now
times O_DIRECT preads (8 threads) against first touches of a mapping (3 threads) on random 1 MiB
pieces of the model file, once, and reads unbuffered when the direct path runs at 4 GB/s or more and
is 1.3x (under 30% held) / 1.5x (under 50%) faster than the cached path. The reporter's PCIe 5 NVMe
(8.18 vs 4.93 GB/s, 13% held) lands on unbuffered (2x prompts, 3-4x decode in their A/B). A 1.7-2.5
GB/s NVMe (P1004) stays on the cache, where it decodes 1.1-1.6x faster; a ratio alone could not
separate the two, hence the absolute floor. STRATA_FILE_TIER_PROBE=0 keeps the room rule alone.
Default change: proposed, not validated on a fast drive here. Decision in platform::file_tier_prefers_direct
(platform_memory_test).
2026-10-20 01:32:52 +02:00
Niko1221 ed4595ad69 Merge branch 'f-021-bimodal' into f-021-int 2026-10-20 01:20:25 +02:00
Niko1221 06d7604af7 Merge branch 'f-021-zc' into f-021-int 2026-10-20 01:07:32 +02:00
Niko1221 ecf2bdcb7f Linux: io prefetch + decode gate by default for experts.bin packs when the page cache has room for 55% of the experts
Zero-copy does not apply to experts.bin (one contiguous blob the kernels already read from the mapping), but the io_fill_ prefetch path does. RTX 3060, IQ3_XXS 43 GB pack, 32 GB cgroup: decode 1.38/1.21/1.33x (5/5 pairs), prompts 1.04/1.06x. At 16 GB the cold 4K prompt was 4-6% slower, so it stays off below the room test and the log says how to force it. Identity gates 10/10, exact_pp 4K/20K identical.
2026-10-20 01:07:19 +02:00
Niko1221 3f1fa67f14 amd: open the GPU queues in a fixed order at start (R9700 fast/slow prompt starts)
On gfx1200/gfx1201 under Linux the compute stream and the expert staging stream
landed on the same compute pipe in about half of the starts, which made 4K prompts
13% slower (p620, 1,980 vs 2,290 tok/s). Five queues (four normal, one highest) are
now opened one after the other on the main thread before any other thread starts.
STRATA_HIP_QUEUE_PRIME=0 turns it off. Answers are unchanged.
2026-10-20 01:04:48 +02:00
Niko1221 881a42adab Merge branch 'f-021-notes' into f-021-int 2026-10-20 00:48:22 +02:00
Niko1221 3616148bbf Merge branch 'f-021-iE' into f-021-int
# Conflicts:
#	apps/host/src/locales/de.json
#	apps/host/src/locales/en.json
#	apps/host/src/locales/es.json
#	apps/host/src/locales/ja.json
#	apps/host/src/locales/ko.json
#	apps/host/src/locales/pt.json
#	apps/host/src/locales/ru.json
#	apps/host/src/locales/zh.json
2026-10-20 00:48:09 +02:00
Niko1221 24e56539c5 Release notes 0.2.1 (draft): downloads table, what is new, measured defaults, fixes, privacy, known limits, TODO markers for the running streams 2026-10-20 00:47:45 +02:00
Niko1221 8e957cd53b SYCL: build.sh stops an AOT Battlemage build before the compile when ocloc is the one that crashes on the int8 kernels (Compute Runtime 26.22.38646.x, IGC 2.36.3); I8_ESIMD=OFF / STRATA_SYCL_I8_ESIMD builds without them; docs say docker build --pull (#3219) 2026-10-20 00:47:08 +02:00
Niko1221 4f6ec303b5 Merge branch 'f-021-tfix' into f-021-int 2026-10-20 00:44:55 +02:00
Niko1221 1f52a00373 apps/host staging-scripts-syntax test: use Git bash with forward-slash paths on Windows instead of the WSL launcher 2026-10-20 00:44:41 +02:00
Niko1221 e68a032a58 math test: the KaTeX render check skips when katex is not installed in a shared node_modules (#3268) 2026-10-20 00:43:04 +02:00
Niko1221 10715d856d gfx906 (HIP wave64) builds again: no Qwen3.6 engine (cuBLAS calls the compat layer does not map), GGML_USE_HIP on the W4A4 target, the trellis overlay sources and the moe_f16_kq stub in the HIP source list; all targets compile for gfx906 (#3172 #3271) 2026-10-20 00:41:38 +02:00
Niko1221 e45a9bfd3a Merge branch 'f-021-split' into f-021-int 2026-10-20 00:19:12 +02:00
Niko1221 34e7c5cb1a Merge branch 'f-021-iA' into f-021-int
# Conflicts:
#	apps/host/server/native-control.mjs
2026-10-20 00:12:03 +02:00
Niko1221 8aaf188aa9 Your own engine settings are kept: user_args, a reserve of 0 on a compute-only card, control vectors survive a load (#3215 #3270 #3216)
The app and setup rewrote the run config's "args" on every Load: the resource preference forced
--vram-reserve-mib to 1536 (2048 with pictures, a fifth of the card on "work") whatever the file said, and
a setup run (started by the Load when the context or pictures differ) dropped every option it did not
choose itself, the control vector options included.

- run config: "user_args" holds the options set by hand; "args_written" is the args as setup/the app last
  wrote them, so a hand edit of "args" is noticed and moved into user_args (strata_control/user_args.py,
  apps/host/server/native-user-args.mjs: the same rule)
- every start (serve/server.py engine_args), every setup run (setup.carry_user_args), every host write and load
  and a saved tuning put the person's options over the generated ones; the same option: theirs wins
- old files without args_written: setup keeps the options the old args had, the new ones lack and setup.py never names
  (plus the control vector group); a reserve that is not setup's own 700
- --vram-reserve-mib: any number the person set (Expert setting, user_args, the key, a value in args) is used as it
  is, 0 included; the safety default (1536/1024, 2048 with pictures, 20% on "work") stays for a card that drives a
  display or is not known to be headless; a card the probe says drives no display keeps 512 (700 with pictures);
  a 0.2.0-beta file where the app wrote the number itself is told from a changed one
- hardware: drives_display per card (nvidia-smi display_active; the Windows adapter with a desktop resolution for AMD)
- setup --vram-reserve-mib, the web settings page and `start --vram-reserve-mib` record the value as the person's
- docs/TROUBLESHOOTING.md: what is kept and what the app still chooses
2026-10-20 00:11:11 +02:00
Niko1221 22703894ec Merge branch 'f-021-iB' into f-021-int
# Conflicts:
#	apps/host/server/native-control.mjs
2026-10-20 00:06:49 +02:00
Niko1221 b1713b3ff2 docs: dual-socket CPU pool measurements (workers across sockets won on one box, lost on another), IOMMU check, no numactl pinning (#3201) 2026-10-20 00:06:38 +02:00
Niko1221 0875029d10 docs: two-card gfx906 tip, --pipeline-windows 2 (+36% decode) and --trim-stage-weights (+5% prompt) measured on 2x MI50, off by default (#3173) 2026-10-20 00:06:38 +02:00
Niko1221 952cce7752 GPU choice (0.2.1): Automatic skips cards the engine cannot run, one card sent as gpu, settings-file GPUs kept, readable dropdown, Monitor opens on assigned cards (#3203 #3195 #3256 #3221 #3211) 2026-10-20 00:06:22 +02:00
Niko1221 30c64f09d7 Test fixes after the 0.2.1 merges: the gfx1151 table parse stops before the iGPU set, the benchmark's model-card link is not a model-host fetch, docker/build.sh copies bench/prompts/standard-v1 for the Terminal package, the Docker script tests use Git bash with unix paths on Windows 2026-10-20 00:05:48 +02:00
Niko1221 cbc0e329c6 Trellis MMQ overlay follows the ggml-cuda it patches: the generated callbacks and the loader pass the precision argument when mmq.cuh has it (llama.cpp b11xxx), identical output on the pinned commit, off with a message for an unknown signature (#3217) 2026-10-20 00:03:13 +02:00
Niko1221 f680a80d88 Batch groups: PROPOSED NVIDIA two-card default is one serial group (measured +12..16% on 2x A4000); owner decides 2026-10-20 00:03:09 +02:00
Niko1221 d8a6a0e012 App settings: the Allow browser origins help says it does nothing in the app, that the app's API already allows pages of any site, and where cors_origins really goes; text in all 8 languages and in DETAILS (#3208) 2026-10-19 23:52:04 +02:00
Niko1221 3cc18d7bb7 Chat: LaTeX math is drawn with KaTeX (inline $..$ and \(..\), display $$..$$ and \[..\]); KaTeX and its woff2 fonts ship with the app and load only when an answer has a formula; money and code stay text (#3268) 2026-10-19 23:49:59 +02:00
Niko1221 1994f3acb2 FP4-Local: setup installs the pinned torch, transformers and safetensors the converter imports (requirements-nvfp4-local.txt) right before a prepare that runs; the tool names a missing package up front instead of after 20 minutes (#3227) 2026-10-19 23:44:24 +02:00
Niko1221 c911065a14 Windows installer: ask for the install folder (another drive when C: is short), keep the earlier folder in app updates and silent installs; docs name what stays on C: (#3186 #3190) 2026-10-19 23:43:47 +02:00
Niko1221 40c605f7bc Windows with a non-UTF-8 code page (Russian): PowerShell output is read as UTF-8 (Documents folder, save dialog, fonts, GPU names), Python runs in UTF-8 mode from the launchers and the host's children, and the engine and picture reader get a UTF-8 manifest so a Cyrillic model folder arrives intact (#3202) 2026-10-19 23:38:49 +02:00
Niko1221 96a9107a27 Downloads: the MTP fetch shows progress in the app and survives a flaky link (runs again, fewer connections, one writer per folder); a cancel and a killed setup end the whole tree on Windows (snapshot + the fetch ends with its parent); Gyro's low-RAM pack gets --compat-bf16 (#3257 #3214 #3193) 2026-10-19 23:38:38 +02:00
Niko1221 4e76d6c9a6 HIP prefill: no start-up hipBLAS warm-up when the cards are of more than one architecture (#3204)
f3be9743 warms the first BF16 and FP16 products of every device at start. On a layer split across a
gfx1151 and a gfx1201 card it slowed prompts about 25% (hc read of the gfx1151 stage 2.7 -> 9.0 s per
32K block; STRATA_PREFILL_WARM=0 restores it). Two cards of one architecture lose nothing (2x R9700,
interleaved pairs). The warm-up now runs only when every visible card has the same gcnArchName;
STRATA_PREFILL_WARM=1 asks for it anyway. Policy in warm_policy.hpp with warm_policy_test.
2026-10-19 23:37:11 +02:00
Niko1221 8441cfd0e2 App: a start that stopped for no VRAM left for the expert cache is tried once more with less kept free (#3192); a model on the person's own GGUF folder keeps that folder, so a reload does not download it (#3199) 2026-10-19 23:31:43 +02:00
Niko1221 9cc2a9b6aa docs: build from git on Linux (clone, setup, rebuild, trying a pull request, engine env, the monitor from a checkout, plain cmake) (#3191) 2026-10-19 23:27:09 +02:00
Niko1221 9b8436df4e tools: gguf-py is also looked up in the install folder (STRATA_INSTALL_DIR, the app's folder), so tools run by hand from the Control package find what setup.py downloaded; the error names STRATA_GGUF_PY (#3269) 2026-10-19 23:26:32 +02:00
Niko1221 c6a20b7e85 Linux: say it when a finite RLIMIT_RTTIME (PipeWire without rtkit) is the reason for an engine death by signal 9, warn at start, troubleshooting entry (#3238) 2026-10-19 23:25:48 +02:00
Niko1221 88b29d481a CPU pool: the LP-E cores (/sys/devices/cpu_lowpower/cpus) are efficiency cores, in the engine and in setup (#3262)
On Meteor Lake and Arrow Lake-H the LP-E cores are in neither the cpu_core nor the cpu_atom
list, so the pool and setup counted them as P-cores: a Core Ultra 7 255H read as 8P+8E, the
more-E-than-P default plan did not apply and 15 workers ran (-35%). A CPU in no list now falls
back to its capacity. Tests: pool_plan_test (helper), tools/test_setup_hybrid.py (fake /sys).
2026-10-19 23:25:46 +02:00
Niko1221 2efa4e067a Merge branch 'f-021-minipc' into f-021-int 2026-10-19 22:20:52 +02:00
Niko1221 eb7eac250b Radeon 780M: the matrix-core prompt set is the default on gfx1103 (integrated, Linux): prompts x1.6 at 1K and x2.0 at 4K, decode unchanged, same greedy ids; STRATA_IGPU_PROMPT_SET=0 turns it off; gfx1150/1151 and Windows stay off; docs note the MES reset issue; per-arch tests 2026-10-19 22:20:33 +02:00
Niko1221 9f5488e3b2 Merge branch 'f-021-arcvis' into f-021-int
# Conflicts:
#	docs/MODELS.md
#	strata_control/vision_reader.py
#	tools/vision/CMakeLists.txt
#	tools/vision/strata_vision.cpp
2026-10-19 21:42:12 +02:00
Niko1221 cac4816afd Intel Arc pictures: on by default for the Alchemist cards (CPU reader), Battlemage stays opt-in; SYCL GPU picture reader (strata-vision-sycl) for --vision gpu; reader rule in vision_reader.py; tests and docs 2026-10-19 21:40:50 +02:00
Niko1221 4ed8eb02fa hipBLASLt tuning table on by default on Strix Halo (gfx1151) only, the owner's choice; opt-in elsewhere 2026-10-19 21:37:42 +02:00
Niko1221 b203b5f71e arch_defaults: UMA arena is not a default (measured slower, resets not fixed): constant off, STRATA_ARENA_PIN_GIB=none stays opt-in; docs say the iGPU prompt set is opt-in with the 780M numbers 2026-10-19 21:37:25 +02:00
Niko1221 468e3a6252 Merge branch 'f-021-hip' into f-021-int 2026-10-19 21:34:18 +02:00
Niko1221 c6aa4223d9 AMD packages carry the hipBLASLt tuning tables; the engine start picks the one for the card
Each AMD engine package gets tuning/<arch>-hipblaslt-<version>.txt for the architectures it covers and the
hipBLASLt version it bundles, and records hipblaslt_version in BUILD.json (packaging/stage_tuning.py for the
Linux build scripts, tools/hip/package_windows.py for the Windows zip). package_engine.py lists the tables in
the manifest and refuses an AMD stage without a version or with a table for another version or architecture.

serve/server.py with_component_engine sets STRATA_HIPBLASLT_TUNING from the package (strata_control/
hipblaslt_tuning.py): a table only for the exact arch and version, a log line for a mismatch, an unreadable
card or a missing version, STRATA_HIPBLASLT_TABLE=on|off (or "hipblaslt_table" in the run config) as the one
switch, and a STRATA_HIPBLASLT_TUNING the person set is kept. setup.py looks in the engine folder's tuning/
first.

A table changes the rounding of the prompt's GEMMs (R9700: residual differs, first-token KL 0.016), so the
automatic choice is off (AUTO_USES_TABLE = False) until the owner decides; "on" uses the table. No gfx1201
table exists for hipBLASLt 1.2.0 and one calibrated for it was not worth shipping (+1% prompt, -2% decode).
2026-10-19 21:33:47 +02:00
Niko1221 c23a78694e Merge branch 'f-021-macvis' into f-021-int 2026-10-19 21:27:28 +02:00
Niko1221 509466a668 Mac pictures: signing smoke checks strata-vision, docs and the 7 READMEs say the Mac reads pictures
sign-and-notarize.sh starts strata-vision --version in the signed engine folders (it already signs every Mach-O).
MAC.md carries the M4 numbers (0.45 s on the GPU, 50.7 s on the CPU for a 260-token picture) and the rule.
2026-10-19 21:26:49 +02:00
Niko1221 a10777a671 Merge branch 'f-021-R' into f-021-int 2026-10-19 21:26:01 +02:00
Niko1221 bcf67d1774 Merge branch 'f-021-bench' into f-021-int 2026-10-19 21:23:46 +02:00
Niko1221 c8e5d2f3ce Docs: BENCHMARK_PROTOCOL (implemented, still draft: field mapping, Metal, versions, runner reading, SECURITY link), COMMUNITY_BENCHMARKS, TERMINAL; BENCH.bat, bench.sh and strata_bench.py point to strata bench run 2026-10-19 21:23:20 +02:00
Niko1221 6d41b37e76 strata bench run: the standard benchmark protocol (standard-v1 is the default suite); fixtures and COMMIT file in the Terminal archive; tests with a mock server 2026-10-19 21:23:20 +02:00
Niko1221 e1ef9df21c Standard benchmark: frozen fixtures (bench/prompts/standard-v1) and the tool that builds them 2026-10-19 21:23:19 +02:00
Niko1221 dab7ee72d0 arch_defaults: the integrated-GPU matrix-core prompt set is opt-in again (bit-changing, owner's call); the Radeon 780M keeps its expert arena unregistered by default on Linux (UMA mode); per-arch selection tests 2026-10-19 21:21:50 +02:00
Niko1221 6b28dbbd1e Merge branch 'f-021-dock' into f-021-int 2026-10-19 21:21:22 +02:00
Niko1221 e0e0c0545a Docker: DOCKER.md records the rocm-gfx120x card test of the 0.2.0-beta packages (Radeon AI PRO R9700) 2026-10-19 21:20:52 +02:00
Niko1221 8acee5a055 Docker: build.sh and publish.sh exit 0 after a good run; DOCKER.md records the 0.2.1 image tests
- The EXIT trap of build.sh (--ctx-only or --ctx given) and publish.sh (--work given) ended with a failing test, so dash reported exit 1 after a
  successful run. Both traps now use if/fi. Test: build.sh --ctx-only exits 0 and its context holds docs/TERMINAL_LAUNCH.md.
- docs/DOCKER.md: image sizes, the 2026-10-19 test of the images built from the 0.2.0-beta packages (cuda13 and cuda12 on an RTX A4000, ROCm images
  built and checked, wrong-card message).
2026-10-19 21:16:34 +02:00
Niko1221 dca9770ef4 Task board: Schedule | Board view on Tasks, derivation with tests, loop record, sidebar count, all 8 locales 2026-10-19 21:05:20 +02:00
Niko1221 ff18fbb868 Task board host side: dismiss mark, newest problem only, one plain line per run, pending approvals of every chat 2026-10-19 21:05:20 +02:00
Niko1221 cf4749276d Mac pictures: strata-vision with Metal built in, Mac setup and launch turn pictures on
tools/vision builds one program for the Mac: ggml's Metal backend inside, --gpu reads on the GPU, without it on the CPU. The reader
choice (strata_control/vision_reader.py) gives a Mac the GPU reader whatever its memory (the CPU reader needs ~50 s per picture:
ggml has no BF16 vector kernel on ARM). setup.py's Mac path downloads the mmproj and writes the vision section; strata launch,
the MCP server and metal/setup_mac.py default pictures on. packaging/build_vision_mac.sh builds the program for the engine package.
2026-10-19 20:59:29 +02:00
Niko1221 578bb6aa7a Docker: publish the images from 0.2.1 (workflow, signed-feed fetch, local script)
- .github/workflows/docker-images.yml: manual only (workflow_dispatch) with the release tag as input; a plan job lists the flavors the
  release has an engine package for; one job per flavor builds the image and, only with push on, pushes ghcr.io/niko1221/strata:<version>-<flavor>
  and latest-<flavor> with GITHUB_TOKEN (packages: write).
- docker/fetch-engines.py: downloads the engine package from the release and checks it against the signed update feed (feed signature, archive
  size and SHA-256, manifest hash, package signature, every file), the same code as `strata update`. Flavors cuda13, cuda12, rocm-gfx120x,
  rocm-gfx110x, rocm-gfx1151 (the halo package), sycl (only if the feed lists one).
- docker/publish.sh: the same steps by hand (fetch, build, start check, push only with --push).
- Fix: docker/build.sh did not copy docs/TERMINAL_LAUNCH.md, which package_terminal.py needs; no image could be built from a clean tree.
- Dockerfile: OCI source/url/documentation labels (ghcr links the package to the repository).
- Compose files and docs/DOCKER.md name the 0.2.1 tags on ghcr.io; DOCKER.md has a maintainers section. Tests for the scripts, the workflow and the fetch.
2026-10-19 20:57:22 +02:00
Niko1221 1073929c80 [Other] serve.test_parallel: split in two files, each about a minute
Not a hang: 42 tests against a fake engine that paces real time took 127 s in one module and passed. The yield/failure tests move to serve/test_parallel_yield.py; the shared fake-engine helpers are FakeEngineCase.
2026-10-19 20:54:34 +02:00
Niko1221 e5dc00c81e docs: integrated GPUs (mini PCs) - the matrix-core prompt set is on by default, numbers, off switches, Windows untested 2026-10-19 20:38:55 +02:00
Niko1221 1c3c6d3351 hip: STRATA_ARENA_PIN_GIB=none leaves the expert arena unregistered (UMA mode for an integrated GPU: no KFD userptr, resident through the working-set lock); opt-in, not default 2026-10-19 20:38:55 +02:00
Niko1221 ce968a2fac hip: integrated Radeons (gfx1103/gfx1150/gfx1151, UMA flag) take the matrix-core prompt set by default; STRATA_IGPU_DEFAULTS=0 and each switch =0 turn it off; unit test with mock devices 2026-10-19 20:38:55 +02:00
Niko1221 40d1a3e7d7 [TUI] strata connect --write: a test with no keyboard, and an ended input counts as no 2026-10-19 20:38:37 +02:00
Niko1221 a269d1b983 [Other] engine hash tests: mock the release probe, keep the engine folder in a temp dir
get_prebuilt has probed the release with a HEAD request since 0.2.0; the refusal tests reached GitHub, got a 404 and compiled instead of refusing. The code is right, the test was stale.
2026-10-19 20:38:04 +02:00
Niko1221 9d2cb4f6d2 [Other] --data-dir never moves the model library without a choice
A --data-dir that names a new folder used to move Models, packs and mtp into it and rewrite the saved data_dir. Now the terminal asks (keep or move), --yes and a missing keyboard keep the old library in place (still found as a root), --move-data moves, and the saved default changes only on a move, --set-default-data-dir, or a PC with no models. Tests use temp folders.
2026-10-19 20:37:29 +02:00
Niko1221 74e25e165b Merge branch 'f-021-hostA' into f-021-int
# Conflicts:
#	apps/host/src/locales/de.json
#	apps/host/src/locales/en.json
#	apps/host/src/locales/es.json
#	apps/host/src/locales/ja.json
#	apps/host/src/locales/ko.json
#	apps/host/src/locales/pt.json
#	apps/host/src/locales/ru.json
#	apps/host/src/locales/zh.json
2026-10-19 20:30:04 +02:00
Niko1221 f5690d51a8 web-ask test: one data folder per bridge so the run exits; the scheduled-task case checks the refusal and the report 2026-10-19 20:25:20 +02:00
Niko1221 e2071c2f8b Downloads: 16 connections, several pieces per request, ModelScope length probe, faster MTP fetch
A single connection to Hugging Face's CDN stalls to 1-3 MB/s for 10-30 s again and again on the
owner's route; eight connections hide it, but a request per 32 MiB piece paid TCP slow start every
time. A request now covers up to four neighbouring pieces (the piece list and resumes are unchanged),
a try that moved bytes forward does not count as a failed one, and the default is 16 connections.
A server whose HEAD gives no length (ModelScope) is asked for bytes 0-0 so it gets the connections
too. mtp_fetch reuses the CDN address, reads the 28 shard headers side by side and lets the
connections run ahead of a slow range.
2026-10-19 20:07:33 +02:00
Niko1221 f2345ebd66 The model page and first-run page show the engine's own pick for this PC (memory tiers, Gyro-M as the other choice); the fit counts the draft layer (F) 2026-10-19 19:56:02 +02:00
Niko1221 671f4573da Fit checks and slot sizing count the draft layer's ~1 GB of graphics memory while MTP is on 2026-10-19 19:56:01 +02:00
Niko1221 3d0a79ca23 SAFETY.md: update checks only on a button press, web search asks once per chat in Manual, starter list refused 2026-10-19 19:54:54 +02:00
Niko1221 f523a94227 Chat footer speed: tool-call writing time counts as decoding time, so tok/s is output tokens over decode time 2026-10-19 19:54:48 +02:00
Niko1221 267f35f777 Chat with no model: plain words from the host, nothing sent before the engine's first state is known; tests for every state 2026-10-19 19:54:48 +02:00
Niko1221 71bdfda0b3 Offline web search: a pretend-offline mode for the search plugin and a test that the model gets the plain sentence 2026-10-19 19:54:48 +02:00
Niko1221 7662bf85c4 Starter model list: with the real engine on, the host refuses to download from it (unpinned files); it is display only 2026-10-19 19:54:47 +02:00
Niko1221 44a56865f5 Manual mode asks once per chat before the first web search or page; Settings, Tools switch to skip the question (off by default) 2026-10-19 19:54:37 +02:00
Niko1221 7f037f4451 Terminal: 'already downloaded' for a model on disk; strata monitor finds a running launch without --url (K) 2026-10-19 19:48:18 +02:00
Niko1221 5c9884cdee A build for another make of graphics card says so in plain words with the right download and is not unpacked (I) 2026-10-19 19:40:57 +02:00
Niko1221 5b4db785ca Start-up line: an installed engine with no model loaded is not 'No Strata engine' (J), and the wrong-make notice wiring 2026-10-19 19:40:57 +02:00
Niko1221 321f159032 Offline: the update check says there is no internet connection in plain words; offline start and load test 2026-10-19 19:40:05 +02:00
Niko1221 54a02dc32c Settings: opening Engine and Update makes no network call; the last check is shown and a new one runs only on a button press 2026-10-19 19:40:05 +02:00
Niko1221 40bf5f5c40 Merge branch 'f-021-settings' into f-021-int 2026-10-19 19:34:47 +02:00
Niko1221 bc54d945f6 Merge branch 'f-021-crash' into f-021-int 2026-10-19 19:34:46 +02:00
Niko1221 7a2bfc52ae Models: a per-model draft layer (MTP) switch, picture-off and custom context that stick
The installed app listed every model's pictures as "off", so each Load of a picture-capable model ran
setup again, which writes the arguments (--mtp, --spec-min-p, --vision, --max-context) and the vision
section from its own choices. Hand edits and the Read pictures tick came back each time.

- host: images comes from the run config's vision section, so a prepared model loads straight away
- host: Read pictures off no longer downloads/keeps the reader; --vision is left out of the engine arguments
- host: Draft layer (MTP) setting; "MTP per request" and it are the app's own and no longer frozen
- host: a context size set by hand shows in the Context list
- "mtp": false in the run config (strata_control/draft_layer.py) is applied at every start by
  serve/server.py, kept by setup, written by `setup --mtp off` and `strata launch --mtp off`
- strata_install accepts any context size, not only the listed ones
- docs: draft layer off, and what the app owns in the run config
2026-10-19 19:29:39 +02:00
Niko1221 e88ee39d1d Docs: state exactly which refusal checks ran (Linux packages) and the Windows PATH note for --version 2026-10-19 19:18:52 +02:00
Niko1221 a5ee74ffa3 Desktop: a failed start on Windows and Mac leaves a log, a crash file and a message box instead of 0xc0000409
Reported crash (0.2.0-beta, Windows 11 26300): strata-desktop.exe exits with 0xc0000409 at 0x31a315 and no log. That offset is the
fast-fail in Rust's abort after a panic (panic = "abort"); a Tauri setup error is a panic ("Failed to setup app"), and the shell had no
log on Windows, no console (windows_subsystem), so the reason was lost.

- the run log opens first in main (%LOCALAPPDATA%\Strata\logs, the folder Save logs already reads) and a panic hook writes the
  message and place to it and to desktop-crash-<time>.log, and shows a plain message box before the abort
- setup no longer returns an error to Tauri: a host that does not start or a window that cannot be made is reported the same way
  and ends the app with exit code 1; the build error is no longer an expect
- the host's output goes to the run log (it was lost without a console)
- plain next step in the box for a WebView2 problem and for a blocked host
- STRATA_NO_DIALOG (scripted runs) and STRATA_DESKTOP_TEST_PANIC (tests) for proving the net
2026-10-19 19:17:43 +02:00
Niko1221 74652692e1 Release matrix tests: Windows rdna3 (RX 7000, Strix Halo) and Linux halo builds, no arch in two hip builds of a platform 2026-10-19 18:32:56 +02:00
Niko1221 85f77658f3 Docs: RX 7000 and Strix Halo installers (Windows rdna3, Linux strixhalo) in the READMEs, AMD_HIP, STRIX_HALO and the 0.2.0 release notes 2026-10-19 18:27:13 +02:00
Niko1221 f57c0adcb3 Merge branch 'f-020-reset' into f-020-int 2026-10-19 17:12:10 +02:00
Niko1221 3fa1b524d7 Portable monitor bundle: carries consoleinfo.py and overview.py (manifest schema 6), the plain Monitor runs when launch is not bundled, smokes accept the Overview's /metrics read, test derives the bundle's import closure 2026-10-19 16:35:51 +02:00
Niko1221 76e51103c7 Merge branch 'f-020-ci' into f-020-int 2026-10-19 16:26:40 +02:00
Niko1221 fcca2ccb84 CI smokes: serve smoke expects the server-package preflight, server smoke accepts OK (skipped=N), nested-telemetry fixture deep enough for Python 3.12+ 2026-10-19 16:23:26 +02:00
Niko1221 eac694ae4c Issue and PR reset bot: intake check, /reopen, 5-day sweep, with node tests 2026-10-19 16:10:26 +02:00
Niko1221 4caf857c76 README translations: the new app and Terminal screenshots with translated captions 2026-10-19 16:03:04 +02:00
Niko1221 e5d987a553 Merge branch 'f-020-shots' into f-020-int 2026-10-19 16:02:32 +02:00
Niko1221 dde39e8a8f README app screenshot: crop the chat footer (its speed figure is wrong, see the 0.2.1 plan) 2026-10-19 16:02:32 +02:00
Niko1221 03b75b8da9 README: new app and Terminal screenshots for 0.2.0-beta 2026-10-19 15:52:41 +02:00
Niko1221 3d1fb48090 Release notes 0.2.0: which file to download, memory tiers, faster checked downloads, the chat fixes, signed Mac 2026-10-19 15:29:09 +02:00
Niko1221 042cd201c9 Merge branch 'f-020-tr2' into f-020-int 2026-10-19 15:04:28 +02:00
Niko1221 b961c7aa8c README: use the app's own UI strings in the ja, pt-BR and zh-CN translations 2026-10-19 15:04:16 +02:00
Niko1221 6ec549d2dd README de/fr/es: use the app's own labels, French keeps English UI names; tidy colon asides 2026-10-19 15:03:27 +02:00
Niko1221 4ff5972fcb README: new Japanese, Brazilian Portuguese and Simplified Chinese translations of the English README 2026-10-19 15:02:13 +02:00
Niko1221 04a2e6a3f1 README: new German, French and Spanish translations of the 0.2.0 README 2026-10-19 15:01:57 +02:00
Niko1221 c301209636 README: the Windows AMD app is for RX 9000 / R9700 cards; other AMD cards point to AMD_HIP.md 2026-10-19 14:57:39 +02:00
Niko1221 78bba245a5 README: file-per-card download table, what-you-need before install, shorter blocks; drop the Mac damaged workaround 2026-10-19 14:57:15 +02:00
Niko1221 18f7b90ca6 Feed: the app entry may carry a pre-release version (0.2.0-beta); the strict key rejected the whole release feed; UPDATES: the release steps for the engine-less update installers, --url-prefix=, Control version 2026-10-19 14:01:29 +02:00
Niko1221 1b8e88b9e5 feed_tool: a Linux or Mac package packed on Windows keeps its programs executable (mode from the file header); test 2026-10-19 12:19:17 +02:00
Niko1221 ef59c0b555 Merge f-020-int (final 0.2.0-beta source 4ea817163) into f-020-rel 2026-10-19 11:53:08 +02:00
Niko1221 4ea8171636 Merge branch 'f-020-dl' into f-020-int 2026-10-19 11:51:12 +02:00
Niko1221 3c3d2881b4 Merge f-020-int into f-020-rel 2026-10-19 11:35:31 +02:00
Niko1221 604e23cd9a Engine-less app update: tauri.update.conf.json drops engine-bundle so one latest.json entry per OS fits every installer variant; tests for the overlay, for the engine-less update on an installed AMD engine, and for the release engine selection (CUDA 13/12 by driver and card, HIP by gfx, Metal) 2026-10-19 11:35:23 +02:00
Niko1221 41cc4b2e89 Plans: no step counter in the progress lines (owner: 'Step 2 of 5' removed like the goal's) 2026-10-19 11:35:11 +02:00
Niko1221 3d9c21509e Merge f-020-int into f-020-rel 2026-10-19 11:32:19 +02:00
Niko1221 779bc30501 Merge branch 'f-020-gui' into f-020-int 2026-10-19 11:29:58 +02:00
Niko1221 d5bcc3bd8d GUI fixes: wait for the assistant to start, no step counter on goals, files open without a chosen folder, results open in the sidebar 2026-10-19 11:29:40 +02:00
Niko1221 116adbf7d6 tester README (Linux NVIDIA): the driver line names the CUDA 13 engine's minimum, 580.65 2026-10-19 10:53:25 +02:00
Niko1221 19d5cd87fa install.ps1: name a discrete graphics card before the built-in one in the line it prints 2026-10-19 10:53:24 +02:00
Niko1221 2ca90e48ec binary_deps: a Windows DLL found only in a sub-folder counts as bundled only when BUILD.json lists that folder in lib_dirs (the RX 9000 package's rocblas.dll under rocm/bin made a plain start die with 0xC0000135) 2026-10-19 10:53:24 +02:00
Niko1221 adfb7ecb2a packaging: the Control python launcher uses no external command (dirname failed inside the AppImage on newer systems); the engine start error names a missing Windows DLL (0xC0000135) and says how to fix it 2026-10-19 10:52:51 +02:00
Niko1221 a122ba0bdf Mac signing: lessons of the first real run (entitlements plist LF everywhere, TMPDIR's trailing slash no longer makes a // path, the Terminal python check finds the python3 symlink) 2026-10-19 10:50:55 +02:00
Niko1221 04221c3e2b Download big model files over several connections
download() reads a file of 256 MiB or more in 32 MiB ranged pieces over 8 connections
(STRATA_DL_CONNECTIONS=1 restores one stream), writes each piece at its own offset,
and keeps the finished pieces in <name>.part.chunks so a stopped download continues
without fetching them again. The redirect to the signed CDN address is resolved once
and again before it expires or after a 401/403/410. A server without Range support
or without a length is read in one stream as before; an older single-stream .part
continues after its last whole piece. The progress line and the .part size the app
reads are unchanged, the SHA-256 pin is still checked on the whole file, and the
.done mark is the same. tools/mtp_fetch.py reads its ranges in parallel too.
2026-10-19 10:38:35 +02:00
Niko1221 fdb5e48616 Merge branch 'f-020-macrel' into f-020-int 2026-10-19 09:33:22 +02:00
Niko1221 af78949b17 build-feed writes install-manifest.txt from the Control packs (what install.sh and install.ps1 read); test of the release path; docs 2026-10-19 09:28:23 +02:00
Niko1221 935407d883 Mac in-app update: sign-and-notarize.sh --updater-archive packs the notarised app; feed_tool set-app-platform verifies the Tauri signature and writes the update entry
tauri build makes Strata.app.tar.gz and its .sig before sign-and-notarize.sh re-signs and notarises the app, so the published Mac update would be the unsigned app. The script now writes Strata.app.tar.gz from the stapled app on request; the Tauri key stays on the PC (npx tauri signer sign), and set-app-platform checks the .sig against tauri.conf.json's pubkey over the exact bytes and records url, signature, sha256 and size.
2026-10-19 09:18:13 +02:00
Niko1221 899d1b1b3a metal: restore the stubs for gemm_iq_f16_grouped, native_q8_0_sm60_pack/_release and swiglu_split_f16 (CUDA-only paths the Metal engine links but never runs)
The Metal engine did not link from f-020-int: prefill.cpp and native_head.cpp reference these four entry points. All answer 'nothing done'.
2026-10-19 09:17:56 +02:00
Niko1221 02a8193da7 test_strata_mcp: the Windows AMD recommend test no longer reads this PC's installed engine (hermetic; fails on a PC with a CUDA engine since e107297a8) 2026-10-19 09:07:01 +02:00
Niko1221 c7bfd131b5 Merge branch 'f-020-int' into f-020-golden 2026-10-19 08:51:07 +02:00
Niko1221 6f2a3e758d Merge branch 'f-020-sha' into f-020-int 2026-10-19 08:41:46 +02:00
Niko1221 e193161de2 Pin SHA-256 and size for every model file setup downloads; stop when the pinned version is gone
Qwen3.8-Flash-Next (ISTA), Swift 1.5, the Coder and the picture reader get fixed
hashes (REPO_PINS, checked in download()). A 404 on the pinned commit stops instead
of taking the current file; STRATA_ALLOW_UNPINNED=1 is the announced tester opt-out
(setup.py and tools/mtp_fetch.py). Swift moves to the commit that has its IQ3_S.
Tests for pins, mismatch, 404, opt-out and offline use; SAFETY.md updated.
2026-10-19 08:41:24 +02:00
Niko1221 06b6b23e51 Merge branch 'f-020-int' into f-020-golden 2026-10-19 08:38:10 +02:00
Niko1221 553c25ca7c Merge branch 'f-020-tiers' into f-020-int 2026-10-19 08:36:15 +02:00
Niko1221 8ac47258bb tests: wizard tests pick IQ3_XXS by hand now that IQ3_S is pre-selected at 64 GB 2026-10-19 08:35:13 +02:00
Niko1221 dbd1cb9af8 setup golden: re-record the run configs after pictures-on-by-default (91baead75) and the memory tiers (aa82a8863); classification in phaseA-tests/golden-020.md 2026-10-19 08:34:26 +02:00
Niko1221 94f095c893 tests: launch TUI pins follow the 64 GB tier (IQ3_S) 2026-10-19 08:33:19 +02:00
Niko1221 a4c1644a37 Merge branch 'f-020-tiers' into f-020-int 2026-10-19 08:30:20 +02:00
Niko1221 aa82a88639 Recommend by the owner's memory tiers (RAM + VRAM): 40 GB Swift 1.5 IQ2_XS with Gyro-M named, 48 GB IQ3_XXS, 64 GB IQ3_S; MODELS.md Pick by memory 2026-10-19 08:26:54 +02:00
Niko1221 9962aae5b7 Merge branch 'f-020-safety' into f-020-int 2026-10-19 08:16:03 +02:00
Niko1221 cbbdd16b43 SAFETY/README: no 'nothing leaves your PC' overclaim (web search is on by default); neutral wording for Mac signing 2026-10-19 08:16:03 +02:00
Niko1221 25492e4a6b Docs: add SAFETY.md (what Strata downloads, when it uses the internet, what stays local, how downloads are checked, what the agent may do) and link it 2026-10-19 08:15:11 +02:00
Niko1221 7e3a9fec71 tests: DOCKER.md no longer carries TODO(owner); check the not-published line instead 2026-10-19 08:13:25 +02:00
Niko1221 2a6650a364 README: memory tiers per the owner (40 GB Swift 1.5 or Gyro-M with its speed note, 64 GB IQ3_S original or Swift), low-RAM line, Gyro-S vs IQ2_XS 2026-10-19 08:11:47 +02:00
Niko1221 5e7b9a8301 Docs: plain-wording pass on the 0.2.0 text (fewer mid-sentence bolds, direct phrasing) 2026-10-19 08:01:47 +02:00
Niko1221 2b209b406d Docs consistency pass: pictures on by default on NVIDIA and AMD (Intel Arc off for now), seven monitor views, area prefix for issue and PR titles in AGENTS.md 2026-10-19 08:00:26 +02:00
Niko1221 6ff75059a9 Merge f-020-docs3 into f-020-int (0.2.0 release wiring and docs)
# Conflicts:
#	packaging/UPDATES.md
2026-10-19 07:59:35 +02:00
Niko1221 e46be1fe0a Merge f-020-docs2 into f-020-int (0.2.0 release wiring and docs) 2026-10-19 07:59:18 +02:00
Niko1221 f86ca9d1c5 Merge f-020-docs into f-020-int (0.2.0 release wiring and docs) 2026-10-19 07:59:17 +02:00
Niko1221 477fd36bce Merge f-020-release into f-020-int (0.2.0 release wiring and docs) 2026-10-19 07:59:17 +02:00
Niko1221 193cbb1fbf README redesign: logo header, clearer install per system, memory rule, Gyro for low memory, library analogy, support section
Header uses the existing brand lockup. Performance tables get bars and plain labels, with the engine versions left to DETAILS. What you need is built around 32 GB of RAM plus VRAM and links the platform docs. Troubleshooting moves below How does it work.
2026-10-19 07:55:50 +02:00
Niko1221 76e4fef398 Intel Arc pictures stay opt-in until the SYCL engine's first-picture GPU fault is fixed; UPDATES: publish as a normal release 2026-10-19 07:48:06 +02:00
Niko1221 dac635445b CONTRIBUTING.md: a Titles section (area prefix for issues and pull requests) 2026-10-19 07:42:35 +02:00
Niko1221 046bdc83d9 Titles start with their area: Area dropdown and title prefix in the issue forms, a benchmark form, a PR template line, a title-area workflow 2026-10-19 07:42:27 +02:00
Niko1221 667d3cf7d7 Intel Arc: pictures on by default through the portable CPU reader (setup_intel, shared /dev/shm for the embeddings) 2026-10-19 07:28:38 +02:00
Niko1221 4794b13779 strata launch --help states the picture rule; strata monitor is the seven-view monitor (dead six-view fallback removed), tests 2026-10-19 07:28:38 +02:00
Niko1221 09b8b54d19 Release app config: the engine travels inside every installer, signed updater artifacts overlay, macOS 15 minimum 2026-10-19 07:28:37 +02:00
Niko1221 98b865c56f Release wiring: release feed key and Tauri updater key, GitHub Releases feed address in the installers, versions 0.1.45.3 and 0.2.0-beta 2026-10-19 07:23:44 +02:00
Niko1221 91baead754 Pictures on by default on every make of card: MCP install, Qwen3.6, Unsloth and the original Flash-Next families follow the one reader rule (CPU under 24 GB, GPU from 24 GB); the CPU reader keeps no VRAM reserve 2026-10-19 07:23:44 +02:00
Niko1221 80a10f31c3 docs: details, batching and tester readmes for 0.2.0
DETAILS: pictures on by default with the 24 GB reader rule, the engine label of the prompt-chunk planner (before 0.1.45.3, not 0.1.46), the per-architecture sets measured on 3060, A4000 and P100, the N7 and ROUTE_PRIOR decisions, a pointer to the app and Terminal troubleshooting. Links to private phaseA-tests notes removed from DETAILS and BATCHING. Tester readmes: AMD pictures are on (CPU under 24 GB, GPU on a 24 GB 7900 XTX), the RX 7900 text reconciled with the community runs, Mac needs macOS 15 and has no picture reader yet.
2026-10-19 07:16:00 +02:00
Niko1221 b27c46cbd6 docs: Mac, AMD, Intel, older GPUs and multi-GPU for 0.2.0
MAC: the .dmg and the Terminal bring the Metal engine; pictures only with the source build. AMD_HIP: the 24 GB
picture rule and the measured R9700 GPU reader. INTEL: the grouped XMX label is engine 0.1.45.3 (not 0.1.48), plus
the A750 prompt-hang fix (fca3c5958; STRATA_PF_DMA_PAGEABLE=1 restores the old copy). INTEL_ARC: maintainers now test
on a B70 and an A750; where the Intel engine comes from. OLDER_GPUS: the P100 sm_60 defaults (STRATA_SM60_PERF).
MULTI_GPU: two models on two cards, one make per engine. MODELS and TROUBLESHOOTING: the Mac and Intel Arc have no
picture reader yet.
2026-10-19 07:15:14 +02:00
Niko1221 1c4970b9ac 0.2.0 release notes: the split, installers, Terminal, pictures, new defaults with measured numbers, tester fixes, known limits 2026-10-19 07:09:40 +02:00
Niko1221 26613a44f6 AGENTS, AI_SETUP, INSTALL: the app and the Terminal first, install from source as the fallback
Agents install the Terminal with the one line and run strata launch --model NAME --yes --no-tui (flags checked against
strata launch --help). Pictures follow the 24 GB rule on every make; the Mac has no picture reader yet. INSTALL gains
The app and The Terminal sections, update paths for both, and a Mac source section. AGENTS points to CONTRIBUTING.md.
2026-10-19 07:08:46 +02:00
Niko1221 afe92c1727 docs: models, NVFP4, Gyro, Qwen3.6 and troubleshooting for 0.2.0
Pictures are on by default (CPU reader under 24 GB, GPU reader from 24 GB, every make; no Mac reader yet).
Gyro and NVFP4-QAD read pictures; Gyro needs engine 0.1.45.1+ and is correct on RTX 50 since the SwiGLU guard.
The three NVFP4 versions get their app names. A Qwen3.6-35B-A3B section, the app and Terminal ways to add a model,
and engine versions on the older speed tables. Troubleshooting: no-engine warning, needs a newer engine / Update
engine, Mac xattr steps, strata doctor, Save logs. Private test-notes references removed.
2026-10-19 07:07:54 +02:00
Niko1221 ad262c80aa README: the app installers and the one-line Terminal install first; build from source moves lower
Pictures on by default (CPU reader under 24 GB, GPU reader from 24 GB, every make; none on the Mac yet), the Mac app,
the Terminal in a short section, the Anthropic base URL without /v1, two models on two cards, one make per engine.
The speed tables stay and say which engine measured them. A Contributing line links CONTRIBUTING.md.
2026-10-19 07:05:43 +02:00
Niko1221 14ec155ec7 0.2.0 text, group 3: Terminal docs (installed command, seven views, release address, connect --check json), Docker, packaging, SECURITY, issue and PR templates, test requests, historical Terminal pages 2026-10-19 07:04:59 +02:00
Niko1221 e9e08adb37 Add CONTRIBUTING.md and the draft standard benchmark protocol (owner text, unchanged) 2026-10-19 06:58:45 +02:00
Niko1221 fd0e3766a3 Merge f-split-tuiqa (95f380769) into f-split-int: TUI QA fixes (launch with flags, discovery, monitor menu, connect checks, layouts, no browser, warnings), picture reader CPU under 24 GB / GPU from 24 GB 2026-10-19 00:03:56 +02:00
Niko1221 95f3807699 Merge f-split-int d6aa6504e into f-split-tuiqa: launch warns and asks (setup stops only for NEED_ENGINE unless launch said go on), the app warns; one picture-reader rule in strata_control/vision_reader.py (CPU reader under 24 GB, GPU reader from 24 GB, every make; pictures stay on) for setup, launch and the app catalog 2026-10-19 00:03:31 +02:00
Niko1221 a2367ca55b Mac: one script to sign (Developer ID, hardened runtime) and notarise the app dmg and the Terminal archive; Mac README text for notarised and not notarised builds 2026-10-19 00:01:30 +02:00
Niko1221 e657d310e7 models import: a folder with several models says which ones it holds and asks for one file, in plain words 2026-10-18 23:33:52 +02:00
Niko1221 64af23dafc Terminal shows the app's version (0.2.0-beta) with the build number beside it; the archive README no longer contradicts itself, ships the launch doc it links and gives free-memory guidance for the system it is for; launch doc updated 2026-10-18 23:28:01 +02:00
Niko1221 bccb0f4280 Terminal wizard: the Save choices stay on screen at 60x20 and 40x15, the Advanced command has its own scroll area, key hints wrap and say what Enter does; request filter explains itself and matches LIVE/DONE; plain-word setting descriptions; tuning default explained; monitor bundle carries nav.py (T09, T11) 2026-10-18 23:23:09 +02:00
Niko1221 adc4a6822b Advanced command preview follows the wizard's context, memory format and picture choice (T09 part 1) 2026-10-18 23:00:01 +02:00
Niko1221 3d2e7f166e strata connect --check --output json runs the same checks and returns each result: ok is false and the exit code 1 when a check failed or the server is unreachable; the app list says Vendor tested (T07) 2026-10-18 22:58:57 +02:00
Niko1221 545fccf007 Terminal monitor: one seven-view model (arrows and 1-7 reach Connect, wrap, hints say 1-7), the selected tab always marked and the names kept at narrow widths, Overview titled Overview, Connect separates vendor-tested history from this server's live result, light theme for Connect and pop-ups (T06, T08, T10) 2026-10-18 22:57:36 +02:00
Niko1221 d6aa6504e0 Merge f-amd-vision (d5fda4d2f) into f-split-int: GPU picture reader for AMD (strata-vision-hip) with automatic CPU fallback; pictures on by default on AMD; packaging for Linux and Windows 2026-10-18 22:53:23 +02:00
Niko1221 d5fda4d2f3 build_windows.bat: STRATA_VISION_HIP_ARCHS 2026-10-18 22:50:16 +02:00
Niko1221 8667a3dbf7 Merge f-split-int (a3024ed10) into f-split-qa1: qa2's s3237-s3238 kept, my 21 strings renumbered s3260-s3280 2026-10-18 22:46:39 +02:00
Niko1221 b7b65edeb1 build_hip_linux.sh: VISION_HIP_DEFS 2026-10-18 22:46:29 +02:00
Niko1221 4440a303f8 Terminal: no browser opens by itself (launch config forbids it, --open is the only way), and the start message promises a browser only with --open 2026-10-18 22:46:10 +02:00
Niko1221 e107297a8f Mixed makes of graphics cards: only the cards the engine can drive are offered, never a cross-make split, one plain line when the other make has the bigger card, two models at once said to be unsupported (launch, setup, app card list) 2026-10-18 22:44:12 +02:00
Niko1221 6b8cfcd1a3 strata-vision-hip (Windows): hipblas.dll is delay-loaded and rocm\bin is put on the DLL search path first, so the reader starts from any PATH 2026-10-18 22:41:18 +02:00
Niko1221 d060783fbb Terminal: pictures are on by default when the model and the engine can read them (card for CUDA and HIP, CPU otherwise); --vision off turns them off 2026-10-18 22:39:16 +02:00
Niko1221 a3024ed10a Merge f-split-qa2c (10f3435b0) into f-split-int: Linux drag-drop through the page widget's GTK drag signals; export header test 2026-10-18 22:39:06 +02:00
Niko1221 c4345a7c03 Terminal: no engine for this computer is a warning, not a refusal: ask (default yes, --yes skips), download the model, end with the engine problem and the next step, exit 0 2026-10-18 22:33:43 +02:00
Niko1221 10f3435b00 Test: the saved Monitor picture with a model loaded is headed by the logo and the model and time line (NativeMonitor is not used by any screen) 2026-10-18 22:30:01 +02:00
Niko1221 35698ffe70 Linux: dropping a file on the window works under WebKitGTK 2.52: the shell reads the file:// addresses from the GTK drag signals, accepts the drag itself, takes the leave with every button up as the drop and hands the paths to the host 2026-10-18 22:28:58 +02:00
Niko1221 e6ab4c024d No recognised engine: the app still downloads the model and warns in its card; loading names the next step
setup.py control_engine_step stops only for strata launch (STRATA_NEED_ENGINE=1); the app warns and continues. The Models card and the first-run
download card show the owner's plain warning before and during the download; the load refusal says the same with Settings > Engine.
2026-10-18 22:28:15 +02:00
Niko1221 23b08c7620 Terminal: one discovery of existing installs and model files for the screens, models list, own-files import and launch; Gyro-S/M GGUFs are known; installed models show what is really missing (T03, T04, T05) 2026-10-18 22:22:31 +02:00
Niko1221 e23a56f2f8 ENGINE-COMPONENT.md: the picture readers in an AMD package 2026-10-18 22:19:49 +02:00
Niko1221 b0b8860d13 build_windows.bat: the HIP reader's host compiler gets the ROCm device-library path 2026-10-18 22:19:18 +02:00
Niko1221 1486151d4d Terminal: strata launch with flags opens its run screen instead of crashing, and a crash of the screens exits non-zero with a plain sentence (T02) 2026-10-18 22:11:21 +02:00
Niko1221 97cb031bf7 docs: AMD pictures run on the card by default, with the CPU reader as the automatic fallback 2026-10-18 22:04:25 +02:00
Niko1221 c59505e638 build_windows.bat: STRATA_ROCM_KEEP uses the ROCm venv as it is 2026-10-18 22:02:17 +02:00
Niko1221 34bd640a19 Merge f-split-qa2b (b3348da49) into f-split-int: the saved Monitor screenshot shows the logo alone in its top line 2026-10-18 22:01:44 +02:00
Niko1221 0f5511cfdf Merge f-split-qa2 follow-ups into f-split-int: drop guard (no navigation to a dropped file), shell-to-host drop hand-over 2026-10-18 22:01:44 +02:00
Niko1221 b3348da493 Monitor: the saved screenshot starts with the Strata logo alone, without the Monitor headline 2026-10-18 22:01:21 +02:00
Niko1221 b31c13875e AMD picture reader: the no-GPU/arch refusal only in the HIP program; the app's CPU choice starts the CPU reader program 2026-10-18 21:55:51 +02:00
Niko1221 b2e3008bfb build_hip_linux.sh: kernel data collected once 2026-10-18 21:51:33 +02:00
Niko1221 55ca7b955a launch: the Pictures screen says the AMD picture reader runs on the graphics card; build_hip_linux.sh collects libraries from the build outputs 2026-10-18 21:51:10 +02:00
Niko1221 979da9d2bc Terminal launch list: Qwen3.6 (own engine path) stays out of Flash-Next's rows 2026-10-18 21:44:49 +02:00
Niko1221 536c7f019e Windows HIP zip: build_windows.bat builds the two picture readers and package_windows.py ships them 2026-10-18 21:44:45 +02:00
Niko1221 de9101935d setup: AMD pictures on by default (the HIP reader with the CPU reader behind it, else the CPU reader), on Windows too; source build compiles the HIP reader; package_engine carries strata-vision-hip 2026-10-18 21:43:23 +02:00
Niko1221 f82fffbe25 picture reader choice for AMD engine folders and the server's fallback from the GPU reader to the CPU reader (one log line) 2026-10-18 21:39:13 +02:00
Niko1221 b808d6e442 Linux and macOS: dropping a file on the window works. WebKit gives the page the drag but not the files, so the shell keeps its drop handler, hands the paths to the host, and the page fetches each file once; a file dropped outside the chat no longer takes the window to it 2026-10-18 21:36:51 +02:00
Niko1221 75127c794e strata-vision: fix the escaped newlines 2026-10-18 21:34:28 +02:00
Niko1221 96658aebae packaging: build_hip_linux.sh builds the HIP engine package with both picture readers 2026-10-18 21:32:58 +02:00
Niko1221 fb14c354b4 strata-vision: HIP build (strata-vision-hip) for AMD GPUs; the reader refuses a GPU that is not in its build; RUNPATH $ORIGIN 2026-10-18 21:31:30 +02:00
Niko1221 43ea31544b native poll: a read that never answers is dropped after 60 s 2026-10-18 21:31:21 +02:00
Niko1221 a1788c54be Read pictures is on unless the person turned it off: an installed model with no saved choice starts checked 2026-10-18 21:30:17 +02:00
Niko1221 7f7bfa962b Merge f-split-qa3 (283258664) into f-split-int: web search back-off and Bing fallback, turn guard with a final message, /loop stops by count/time/condition, Anthropic base URL without /v1 2026-10-18 21:25:06 +02:00
Niko1221 2832586641 Chat stream: translate the app's own delta lines before the delta branch, so the answer-text code stays as merged 2026-10-18 21:17:43 +02:00
Niko1221 a04244a4be Merge f-gyro-swiglu (8837cb6f0) into f-split-int: the fused gate/up SwiGLU path takes only 80-dot rows; Gyro's trellis types fall back to the full-warp kernel (correct Gyro output on sm_120) 2026-10-18 21:13:13 +02:00
Niko1221 714ca744dc Merge f-split-int (c157ea373) into f-split-qa3 2026-10-18 21:10:12 +02:00
Niko1221 e5476b0e12 Merge f-split-int (c157ea373) into f-split-qa1: my new strings renumbered after s3236 (s3237-s3255) 2026-10-18 21:09:33 +02:00
Niko1221 fef5dfec03 Storage list full width; version list title keeps its spaces 2026-10-18 21:08:41 +02:00
Niko1221 c157ea373f Merge f-split-qa2 into f-split-int: drag-drop in the native window, files-edited count only from tool changes in the workspace, answer-text guard, dialog focus, skill enabled flag, number field checks, folder picker release, small-window composer, Monitor screenshot feedback 2026-10-18 21:07:47 +02:00
Niko1221 40b41da916 Turn guard: a search-heavy /plan that keeps failing is stopped by the app, and a full context never ends in silence
A /plan on a small model searched again after every refused search and rewrote its plan after each one until the context reached 100%,
then the turn ended with no answer and no message. The bridge now counts what the turn shows (turn-guard.mjs): 3 failed searches in a
row, 10 searches in a plan turn (25 otherwise), a plan written from 1. again 4 times, a local model's context at 93%. The turn is stopped the
way Stop stops it but ends as a normal turn: the plan it had is still proposed (the plan card says why it stopped early), any other turn
ends with one plain translated sentence with a next step (frames carry the English sentence as `key`, the page translates it, 8 languages),
and the Harness's own /compact is asked for so the next message has room. A turn that ends with no words at all always gets a message:
"I ran out of room in this chat..." at 85% of the window or more, otherwise "I stopped without writing an answer...". Settings -> Tools says
results may come from Bing when DuckDuckGo is busy. Tests: counting (pure) and three end-to-end runs through the real Harness with the scripted
model (searchloop / bloat) and the real search code on a pretend web that refuses (bridge option searchMock), no network.
2026-10-18 21:07:38 +02:00
Niko1221 31030b170a storage and version: one Downloaded models list from the real files, Check folder unloads nothing, app version shown as Strata
- tools/strata_mcp.py: disk usage from the files a model really uses (every part of a multi-part GGUF, the picture reader, packs, draft layer),
  a file named by several flags or shared by several models counted once; Delete's plan stays separate (removable_gb)
- Settings > Storage: one list and one total (optimized models and other local models), shared files as a line of their own
- Check folder on the same folder no longer unloads the loaded models (O01); an actual change still asks first
- Settings shows the app version (0.2.0-beta) as Strata with the engine beside it; Control's 0.1.45 is no longer shown as Strata
  and no longer goes into the update check as the app version
2026-10-18 20:55:06 +02:00
Niko1221 b23dad1d5f scripts/qa2-flows.mjs: browser checks for the QA fixes (dialog focus, number fields, small window, picture drop, screenshot) 2026-10-18 20:54:29 +02:00
Niko1221 8837cb6f0f gu_swiglu: the 80-dots-a-row guard for every format (Gyro's trellis gate/up rows went through the 16-lane kernel)
launch_gu_swiglu (STRATA_GU_SWIGLU, on by default on compute capability 12.0 cards since 147-verify) runs
16-lane gate/up rows that assume 80 dots a row, but its guard rejected only the K-quants. The APR trellis
types (TQ2_T / TQK6 gate/up: qk 128, ipb 8, 160 dots at n_embd 2560) went through it: half of each row,
Gyro-S answered with loops, garbage or nothing on every RTX 50 / RTX PRO Blackwell card. They now take
launch_gu's full-warp kernel, as launch_gu itself always did for them; 80-dot formats are unchanged.

l620 (RTX PRO 5000, Gyro-S through serve/server.py, the owner's QA prompts, greedy and the app's sampling):
before 0/8 correct, after 8/8 ("STRATA_OK 42", "4", the follow-up sentence, "PING"), the same as
STRATA_SM120_PERF=0 / STRATA_GU_SWIGLU=0; Gyro-S fix vs STRATA_SM120_PERF=0: identity gate 10/10, exact_pp
4K / 20K IDENTICAL (KL 0).
2026-10-18 20:54:11 +02:00
Niko1221 707eebd1dd Web search: spaced and backed-off requests, a session cache, Bing as the second keyless source, one plain message when every source refuses
The default DuckDuckGo search failed with "limiting searches from this computer" after two or three quick searches (reproduced on a
Linux box: two searches 1.2 s apart answered, the third got HTTP 202 and every DuckDuckGo page, html and lite, refused for many minutes
while it was polled). Now: browser-shaped requests (Referer, Origin, Sec-Fetch-*), one request at a time per source 2.5 to 4 s apart with
random jitter and at most 8 a minute, a 30 minute session cache per question, a per-source cooldown after a refusal that doubles from
1 to 10 minutes (Retry-After honoured) during which that source is not asked, no lite-page follow-up after a refusal, one retry for a
timeout or 5xx. When DuckDuckGo refuses, Bing's result page is used (links decoded from its redirects). When every source refuses the
tool says, in one plain sentence, that web search is limited, to try again in a minute or add a key (Exa, Perplexity or DeepSeek) in
Settings -> Tools -> Web search, and tells the model not to retry. Tests use a mocked rate-limited search and a real Bing page fixture.
2026-10-18 20:53:37 +02:00
Niko1221 f4cb8095c4 Locales: the new plain sentences (number range, Add skill from another computer, screenshot saved, folder window) 2026-10-18 20:52:35 +02:00
Niko1221 2325b2ee26 Desktop: pictures can be dropped on the chat in the Windows window (the shell's own drop handler is off, the page gets the drop); the drop box is animated 2026-10-18 20:52:05 +02:00
Niko1221 7572afc5bf Monitor: saving the screenshot says so (it is in the Downloads folder) or says it failed 2026-10-18 20:51:04 +02:00
Niko1221 4794a8067f Test fix for the message box cap 2026-10-18 20:49:39 +02:00
Niko1221 95ece0bcd2 Chat: the message box stops at 30% of the window height and scrolls inside, so Send and the toolbar stay in view in a small window 2026-10-18 20:49:29 +02:00
Niko1221 e0aa3b872a Windows folder window: the host waits for the window to report it is opening and otherwise asks for the path in words, so the button is released; Add skill from another computer says why it cannot 2026-10-18 20:48:17 +02:00
Niko1221 1237d9a5fa downloads and models: live progress, parts counted, choices kept, clear NVFP4 names, More versions list
- native state: the host's 'native' events are applied at once; the light poll (1 s while a job runs, none while hidden) is no longer
  held by a slow forced read; a read that never answers is dropped after 20 s
- install progress: every file of the model is counted (parts, picture reader beside the model folders, NVFP4 packs by their pinned sizes);
  the bar no longer sits at 99 % while the picture reader downloads; the screen names the part (part 2 of 4) and lists the parts
- a config is 'ready' only when every part of a multi-part GGUF is whole
- Models page keeps version, context, computer use, picture reading and expert choices across tabs; Read pictures is on for a version that
  is not downloaded yet; the running download is named
- version list: smaller rows, a Downloaded tag, NVFP4 / NVFP4-NVIDIA / NVFP4-QAD named by who made them with one line each
- first-run page: the four recommended versions, then a More versions list (the Models page list)
2026-10-18 20:47:43 +02:00
Niko1221 337c70d3c2 Response settings: a number outside its range (Max output tokens -1) is refused with a plain sentence in the dialog and by the host 2026-10-18 20:46:09 +02:00
Niko1221 e6aa6f317f Skills: a skill created with Skill enabled off is saved off (and keeps its trigger phrases) 2026-10-18 20:44:33 +02:00
Niko1221 969d7da7df Narrow window: a dialog opened from the menu closes the menu and takes the keyboard (focus also moves in when animation frames are held back) 2026-10-18 20:44:33 +02:00
Niko1221 c0abfdc358 /loop stops by itself: a count, a time or a stop condition in words is read when the loop is created and enforced by the scheduler
"/loop 1m say hi, stop after 3 answers" kept firing and the model answered "stopped" every minute until /loop stop. The count
("stop after 3 answers", "three replies", "5 times"), the time ("for the next 2 hours", "stop after 30 minutes") and a stop
condition ("stop when it passes", "until the file exists") are now parsed out of the line, left out of the message the model gets,
and enforced by a new scheduler (lib/loop-runner.ts): after the last allowed answer nothing is sent again, and the person is told in
one plain sentence why it ended. The scheduler also ends the loop when an answer says it is done or stopped (a bare "Stopped."
always; "Done ..." and similar when a limit was stated; DONE as the first word when a stop condition was given in words), and after
three failed answers in a row. A busy chat waits without using up the count. Tests drive the scheduler with a scripted model and a
fake clock. Strings in 8 languages.
2026-10-18 20:41:28 +02:00
Niko1221 323a11e721 Connect app: the third tab and strata connect give the server root (no /v1); the server also accepts /v1/v1/messages
Messages-API clients (coding CLIs, the SDKs) add /v1/messages to their base URL themselves. The Connect dialog showed the OpenAI-style
base that ends in /v1, so those apps posted /v1/v1/messages and got a 404. The third tab now shows the root address with a
one-line note (the base-URL variable for coding CLIs), the other tabs keep /v1. strata connect cuts a pasted /v1 back to the root for
every app and says so for coding CLIs, and strata launch and the monitor's Connect view list the Messages-API address. The server (serve
and the app's host, plus the shared listener) reads /v1/v1/messages and /v1/v1/messages/count_tokens as the real paths; nothing else
is aliased. Tested with the real Python SDK client against the mock-engine server.
2026-10-18 20:40:33 +02:00
Niko1221 f4078faf4f Answers: reasoning wrapper tags and a stray leading tag fragment never show in the answer or the Markdown export (UI side only) 2026-10-18 20:36:25 +02:00
Niko1221 e09eccc839 Files edited: only report what the turn itself wrote (a cut-short first look no longer makes the rest of the folder new files); never look in app, engine, component, AppData or temp folders 2026-10-18 20:33:55 +02:00
Niko1221 83f2a2c73a model list: Qwen3.6-35B-A3B has its three sizes (the first-run page showed no versions)
The catalog only walked setup.FAMILIES; Qwen3.6 lives in OWN_PATH_FAMILIES, so the app's Qwen3.6 page had an empty list.
strata_models now lists its sizes (download, memory, minimum engine 0.1.45, pictures) and strata_install plans them.
2026-10-18 20:33:08 +02:00
Niko1221 66ad879d18 Merge f-149-default (bac1a3e0e) into f-split-int: Linux defaults for the GGUF read in place (zero-copy page-cached experts, io prefetch with the usefulness/room gate), cgroup-aware RAM check, docs 2026-10-18 17:22:54 +02:00
Niko1221 76ceb365ca Merge f-split-launchfix (8f2f34ec4) into f-split-int: strata launch refuses at once when no usable engine exists, before any model download 2026-10-18 16:52:18 +02:00
Niko1221 8f2f34ec4e strata launch: settle the engine before any model download
strata update exits 0 when the feed has no engine for this PC (or only one for another card family), so launch went on and downloaded the model before failing with the generic engine line. ensure_engine now re-checks the engine after the update step and refuses at once with the update's own plain sentence plus one next step; setup.py in Control mode stops at the engine step for a build of another card family, an old driver, or when launch asks for an engine (STRATA_NEED_ENGINE=1), before the download step. The app without any engine keeps its model-first order.
2026-10-18 16:50:19 +02:00
Niko1221 bac1a3e0e9 Merge f-149-prefetch (docs) into f-149-default 2026-10-18 16:31:49 +02:00
Niko1221 24dc4a1dd6 docs: the 0.2.0 Linux file-tier defaults - the measured A/B against the 0.1.4x defaults 2026-10-18 16:31:49 +02:00
Niko1221 44fbe69c66 Merge f-149-prefetch (GGUF-only defaults) into f-149-default 2026-10-18 15:58:23 +02:00
Niko1221 c31367979d 149: the Linux file-tier defaults only for the GGUF read in place
An experts.bin pack (the NVFP4 packs) keeps STRATA_COLD_OVERLAP, STRATA_IO_PREFETCH and STRATA_IO_PF_GATE
off unless they are set: the defaults were measured on the GGUF read in place only. The startup line and
docs/DETAILS.md say so.
2026-10-18 15:58:22 +02:00
Niko1221 240304928b Merge f-149-prefetch (82c80ac4d) into f-149-default (from f-split-int fb7c62963) 2026-10-18 15:39:49 +02:00
Niko1221 82c80ac4d7 149: Linux defaults - zero-copy page-cached experts, io prefetch and its decode gate; cgroup RAM check counts the clean page cache
- STRATA_COLD_OVERLAP defaults to 4 on Linux (0 elsewhere), STRATA_IO_PREFETCH to 1 on Linux for the
  file tier (mapped GGUF in place or experts.bin), STRATA_IO_PF_GATE to 1 with io prefetch. Off switches:
  STRATA_COLD_OVERLAP=0, STRATA_IO_PREFETCH=0, STRATA_IO_PF_GATE=0. One startup line names them.
- cgroup_available_bytes: every clean file page of the group (memory.stat file, active + inactive; else
  inactive_file) is reclaimable, less min(half of it, limit / 64), as MemAvailable counts page cache on a
  bare host. A container whose own reads left active cache was judged short of RAM and its file tier
  switched to unbuffered reads (an 18 GiB RAM tier in a 32 GiB group: 1.2 GiB available instead of ~11).
  Unit tests with mock memory.stat / memory.max values.
- The per-request cold-reads line only with STRATA_COLD_OVERLAP or STRATA_IO_STATS set.
- docs/DETAILS.md: the 0.2.0 Linux file-tier defaults.
2026-10-18 15:39:41 +02:00
Niko1221 fb7c62963e Merge f-152-users (2e111557b) into f-split-int: CPU worker counts per physical core with a cap of 32, host socket, quota; warning above the recommendation; setup caps what it writes 2026-10-18 15:38:16 +02:00
Niko1221 8b1139d026 149: STRATA_IO_PF_GATE=1 - the decode's read-ahead only where the page cache has room and it pays (opt-in)
With STRATA_IO_PREFETCH=1, the decode's read-ahead (pread on the I/O threads, the look-ahead two layers
deep) and its pread demand reads run only while the gate is open; the prompt path is never gated.
Checked every 16 decode windows:
- room: (the tightest cgroup v2 limit - the group's non-file memory, else MemAvailable) / the experts'
  bytes; closes below STRATA_IO_PF_GATE_LO 0.55, opens above STRATA_IO_PF_GATE_HI 0.65;
- pays: closes when fewer than 128 MB were read ahead in 16 windows, or under STRATA_IO_PF_GATE_USEFUL
  (0.10) of those bytes were used; then a probe every 256 windows.
Closed, the look-ahead predicts one layer and warms with the madvise hint, as without io prefetch. The
decode thread (and the fetch threads it starts) is marked by FileExpertSource::DecodeScope. The serve
log's file-tier line says the room, the share used, the state and the flips. A request's wait for the
adaptive round in flight at its start is logged when >= 1 ms.

5 pairs (Q2_0 GGUF in place, 12 GB card emulated, cold SSD, cgroup RAM caps), mode 4 + IO_PREFETCH +
gate vs mode 4 alone: 32 GB 1.16 / 1.21 / 1.70 decode, 1.86 / 1.98 prompts; 16 GB (closed) 1.00 / 1.01 /
1.10, 1.54 / 1.78 prompts; an 18 GiB RAM tier 0.91 / 0.84 / 0.86 (its IO_PREFETCH start re-checks the
RAM inside the cgroup and switches the file tier to unbuffered - not the gate's doing).
Identity gates vs b829a3189 (fixed flags, Q2_0 and IQ3_S, default and mode 4 + IO_PREFETCH + gate with
--mmap-experts): 10/10 and exact_pp 4K / 20K IDENTICAL in all four.
2026-10-18 15:19:59 +02:00
Niko1221 7b06ea51bd Merge f-split-fb3 into f-split-int (locale keys re-added after the card-list strings) 2026-10-18 15:02:48 +02:00
Niko1221 2e111557b0 docs: how many CPU threads the engine starts and when a configured count is warned about 2026-10-18 14:59:31 +02:00
Niko1221 c47fbb3f22 Merge f-split-fb5 (cd1b3bd31) into f-split-int: the card list preselects the card a loaded model runs on; sidebar shows version and GPU 2026-10-18 14:57:18 +02:00
Niko1221 cd1b3bd31a Models: the card list preselects the card a loaded model runs on; the sidebar model card says the version and the card (Q2_0 · GPU 2) 2026-10-18 14:46:30 +02:00
Niko1221 0c7872ddc2 Merge f-split-pics3 (ea1e28436) into f-split-int: NVFP4-QAD reads pictures; setup accepts a prepared model without its checkpoint 2026-10-18 14:45:12 +02:00
Niko1221 ea1e28436f TUI test: the model without pictures is a stand-in now that Gyro reads them 2026-10-18 14:43:02 +02:00
Niko1221 261145baaf Merge f-split-fb4 (201d674e9) into f-split-int: cloudflared discovery, settings file, Gyro minimum engine, two models on two GPUs 2026-10-18 14:42:15 +02:00
Niko1221 201d674e94 Models: GPU numbering matches the other screens, friendly model names, fit figures less what the kept models use
Owner-approved for 0.2.0. The card list says (GPU n) with the same numbers as Monitor and the picker; a loaded
model is named from the catalog (Qwen3.8-Flash-Next Q2_0) in the picker, the Loaded list, the sidebar card and the
Monitor, the raw file id only as a fallback; with Run multiple models on, This computer and the card rows show what is
left after the models that stay loaded (a short muted line says so), in 8 languages. The picker rows no longer
print the raw id beside the name.
2026-10-18 14:41:11 +02:00
Niko1221 4eeccd9c00 Task handover: markdown bubble, clean chat title, long step lists capped 2026-10-18 14:39:45 +02:00
Niko1221 2dbd302038 setup: a prepared NVFP4-QAD model needs none of the checkpoint's safetensors; NVFP4-QAD reads pictures
ready_needed() drops the checkpoint's tensors/* once the model is prepared (prepared_done): the engine reads the prepared files only. have_model, ready_fetch, the already-downloaded search in other Strata folders and the disk need now use it, so a prepared model whose 95 GB of safetensors were deleted is ready instead of failing with 'not enough free disk' and a 102 GB download.
NVFP4-QAD (FP4-Local) reads pictures: vision True in setup.py and the MCP catalog; the config's vision section uses prepared/dense.gguf as the text model with the shared mmproj, like the GPTQ pack.
Tests for both; the launch test for a model without pictures now stands one in.
2026-10-18 14:32:19 +02:00
Niko1221 6149cec03e cloudflared: only Strata's own tunnel (same port or same token) blocks a start; other running connectors are listed in plain words with their PID
The coordinator's rule: the owner PC and a tester's system service run connectors for unrelated tunnels, and
quick tunnels must keep working. Running connectors are listed (pid; the command line is only matched,
never shown). A refusal only when one is clearly Strata's own: --url at Strata's port, or the same token.
Terminal (strata launch) follows the same rule. Checked with a real cloudflared on r9700.
2026-10-18 14:24:08 +02:00
Niko1221 5eea92bb09 Helper run no longer ends early when the helper reports while the chat model is still answering (the "interrupted before it finished" error) 2026-10-18 14:17:53 +02:00
Niko1221 6ad1ad69ab Settings file check: the app's own ui_* notes are not unknown keys (found by checking the configs the app wrote on r9700) 2026-10-18 14:14:18 +02:00
Niko1221 c1440388fb Two models on two GPUs in the app: say which card a loaded model uses, prefer a free card for the second, explain a failed start on a shared card, show an unload refusal beside its button, keep a failed instance out of the chat picker
Found running the AMD AppImage on r9700 (Q2_0 on card 1, IQ3_XXS on card 2, Run multiple models):
- the card list ignored loaded models (same text, same pre-selected card); a second model on a used card died
  after two slow engine starts with 'server_exited'. Now the row says 'Already used by <model>', a free card is
  pre-selected while Run multiple models is on, and a failed start there is one plain sentence (no useless retry).
- Unload refused because a chat still used the model (409) showed nothing near the button; it now shows the
  sentence and the next step in the Loaded models card.
- a model that failed to start was listed in the chat picker (disabled); it is hidden.
2026-10-18 14:13:35 +02:00
Niko1221 e2d03ac4fa scripted worker: multi script ends with its own text 2026-10-18 13:41:41 +02:00
Niko1221 b533ae5246 Skills used under an answer; Continue in a new chat on a task (owner-approved mockups), 8 languages, tests 2026-10-18 13:41:17 +02:00
Niko1221 7a70fcccbc Remove the generated Tauri schema files a cargo test run wrote 2026-10-18 13:34:44 +02:00
Niko1221 796937d485 agent README: model choice, reasoning, pictures 2026-10-18 13:34:34 +02:00
Niko1221 e932ad7eb7 Plain next step after a refused picture names the new switch 2026-10-18 13:31:33 +02:00
Niko1221 3084cd93de pics-e2e: on-by-default, OFF, silent server, agent step 2026-10-18 13:29:04 +02:00
Niko1221 14691f6e53 Control launcher: shell built-ins only (no dirname)
Inside the AppImage LD_LIBRARY_PATH holds the bundled libraries, and on Ubuntu 26.04 (uutils coreutils)
dirname fails there: 'libsystemd.so.0: version LIBSYSTEMD_254 not found'. The python/python3 launchers then
computed an empty folder and the app said Control stopped / no engine. Found with the AMD AppImage on r9700.
2026-10-18 13:24:36 +02:00
Niko1221 36e06fcd86 pictures-ui: temp path 2026-10-18 13:23:24 +02:00
Niko1221 65ac8f9106 Pictures: 8-language strings, plain-error rule narrowed, status unit test, real-browser paste/+/drop/off script 2026-10-18 13:22:51 +02:00
Niko1221 138075961f Merge f-148-intel-xmxdef (cfe65f8d1) into f-split-int2 2026-10-18 13:21:18 +02:00
Niko1221 bec4c838ab CPU pool plan: hybrid CPUs with more E-cores than P-cores default to the P-cores but the host's plus half of the E-cores (Core Ultra 7 265: 12-16 workers +12.6% decode over 19; 24 workers -44%) 2026-10-18 13:18:38 +02:00
Niko1221 af26053565 Pictures on by default everywhere: paste, shrink big pictures, one status for composer and Send, plain tool error, per-model off switch wording 2026-10-18 13:12:12 +02:00
Niko1221 cfe65f8d1a docs/sycl: PF_XMX=2 Alchemist default approved by the owner 2026-10-18 (comment and INTEL.md heading only) 2026-10-18 13:11:44 +02:00
Niko1221 ae40bd7516 Merge f-148-intel-xmxdef (12fd8cff6) into f-split-int2 2026-10-18 13:10:38 +02:00
Niko1221 79e94f37f0 OpenRouter reasoning from the model list (supported_parameters), effort sent as reasoning.effort; API pictures on by default 2026-10-18 13:07:03 +02:00
Niko1221 bca49fc5f2 Helper agents on a named model: forgiving model choice (id, provider/id, name, local), did-you-mean, plain list, guidance for every model 2026-10-18 13:05:00 +02:00
Niko1221 d631ed2da7 Gyro: the minimum engine is 0.1.45.1, not 0.1.45; the app says 'needs a newer engine' with the update step
Gyro's expert files are native_experts.txt v5 (Hadamard-folded). The engine reads v5 since the APR port
(f47b0385f); the released 0.1.45 stops at v4 (its binary and source have no v5 reader, every 0.1.45.1+ package
has). GYRO_ENGINE in setup.py and the catalog the app reads (tools/strata_mcp.py: engine in the fallback table,
engine_min on every size) now say 0.1.45.1. The host marks sizes the installed engine cannot read
(needsNewerEngine; an engine built from this checkout or with no readable version is never called too old),
refuses the download and the load with a plain sentence, and Models / first-run setup show the existing
'This model needs a newer engine' notice with Update engine and hold the buttons back.
2026-10-18 13:04:09 +02:00
Niko1221 036c8f6ec3 CPU pool plan: cap 32 (measured on the 32-core EPYC: 24 lost 2-5% decode, 32 and 48 equal, 63 oversubscribed lost 35%) 2026-10-18 13:03:10 +02:00
Niko1221 9319219623 Expert settings: Open settings file (the model's run config in the system text editor), and a plain check of it before every start
The per-model settings already live in one text file (strata-<model>.json: args, env, sampling and the
named keys). Expert settings gets an Open settings file button (Notepad, open -t, or the system's
plain-text program) with the note that changes apply on the next start. The file is checked when the
panel opens, when the window comes back into focus, and before a load: bad JSON, a wrong value, an
unknown key (the nearest real name is offered) are said in plain words with the line, in 8 languages.
A mistake holds the start back instead of an engine that dies on it; an unknown key only warns.
2026-10-18 12:59:05 +02:00
Niko1221 31f225e4fb cloudflared: find a system install (PATH and the usual places), check its version, say which one is used, and never start a second connector next to a running one
The app only looked in Strata's own connector folders, so a cloudflared installed by the system
(/usr/bin, brew, winget, a .deb) was reported as not installed. Now: own folder, component folder,
PATH, then /usr/bin, /usr/local/bin, ~/.local/bin, Homebrew, Program Files. The first copy that runs
--version and is 2022.1.0 or newer is used; the Connector step names it and its place. A cloudflared
already running (system service or started by the person) is said so plainly and not doubled.
Terminal (strata launch) gets the same search and check. Plain messages in 8 languages.
2026-10-18 12:52:12 +02:00
Niko1221 eeb05631fe tests: the Mac catalog test looked for '0 GB' in the whole computer line, which also holds the free-disk figure ('60 GB free'); it now checks the card part only 2026-10-18 12:51:17 +02:00
Niko1221 8a66da38a5 Merge f-split-pics2 (ac658f54a) into f-split-int2 2026-10-18 12:51:01 +02:00
Niko1221 a451dcd913 Merge f-148-intel (fca3c5958) into f-split-int2 2026-10-18 12:38:59 +02:00
Niko1221 ac658f54a6 Test: strata-vision-gguf-test wrapper and Gyro pictures flag 2026-10-18 12:25:37 +02:00
Niko1221 12fd8cff65 sycl: STRATA_PF_XMX=2 is the default on Arc Alchemist (opt-out STRATA_PF_XMX=0); Battlemage unchanged
A750 prompt prefill +30..39% at 4K (mode 2: 79.3 -> 110.4 tok/s, 5 of 5 rounds), +32..35% at 1-2K tokens; decode and cache slots unchanged.
Teacher-forced KL against the default 0.0002 (floor 0.0001), first-token KL 0.0017 (floor 0.0005), tasks inside the default's run-to-run spread.
Docs: INTEL.md has the numbers.
2026-10-18 12:22:23 +02:00
Niko1221 fca3c59586 sycl prefill: DMA an expert straight from the arena only when it is USM host memory (A750 prompt deadlock)
SYCL has no cudaHostRegister, so the expert arena is pageable memory even where the engine calls it pinned. A copy->memcpy of a 2.3 MB blob
from it goes through the Level Zero driver's staging path on the calling thread, which never returned on some prompts (A750, default
flags, a 2,744-token docs excerpt: the thread spun in sched_yield inside libze_intel_gpu with every engine idle, 6 of 6 runs; also seen
in a 5.3K-token needle prompt of the task suite). Only USM host allocations (the --stream-experts pinned mirror) are DMA'd directly now
(answer cached per 2 MiB bucket); every other expert goes through the stager's pinned buffers like an unpinned one.
STRATA_PF_DMA_PAGEABLE=1 restores the old direct copy.
Checks: the repro completes 3 of 3 (0 of 6 before); identical first-token logits and tokens, old vs new: A750 (frozen share, 1.4K, 2.5K and
4K prompts), B70 stock config and B70 without --stream-experts (there the new path is also faster: prefill 680-740 vs 77-456 tok/s); A750 4K
prefill 79.4 vs 79.3 tok/s, B70 stock 1246 vs 1240.
Adds sycl/tools/test_prompt_deadlock.sh with the 2,744-token prompt (passes on the fix, times out on the old build).
2026-10-18 12:19:14 +02:00
Niko1221 35af455aec Gyro catalog test: pictures are on 2026-10-18 12:16:56 +02:00
Niko1221 3fe84ec28d strata-vision reads only the vocabulary and header of the text model; Gyro reads pictures
llama.cpp refuses a GGUF whose tensor directory has a type it does not know, so the picture reader could not open a Gyro model (TQK6 / TQK7, ids 145 and 146) and Gyro was left without pictures. strata-vision now copies the key/value header into a small tensor-less GGUF and loads that vocab-only; the weights are never read or type-checked. Falls back to the file itself when the header holds a value type the copier does not handle.
Gyro-S and Gyro-M read pictures (setup.py, MCP catalog). Adds a C++ test (strata-vision-gguf-test) with a GGUF that has unknown tensor types.
2026-10-18 12:14:19 +02:00
Niko1221 83cde2ca30 Merge f-151-b70 (c765c5f87) into f-split-int2 2026-10-18 11:29:12 +02:00
Niko1221 5ece068f7e CPU expert pool: default worker count from physical cores, host socket, quota and a cap; warning for a configured count above it; setup caps what it writes 2026-10-18 11:20:20 +02:00
Niko1221 196a683d6d docs: the prompt chunk planner (file tier, Linux) - what it does, where it applies, the measured numbers, STRATA_PREFILL_HOST_PLAN=0 2026-10-18 11:14:16 +02:00
Niko1221 fd46e34d5c Merge f-150-planner (8253477bf) into f-split-int2 2026-10-18 11:13:37 +02:00
Niko1221 c765c5f87b sycl: int8 prompt experts default to auto on Battlemage (bmg-g21 / bmg-g31 by architecture id), FP16 elsewhere; STRATA_PREFILL_MMQ=0 off, =1 forced; launch failure or missing kernels fall back to the FP16 chain with one log line; load-time log line; mode-choice unit test; docs 2026-10-18 10:51:21 +02:00
Niko1221 983fa18ad3 docs: B70 follow-up measurements (GDN variant, XMX attention, attention layout, KqSplit, group size); static_assert on the group size 2026-10-18 10:31:41 +02:00
Niko1221 5bcc208a39 sycl: QSA prompt attention: probabilities in [cell][head] order with vector reads in the value phase (default, bit-identical, +0.9% prompt on the B70); STRATA_ATTN_RS=1 reduce-scatter scores measured -1.5% 2026-10-18 09:53:11 +02:00
Niko1221 0e58a652fb tests: iq_multi_parity under STRATA_KQ_SPLIT=1 also without V2K, with the fused gate/up SwiGLU, and with the sub-16 gate/up rows off
The NVIDIA worker's non-bitwise result came from a cherry-pick onto a tree without the dot-count guard in launch_gu (K-quant rows have 160 dots, the sub-16 rows assume 80); the merged tree has it and the Split path is bitwise on every route.
2026-10-18 09:50:49 +02:00
Niko1221 88c1502dc7 pt: s3158 without the word 'demora' (the no-demo catalog test matches 'demo' inside it) 2026-10-18 09:34:41 +02:00
Niko1221 97b680e13e gfx12: the WMMA prompt attention and the Q2_0 fused prompt experts are default-on on Linux only; Windows HIP builds keep them opt-in until tested there
core/gfx12_platform_defaults.hpp holds the per-platform rule (checked in arch_defaults_test); the engine log names it at the first prompt; docs/AMD_HIP.md.
2026-10-18 09:34:37 +02:00
Niko1221 a77d9fab54 Merge f-149-prefetch (2044864db) into f-split-int2 2026-10-18 09:34:20 +02:00
Niko1221 58f81f20e3 Merge f-151-amd (d9070189c) into f-split-int2
Conflicts with 148-amd's gfx12 WMMA default resolved: STRATA_HIP_WMMA unset = gfx12 on every call (148-amd), 0 = off, 1 = forced (also gfx11), auto = gfx12 with n_q >= STRATA_HIP_WMMA_MIN_Q; failed launch falls back to the FP32 kernel; the 96-slot ring follows the same state; the opt-in hint is gone.
2026-10-18 09:34:20 +02:00
Niko1221 8253477bf9 Prefill planner: request-sized chunks. A prompt that fits today's chunk keeps one chunk; a longer one reads in equal chunks of at most the plan's size (no stub tail). Buffers stay sized for today's chunk. 2026-10-18 09:20:24 +02:00
Niko1221 6aa8e25f49 docs: INTEL.md section for the fused int8 expert path (numbers, quality, crossover) 2026-10-18 09:19:38 +02:00
Niko1221 af3770c6b0 Merge f-148-amd (2053388dc, without the IQ2 fused default) into f-split-int2 2026-10-18 09:18:46 +02:00
Niko1221 40a2216b42 Merge f-148-nv into f-split-int2 2026-10-18 09:18:38 +02:00
Niko1221 2e1d62961a Merge f-split-pics into f-split-int2 2026-10-18 09:18:38 +02:00
Niko1221 b6a38b3288 Merge f-split-linuxui (83aa59fdc) into f-split-int2 2026-10-18 09:18:33 +02:00
Niko1221 d9070189c0 docs: --kv-grow on HIP with numbers, STRATA_PREFILL_HELP on two R9700 2026-10-18 08:32:28 +02:00
Niko1221 2725c23f74 NVIDIA sm_86 and sm_60: the measured bitwise decode switches on by default per architecture (nv_defaults; STRATA_SM86_PERF=0 / STRATA_SM60_PERF=0 turn them off) 2026-10-18 08:05:20 +02:00
Niko1221 3980a5ce70 HIP: virtual memory ranges (hipMemAddressReserve/Create/Map/SetAccess, 2 MiB chunks) so --kv-grow can run on ROCm 6+ (STRATA_HIP_VMM=0 turns it off) 2026-10-18 07:58:25 +02:00
Niko1221 2044864dba 149: STRATA_COLD_OVERLAP=4 (zero-copy without the overlap); no residency query for RAM-resident experts
The zero-copy pre-pass ran mincore for every CPU expert of a layer, the RAM copy's too: -2..-9% decode
with an 18 GiB RAM tier. It now skips the RAM-resident and override experts (b32: 0.98/0.97/0.91 ->
1.05/1.10/1.08 vs mode 0, 3 pairs). A file without a residency query (not Linux) never goes zero-copy.
2026-10-18 07:12:43 +02:00
Niko1221 9a01b6e3b8 sycl: int8 expert products: work-group kernels (activation tiles and codebook in local memory), STRATA_PREFILL_MMQ=auto from a measured per-GPU crossover cached per device/driver/format, per-group FP16 fallback, per-device state, clamped down-matrix reads, AOT guard for non-Xe2, docs; 4K +12.5%, 1K +20% with identical tokens 2026-10-18 07:09:40 +02:00
Niko1221 961d37cd55 gfx12: the 96-slot prompt ring follows the matrix-core attention's real state (STRATA_HIP_WMMA=1/auto on a card that has it, not failed) 2026-10-18 06:21:31 +02:00
Niko1221 2053388dc9 docs: AMD_HIP.md - the gfx12 matrix-core prompt attention and the Q2_0 fused prompt experts as defaults, with the 148-amd quality and speed numbers 2026-10-18 06:08:29 +02:00
Niko1221 ebdfaacd1c sycl: int8 DPAS expert products for IQ3_XXS and IQ2_S gate/up and Q2_0 down too (every layer type of the IQ3_S pack but one), thread tile NT=1 with MT 2/4/8, parity test over all six type pairs; STRATA_PREFILL_MMQ=1 prompts +10% (4K), +16% (2K), +20% (1K) with identical tokens 2026-10-18 05:54:34 +02:00
Niko1221 9c4516509f tests: iq_multi_parity runs with STRATA_KQ_SPLIT=1 2026-10-18 05:35:23 +02:00
Niko1221 4908ddaa49 docs: STRATA_HIP_WMMA=auto, the first-prompt warm-up, the gfx1201 hipBLASLt version note; timing: the first chunk's PLE wait on the ple line 2026-10-18 05:35:05 +02:00
Niko1221 d8471050bd Prefill planner: refit the miss share to two rigs (#2824 SATA 2x3060, P2003 NVMe cgroup sweep); read the drive class from the GGUF path 2026-10-18 05:09:14 +02:00
Niko1221 2856a8045e sycl: fused int8 DPAS expert products for the prompt path (ESIMD, Battlemage): IQ3_S gate/up + IQ4_NL down, int8 q8_1 activations, parity test, opt-in STRATA_PREFILL_MMQ=1; first correct version, not yet faster than dequantize + oneMKL 2026-10-18 05:08:50 +02:00
Niko1221 c85cc6dd19 149: io prefetch scheduling, RAM_ADAPT2, cold-read overlap and zero-copy GGUF experts (all opt-in)
- STRATA_IO_PF_SCHED=1 (with STRATA_IO_PREFETCH=1): confidence-scored read-ahead from the router
  lookahead, per-window speculative byte budget, deadlines (late jobs dropped), useful-share feedback;
  new per-request counters (read ahead MB, used before demand, late, not admitted).
- STRATA_RAM_ADAPT2=1 (with STRATA_RAM_ADAPT=n): RAM-tier promotions ranked by expected stall saved per
  byte (drive vs page cache), read on the source's I/O thread, installed at window boundaries; the
  prompt's routing seeds it with a limited weight (Prefill::on_routes).
- STRATA_COLD_OVERLAP: 1 reads a layer's staged blobs in the background while the pool computes the
  ready experts, each landed group computed as it lands; 2 measures R / C_r / C_c per layer; 3 adds
  zero-copy and 4 is zero-copy alone: an expert the page cache holds whole (GGUF in place, Linux mincore)
  is computed from the mapping through a per-job NativeFmt (ExpertJobMulti::fmt) instead of being
  assembled into a stage buffer. Same bytes, same rows, same bits.

Default (all unset) is the old code path: identity gates vs b829a3189 IDENTICAL (Q2_0 and IQ3_S,
fixed flags, 10/10 + exact_pp 4K/20K); mode 3 with --mmap-experts also 10/10 + 4K/20K IDENTICAL.
2026-10-18 05:06:29 +02:00
Niko1221 aef32e224b prefill timing: the hyper-connection write-back on its own line (it was charged to the phase before it, e.g. the wait for the first chunk's PLE rows); hip_prefill_gr_write_bench 2026-10-18 04:57:28 +02:00
Niko1221 83aa59fdc5 setup: say why the page waits while the bundled engine unpacks (8 languages) 2026-10-18 04:42:27 +02:00
Niko1221 f3be974359 prefill: warm the first BF16 and FP16 hipBLAS products on a thread at start (STRATA_PREFILL_WARM=0 skips), STRATA_COLD_TRACE diagnostic 2026-10-18 04:24:19 +02:00
Niko1221 9b75c6a127 desktop: host_command test (loopback bind forced, shutdown secret, plain refusal); Cargo.lock follows the app version 2026-10-18 04:17:21 +02:00
Niko1221 219ae436fa setup_intel: recommend --prefill 8192 for a Battlemage card in the borrowing (over 32K context) config, with the measured numbers and a test 2026-10-18 04:07:31 +02:00
Niko1221 79ac3164a0 gfx12: STRATA_HIP_WMMA=auto (prompt attention on matrix cores chosen per call, FP32 fallback on a failed launch); Split traits for Q4_K/Q5_K/Q8_0 (STRATA_KQ_SPLIT=1, bitwise, opt-in) with iq_multi_parity cases 2026-10-18 03:51:49 +02:00
Niko1221 2c6bfef271 gfx12: the native Q2_0 pack's gate/up take the fused int8 prompt experts by default (STRATA_PF_FUSED=0 keeps MMQ)
R9700 Q2_0: prompt 4K +16.7%, 20K +19.0% (5/5 pairs); teacher-forced KL 0.0004 over 6,022 positions, 0.0016 over 1,760 long-document ones.
2026-10-18 03:47:13 +02:00
Niko1221 99f6d47233 gfx12: the matrix-core prompt attention (STRATA_HIP_WMMA) is the default; STRATA_HIP_WMMA=0 turns it off
Teacher-forced KL against the old kernel on an R9700 (Q2_0, int8 KV, fixed cache so the floor is 0.0000): 0.0003 over 6,024
positions (p99 0.006, top-1 99.8%), 0.0014 over 1,760 positions of 8 long documents (top-1 98.9%); HumanEval, reasoning, agent,
needles, CJK and tool calls equal. gfx11 stays opt-in. The 96-slot prompt ring follows the kernel (qsa_hip_wmma_active()).
The start-up hint about the switch is gone.
2026-10-18 03:47:13 +02:00
Niko1221 577d644480 sycl: STRATA_FOLLOW / STRATA_FOLLOW_LOGPOS test path in the serve decode loop (teacher-forced KL), off unless set; measurement tooling 2026-10-18 03:40:58 +02:00
Niko1221 b1d4686f4a pics-e2e: optional API part, model match and context options 2026-10-18 03:25:01 +02:00
Niko1221 00d2f12a2d desktoplog: drop an unused constructor 2026-10-18 03:24:55 +02:00
Niko1221 e77f1dfc3f Prefill planner: price the host (RAM page cache, displaced experts, drive) when choosing chunk and ring (#2824)
The auto chunk scan only tested the GPU cache.  A lent slot's expert comes back from the RAM copy or the model file for the length of a
prompt, and every chunk streams the whole file set; past the page cache that set is re-read per chunk (28-45 GB per 29K prompt on
2x RTX 3060 / 32 GB / IQ2_XS at 8192-token chunks, ~1 GB at 4096).  prefill_plan.hpp is the pure model (file set, miss share fitted to
#2824, additive stream + compute + drive time); the serve scan and the generate path refine the VRAM pick with it, after the RAM copy
exists.  A set that fits the page cache keeps today's pick.  STRATA_PREFILL_HOST_PLAN=0 turns it off; --prefill N is untouched.
2026-10-18 03:21:24 +02:00
Niko1221 4306182741 tester READMEs: --save-logs, crash log location, the app version from Cargo.toml 2026-10-18 03:21:01 +02:00
Niko1221 1b003bd406 desktop shell: logs, crash log, --save-logs; the window blocks startViewTransition; one app version (Cargo.toml, 0.2.0-beta); banner while the bundled engine unpacks; diagnostics include the desktop logs 2026-10-18 03:19:07 +02:00
Niko1221 e4ed5d1a4e Pictures: untick means does not read, API refusal hint, e2e catalog check skips known false 2026-10-18 03:16:43 +02:00
Niko1221 52f50ba1f1 Pictures: plainer refusal wording, e2e reads answer tokens only 2026-10-18 03:15:43 +02:00
Niko1221 a3cf8a647b Pictures reach models: catalog marks picture-capable models, API models take image_url, import knows picture readers
The catalog no longer forces images:false in the app; a model reads pictures when it is picture-capable and the engine component carries strata-vision.
API models: a model reads pictures when the person ticks 'This model reads pictures' or the server says so (model list modalities, llama-server /props); the chat sends image_url to any OpenAI-style API that does, and says in plain words how to fix every case where it cannot.
Importing a model folder recognises the picture reader next to it and says plainly when a model type is not ours (use a Custom model API).
2026-10-18 03:14:46 +02:00
Niko1221 6c16529a84 ui: Quit dialog can be dismissed after Strata has stopped 2026-10-18 02:21:09 +02:00
Niko1221 01c8381ce6 make-tester-readme --verify: hash only the files the README names; a sentence that merely names a file is not a stale hash 2026-10-18 02:14:23 +02:00
Niko1221 efa888b143 Merge commit 'b1e0fdaa4' into f-split-int 2026-10-18 01:32:22 +02:00
Niko1221 c37a3c415b repack-appimage: write <out>.sha256 from the finished file 2026-10-18 01:30:50 +02:00
Niko1221 dd1a55f99a make-tester-readme: verify by checksum set 2026-10-18 01:30:04 +02:00
Niko1221 51c60366b8 make-tester-readme: plain replacement 2026-10-18 01:29:06 +02:00
Niko1221 1b27554b9c make-tester-readme: templates may have CRLF (archives of a Windows checkout) 2026-10-18 01:28:00 +02:00
Niko1221 c1a5b13fdf sycl: PcieAuto on SYCL, opt-in STRATA_PCIE_FRAC_DEFAULT=auto (single card, native pack): the PCIe share refined from the CPU pool's speed in the first decode windows 2026-10-18 01:27:12 +02:00
Niko1221 9d868f38e8 make-tester-readme: templates may have CRLF (archives of a Windows checkout) 2026-10-18 01:27:11 +02:00
Niko1221 3409d9aaec gfx12: the native Q2_0 pack's gate/up on the fused int8 prompt experts (STRATA_PF_FUSED=1; opt-in until KL-checked)
native_w11_kernel's gate/up path walked 256-value super-blocks only; a Q2_0 row is 64-value blocks, so gate/up of that
format now takes the same one-block-a-stage walk its down path has. R9700 Q2_0: prompt +16.7% (4K) / +19.0% (20K),
5/5 pairs; hip_prefill_fused_iq Q2_0 / Q2_0: fused vs MMQ rel RMS 1.6e-5.
2026-10-18 01:26:43 +02:00
Niko1221 20f92128c5 tester README test as unittest 2026-10-18 01:25:02 +02:00
Niko1221 b1e0fdaa4b sm_120: PF_FUSED default and per-token HC down blocks only on 80+ SM cards
The RTX 5070 check (48 SMs, IQ2_XS pack, experts partly in RAM): the
fused int8 prompt experts were 4.7% slower on 20K prompts (0/6 pairs) with
no decode gain, and one token a block row could not be told apart from
token pairs in the hyper-connection down projection (an earlier run leaned
to pairs).  So on a 12.0 card with fewer than 80 SMs the prompt experts
keep MMQ (STRATA_PF_FUSED=1 still takes the fused kernels) and the down
projection takes token pairs at every even launch (STRATA_GR_DOWN_TG=2);
12.0 cards with 80+ SMs (the RTX PRO 5000 measured) are unchanged.
2026-10-18 01:24:48 +02:00
Niko1221 a0469e9455 tester READMEs: templates with checksums computed from the files, make-tester-readme.sh (+ --verify), test 2026-10-18 01:24:05 +02:00
Niko1221 e1168345f1 desktop: tester README templates (hashes filled at build time), plain_path warning 2026-10-18 01:15:48 +02:00
Niko1221 5c5fdafb94 sycl: STRATA_FOLLOW / STRATA_FOLLOW_LOGPOS test path in the serve decode loop (teacher-forced KL), off unless set; measurement tooling 2026-10-18 01:09:08 +02:00
Niko1221 eaddf15b77 desktop: Mutex import only where used 2026-10-18 01:02:33 +02:00
Niko1221 a8fdceba5b ui: Quit Strata in the menu in browser mode (confirm, stopped screen), 8 languages; host: open the page without changing the mode 2026-10-18 01:01:32 +02:00
Niko1221 8be9e0976f desktop (Linux): the shell owns the host, the window is its own process; a crashed window moves Strata to the browser (--browser, STRATA_UI=browser, --quit, session file) 2026-10-18 00:58:54 +02:00
Niko1221 e01f766118 host: window mode vs browser mode (app-ui.mjs): Quit Strata, the shell's UI route, app updater unavailable in browser mode with a plain message 2026-10-18 00:55:37 +02:00
Niko1221 401169c02f ui: no route cross-fade (View Transitions) on WebKitGTK, which crashes its UI process on startViewTransition (null pointer in AcceleratedBackingStore::update) 2026-10-18 00:47:02 +02:00
Niko1221 b829a3189d docs: the 12.0 defaults with the final A/B (decode +14-16%, prompts +3-10%), PF_FUSED quality, the ple-io rule 2026-10-18 00:27:27 +02:00
Niko1221 b25b309703 setup: --ple-io ram on an SSD too when the RAM can spare the n-gram table (Linux)
A new prompt's first chunk waits for its n-gram rows (65,536 random SSD
reads at 4K tokens) unless the table is in RAM.  RTX PRO 5000 (Linux, NVMe,
251 GB, a fresh engine per prompt, 3 rounds, the same bits): prompt 4K
1298 -> 1059 ms (-18%), 20K 4644 -> 4223 (-9%), 98K 20447 -> 20027 (-2%),
decode +0.8%.  On an R9700 (slow prompt) the read hides behind layer 0 and
nothing changes; under 64-200 GB RAM caps (cgroup, page cache included)
ram mode equalled direct there and cost ~6 s of a cold start.

The rule: not low-RAM mode, Linux (not WSL), and RAM >= the model's own
need + the table + 16 GB (Q2_0: 93 GB, IQ3_S: 107 GB); a smaller PC keeps
the SSD reads as before.  Setup says what it did and how to undo it.
Tests: RotationalDisk gets three SSD cases; the golden configs keep the
Windows behaviour (ple_ram_platform_ok mocked).
2026-10-18 00:06:47 +02:00
Niko1221 959e6c34c4 prefill: fused int8 prompt experts by default on sm_120 (IQ2_S/IQ3_XXS/IQ3_S/IQ4_XS)
Owner decision, the AMD gfx12 rule: on a compute capability 12.0 card the
native packs' prompt experts run on the fused int8 kernels (moe_fused_iq)
for the gate/up formats measured there - the IQ3_S pack's IQ2_S, IQ3_XXS,
IQ3_S and IQ4_XS layers with IQ4_NL / Q2_0 down.  IQ2_XXS / IQ2_XS gate/up
keep MMQ until measured.  This rounds differently from MMQ:

RTX PRO 5000, IQ3_S: prompt 4K +7.0%, 20K +5.2%, 98K +3.4% (3/3 pairs);
teacher-forced KL against MMQ 0.0002 (5998 positions, noise floor 0.0000)
and 0.0010 (1760 positions after ~6K-token documents); first-token KL
against the FP16 prompt path 0.01181 vs MMQ's 0.01236 (48 prompts of
1.5K-24K tokens); HumanEval 156/164, reasoning 26/30, agent 6/6, needles
6/6, CJK 8/8, tool calls 4/5 - the same as MMQ.

STRATA_PF_FUSED=0 keeps MMQ, =1 takes every covered format (as before),
STRATA_SM120_PERF=0 drops the 12.0 default.  Other cards: unchanged.
2026-10-18 00:06:47 +02:00
Niko1221 75afe3f4a0 experts: gate/up, SwiGLU and q8_1 in one launch (bitwise; default on sm_120)
native_gu_swiglu_kernel: a 512-thread block owns 32 hidden columns of a
group; each 16-lane sub-warp computes gate row r and up row r together
(row_dot2_80_sub16: both rows' loads before the applies, each row's sums in
row_dot_80_sub16's order), the block forms h = silu(gate) * up in shared
memory, and warp k writes entry k's q8_1 block with q8_1_store.  The values
are the separate kernels' bit for bit (gate/up are the same floats, the
product is rounded on its own as in swiglu_q8_1_entries_kernel); it saves
the gate/up round trip and one launch per layer.

RTX PRO 5000 (IQ3_S, all experts resident): a layer's gate/up + SwiGLU
35.5 + 2.4 us -> 36.3 us; fixed 4-token rounds 11.92 -> 11.80 ms; default
flags, 6 interleaved pairs: decode +0.6% story, +0.9% code, +0.5% 8K
document, 6/6 pairs each, the same tokens in every case.

On by default for compute capability 12.0 (sm120_series.hpp); elsewhere
opt-in with STRATA_GU_SWIGLU=1; STRATA_GU_SWIGLU=0 turns it off.
2026-10-18 00:06:47 +02:00
Niko1221 fa726b4c9c launch: shorter model-list wording (too big, SSD N GB) so it fits 110 columns; docs and previews for the classic-console look and several cards 2026-10-17 23:53:48 +02:00
Niko1221 1d03976529 monitor: the classic Windows console switches to Cascadia Mono when installed, else a solid-block look with palette colours; several cards and CPUs in the Overview (totals, a Cards strip, sockets); whole card names in view 5 2026-10-17 23:39:05 +02:00
Niko1221 941041768b sm_120: the hyper-connection down projection with one token a block row (bitwise)
Measured on the RTX PRO 5000 against the token pairs of the previous commit,
fixed verify rounds, same text: 2 tokens 9.44 -> 9.24 ms, 3 tokens 10.81 ->
10.56, 4 tokens 11.94 -> 11.90, 5 tokens 14.04 -> 13.70.  Each (row, token)
is still summed by one warp in the same order.  STRATA_GR_DOWN_TG=2 / 0 picks
pairs / no split as before.
2026-10-17 23:05:40 +02:00
Niko1221 44997124ff mmvq_il: the sm_120 2-4 column table only on 80+ SM cards; docs
The shared table was measured on an RTX 5070 (48 SMs) together with an RTX
3060, so a small 12.0 card keeps it; the RTX PRO 5000 table (110 SMs) now
applies to 12.0 cards with 80 or more SMs (il_cc answers 1201 for them).
The 5-8 column table stays for every 12.0 card.  docs/DETAILS.md: the
measured decode gain of the sm_120 defaults, docs/MMVQ_IL_TABLE.md: the
new tables and switches.
2026-10-17 22:49:13 +02:00
Niko1221 3b4c379b68 Merge commit '7bdc1ffb7' into f-split-int 2026-10-17 22:48:56 +02:00
Niko1221 2aeb246cfc Merge commit 'ae31e3cd3' into f-split-int 2026-10-17 22:41:42 +02:00
Niko1221 ae31e3cd36 Docker: smoke-test.sh (the checks run on real cards), executable bits 2026-10-17 22:41:09 +02:00
Niko1221 8d838f770f DOCKER.md: sizes and what was tested 2026-10-17 22:38:47 +02:00
Niko1221 7bdc1ffb72 launch: the card or cards chosen this time go into every launch's own config (--gpu, the Cards step, or the fit rule), so an installed model never keeps its old card and nothing is reinstalled 2026-10-17 22:34:19 +02:00
Niko1221 a00ed935f1 Docker: GPU/GPUS take effect on a volume that already has the model, tuning wording, build id of the source build 2026-10-17 22:15:53 +02:00
Niko1221 404713f234 Dockerfile (source): no NCCL in the picture reader, so the package check passes 2026-10-17 21:59:11 +02:00
Niko1221 4895b1cfa0 Docker: keep only gguf-py and the ggml marker of the baked llama.cpp; root Dockerfile PATH fix 2026-10-17 21:55:59 +02:00
Niko1221 039d2d4609 desktop: cards-e2e.mjs, the Graphics cards choice end to end through the host API 2026-10-17 21:52:31 +02:00
Niko1221 fa6367a98f Merge branch 'f-split-int' into f-split-fit 2026-10-17 21:51:30 +02:00
Niko1221 33d37ddff6 app: the Graphics cards choice on the model page (the recommended option pre-selected, neutral style), passed on install and load 2026-10-17 21:51:30 +02:00
Niko1221 80e6d91a63 Merge commit 'a6759787c' into f-split-int 2026-10-17 21:49:11 +02:00
Niko1221 ba2c2d5fb7 sm_120: four bitwise decode switches on by default on a 12.0 card
Measured on an RTX PRO 5000 Blackwell 72 GB (IQ3_S, every expert in VRAM),
fixed 4-token verify rounds, story / code, the same text as before:
  STRATA_SIDE_MIXER=2   13.67 -> 13.15 ms a round
  STRATA_ATTN_DAILY=1   -> 13.31
  STRATA_GR_DOWN_TG=2   -> 13.34
  STRATA_GR_UP75=1      -> 13.44
  all four              13.70 -> 12.23 ms (2-token rounds 10.37 -> 9.59)
Each is bitwise equal to the kernels it replaces (their parity tests; the
identity gate).  include/strata/core/sm120_series.hpp holds the rule: on for
compute capability 12.0 unless STRATA_SM120_PERF=0; each switch's own
variable still wins in both directions on any card.  The down projection's
token pairs run at every even launch there (Turing keeps 4 and 6 only).
STRATA_ATTN_DAILY / STRATA_ATTN_MERGE_V2 and STRATA_SIDE_MIXER are now
decided per device (a layer split can mix cards).  Other cards: unchanged.
2026-10-17 21:45:38 +02:00
Niko1221 75fabbd941 mmvq_il: 5-8 column interleaved kernels and the sm_120 tables (bitwise)
A verify window of 5+ rows fell from the interleaved small-batch kernels (1-4
columns) back to native_mmvq_multi: on an RTX PRO 5000 Blackwell the dense
projections of a round jumped +2.0 ms from T=4 to T=5 (the main Q5_K head
521 -> 1011 us).  native_mmvq_il_kernel now also runs 5-8 columns from the
cp = 8 interleaved copy (1-2 rows a warp), every value bitwise native_mmvq's
(mmvq_il_parity, T 1-8, every rows choice).

Tables for compute capability 12.0, read off mmvq_il_parity --bench
--emit-table on the RTX PRO 5000 (3% rule): kIlRows_120 for 2-4 columns (it
takes the interleaved kernels in more cells than the shared table, e.g. the
head at 2 columns 507 -> 367 us) and kIlRows58_120 for 5-8 columns (the head
at 5 / 8 columns 899 -> 491 / 1178 -> 667 us).  Other cards keep their
tables and native_mmvq at 5+ columns.

STRATA_MMVQ_IL_WIDE=0 turns the 5-8 column path off; STRATA_MMVQ_IL_120=0
gives a 12.0 card the shared 2-4 column table again.
2026-10-17 21:45:28 +02:00
Niko1221 390336f68d Docker: pinned llama.cpp baked into the image (no GitHub download on first start), file consistency tests 2026-10-17 21:43:37 +02:00
Niko1221 7e490ef63c Docker: launch gets SIGINT back so docker stop ends a download in 1 s; INSTALL.md points to DOCKER.md 2026-10-17 21:26:58 +02:00
Niko1221 da34c60ba7 Merge commit '91cccc1ae' into f-split-int 2026-10-17 21:21:13 +02:00
Niko1221 3641dafe98 Docker: DOCKER.md, MODEL_PATH in compose, device check for AMD and Intel 2026-10-17 21:15:13 +02:00
Niko1221 c7f803d0db Merge commit 'e77e5550d' into f-split-int 2026-10-17 21:06:44 +02:00
Niko1221 5f1bd58e30 Docker: images built from the engine component packages (cuda13, cuda12, HIP gfx120X/gfx110X, SYCL), launch-based entrypoint, compose examples, from-source Dockerfile 2026-10-17 21:00:56 +02:00
Niko1221 f533c7faa6 docs: which cards a model uses; Cards step column widths and previews 2026-10-17 20:35:07 +02:00
Niko1221 31abd50a60 launch: the installed config must carry the chosen cards (a forced split re-runs setup), unified-memory chips count when they are the only device; setup tests follow the fit rule 2026-10-17 20:28:16 +02:00
Niko1221 91cccc1ae1 Merge branch 'f-split-testfix' into f-split-amdtester
# Conflicts:
#	apps/desktop/scripts/stage-harness.mjs
2026-10-17 20:27:24 +02:00
Niko1221 e77e5550df stage-harness: fix a broken string literal in the import check; node --check / bash -n / py compile of every build script in the host tests 2026-10-17 20:26:24 +02:00
Niko1221 15339326e9 model list: gpu_options per model from the shared fit rule (the app reads the choice from here) 2026-10-17 20:16:48 +02:00
Niko1221 1d8d830afa setup: the multi-GPU default follows the fit rule (largest card + RAM first, the split recommended only when the model does not fit one card), goldens re-recorded, setup tests 2026-10-17 20:16:02 +02:00
Niko1221 c198c25e57 launch: the Cards step uses the shared gpu_fit rule (largest card + RAM recommended, the layer split offered with plain numbers, an integrated chip ignored) 2026-10-17 20:13:33 +02:00
Niko1221 ac38793caa gpu_fit: one rule for which graphics cards a model uses (largest card + RAM first, the split offered when it does not fit, an iGPU ignored), with tests 2026-10-17 20:10:22 +02:00
Niko1221 a6759787c3 Merge f-146-int (b7505b72a, 0.1.46 engine: Volta perf PRs + QSA_WARP sm<75 fix) into f-split-146
Conflict in src/prefill/prefill.cpp (step 2b): the split rotates mixed into Xr (APR, had_gu)
before 0.1.45's quantize_scatter (NVFP4 activations keep the gather); 0.1.46 adds the pf_wmma
FP16 gather branch. Both kept: wmma_l gathers FP16 rows (and rotates Xs / the swiglu output Hh
in fp16 when the layer is Hadamard-folded), otherwise the split's rotate then quantize_scatter
or the NVFP4 gather.
2026-10-17 20:09:14 +02:00
Niko1221 b2761df626 stage-harness.mjs: the missing-imports message had raw line breaks inside a string (SyntaxError, no Harness could be staged on any OS) 2026-10-17 19:53:48 +02:00
Niko1221 bfed34873a Merge branch 'f-split-testfix' into f-split-amdtester 2026-10-17 19:53:36 +02:00
Niko1221 b7505b72a4 Merge branch 'f-146-qsawarp' into f-146-int 2026-10-17 19:23:39 +02:00
Niko1221 1b856af242 Installer: Uninstall only uninstalls; the uninstaller offers Also remove my Strata data (off by default) and lists exactly what it deletes 2026-10-17 19:17:44 +02:00
Niko1221 051d0fc9c4 Models tab: Computer use buttons level with the context dropdown, clear of the separator 2026-10-17 19:15:08 +02:00
Niko1221 447b99ba38 Folder chooser fallback in the app: plain message and a field for the path 2026-10-17 19:12:09 +02:00
Niko1221 d85bb21c6b Linux: programs that are not Strata's own get the AppImage's original environment; folder chooser tries zenity, kdialog, portal and never fails silently 2026-10-17 19:11:09 +02:00
Niko1221 d7347cdde6 Start failures: plain, correct messages from the engine's exit code (missing DLL, bad DLL, crash, blocked, file missing); low memory only with evidence; no memory retry for non-memory failures 2026-10-17 19:07:42 +02:00
Niko1221 ad3b340d30 App never adopts an older CLI install: own settings file for Control and the installer, explicit install folder, tests 2026-10-17 19:02:17 +02:00
Niko1221 37f7f03cd2 Engine packages: bundle every imported non-system library, refuse a package a clean PC could not start, PE/ELF/Mach-O import scanner and tests 2026-10-17 18:59:45 +02:00
Niko1221 cbb249d3dd Windows HIP package: ship msvcp140_atomic_wait.dll (strata.exe imports it) and the other VC++ runtime DLLs next to the exes as well as in rocm/bin 2026-10-17 18:54:32 +02:00
Niko1221 f95fb3e074 Fix agent chat on Linux and Mac: keep the Harness's own dsh-* packages when pruning other-OS binaries, verify imports at staging, plain start-failure message 2026-10-17 18:54:11 +02:00
Niko1221 6aa502d938 strata update: a feed whose only HIP engine is for another AMD card family says so in one plain line (This build of Strata is for AMD RX 7000 cards ... Ask for the RX 9000 build) 2026-10-17 18:25:27 +02:00
Niko1221 67bd2a1609 hip_archs_present: an explicit kfd folder is always read (the Windows adapter lookup only replaces the default Linux one) 2026-10-17 18:21:43 +02:00
Niko1221 fe6fa9a274 Merge commit 'db5be2aa554cb248beb70c9595b9719254373772' into f-split-int 2026-10-17 18:01:54 +02:00
Niko1221 db5be2aa55 vision-e2e: load by the installed model name 2026-10-17 14:50:28 +02:00
Niko1221 50768496f5 vision-e2e: find the installed model by id 2026-10-17 14:48:34 +02:00
Niko1221 ab5adeb79f docs: pictures in strata launch 2026-10-17 14:41:45 +02:00
Niko1221 ac5f0d19a3 desktop: vision-e2e.mjs, the picture-reading end to end test through the installed app layout 2026-10-17 14:41:19 +02:00
Niko1221 9a22b78887 setup: the Mac control path says plainly that the Mac engine has no picture reader yet 2026-10-17 14:40:09 +02:00
Niko1221 7c5e846121 Merge branch 'f-split-int' into f-split-vision 2026-10-17 14:40:04 +02:00
Niko1221 417cbab4ff packaging: the engine package carries strata-vision when the build has it 2026-10-17 14:40:04 +02:00
Niko1221 788e0b731b Merge branch 'f-split-agent8' into f-split-vision 2026-10-17 14:38:52 +02:00
Niko1221 debe8ddabb launch: a missing picture reader file is fetched again instead of starting an engine that cannot read pictures 2026-10-17 14:31:03 +02:00
Niko1221 7ecfe4a6a6 A tester build for another AMD card family: one plain line (This build is for AMD RX 9000 cards ... Ask for the RX 7000 build) in the engine status and the start error 2026-10-17 14:16:44 +02:00
Niko1221 4db866ae00 Engine packages: the folders named in BUILD.json lib_dirs go on the library path with the package folder (the Windows HIP package keeps the ROCm DLLs in rocm/bin), for serve, the binary smoke test and tuning 2026-10-17 14:16:02 +02:00
Niko1221 8c676a7ccd Windows AMD: the engine is picked from the display adapters when no GPU tool answers (an AMD card, not an integrated Radeon), the app lists AMD cards on Windows and no longer refuses the hip backend there 2026-10-17 14:10:08 +02:00
Niko1221 8b3a96779e Merge f-145-pp-volta into f-146-volta (0.1.46): the community Volta/Pascal perf-PR group (35 commits)
Merge resolution as in f-145-perfprs-withvolta: iq_kernels.cu (IQ3_XXS / IQ3_S staged sign-mask tables over the EXPDEC grid staging), prefill/kernels.cu and prefill/prefill.cpp keep both sides; CMakeLists keeps both test blocks.
2026-10-17 14:00:21 +02:00
Niko1221 c61364fc89 qsa prompt attention: STRATA_QSA_WARP=1 only forces the Turing kernels on cards that have the Turing MMA (sm_75+)
The switch made every card answer as sm_75, so a Tesla P100 (sm_60) launched the m16n8k8 prompt-attention kernels, whose sm_60 code is a __trap(): the engine died on the first long prompt with "prefill copy_i32: unspecified launch failure". A card below sm_75 now ignores the switch and runs the kernel it runs without it (the same tokens).
2026-10-17 13:49:20 +02:00
Niko1221 e29112390f launch: turning pictures on for a model that is already installed says it downloads only the picture reader, not the whole model 2026-10-17 13:48:17 +02:00
Niko1221 8d65058c9f launch: Pictures on goes through setup's Control mode (setup keeps the choice when the engine component carries strata-vision); the screen warns only when the installed engine lacks the reader 2026-10-17 13:46:42 +02:00
Niko1221 e819ba0bd0 launch: the Folder screen says that model files already stored are moved into a new folder 2026-10-17 13:10:14 +02:00
Niko1221 0b055d8316 Merge commit 'ee0f32805' into f-split-int 2026-10-17 12:04:03 +02:00
Niko1221 ee0f328053 test: the bundle keeps one real python and launchers for the other names 2026-10-17 11:58:14 +02:00
Niko1221 d4abe4b745 strata launch on a Mac: a Terminal that carries strata_control/mac_fit.py installs the model through setup.py's Control mode instead of adopting prepared files 2026-10-17 11:55:03 +02:00
Niko1221 78fc321cd3 launch: the Pictures screen says plainly that this Terminal cannot set up the picture reader yet 2026-10-17 11:52:23 +02:00
Niko1221 254ee2b9cd Control: the NVIDIA driver version also from /proc/driver/nvidia/version (Fedora has no nvidia-smi without its cuda package); an integrated AMD GPU beside a usable NVIDIA card is not offered 2026-10-17 11:27:51 +02:00
Niko1221 a93264877c metal: the opt-in HIP/Turing entry points (fused_gr_persist_init, fused_gr_read_persist, f16_range_read, gemm_f16_range_read) answer 'nothing to do' instead of stopping the engine; e2e script waits for the bundled engine 2026-10-17 11:26:20 +02:00
Niko1221 e4b885e9fa repack-appimage.sh is executable 2026-10-17 11:19:53 +02:00
Niko1221 f2fe9a9097 metal: stubs for the 0.1.45 entry points Metal does not implement yet (stubs_0145.cpp) and the Gemm::bf16 / bf16_converts_x signatures, so the engine links from the 0.1.45 tree 2026-10-17 11:18:31 +02:00
Niko1221 d2a38c9e5c Linux tester AppImage: repack-appimage.sh puts the staged engine/Node/Python/Harness back byte for byte (linuxdeploy rewrites their RPATH, which broke the bundled engine's manifest check); control status says the bundled engine is being set up; WebKit DMA-BUF renderer off on Linux; e2e script takes --backend and finds the engine by its components path 2026-10-17 11:13:50 +02:00
Niko1221 47e2343b1b launch: Pictures on means the CPU picture reader on an AMD card, and a plain warning when pictures stay off 2026-10-17 11:10:47 +02:00
Niko1221 eeeeddd3f6 metal: the CUDA-name compat layer declares the seven calls the 0.1.45 sources added (compute capability, stream priority, cudaMemcpyPeer, DisablePeerAccess), so the Metal engine builds from the 0.1.45 tree again 2026-10-17 10:54:20 +02:00
Niko1221 63ac0838bc docs/MAC.md: the app and strata launch on a Mac 2026-10-17 10:47:04 +02:00
Niko1221 ccb33b7c7c Mac install plan: no card, KV or RAM-mode options; the context follows unified memory; a Mac too small for the model is refused in plain words 2026-10-17 10:45:17 +02:00
Niko1221 78914f55be Mac app: the model list, fit words and recommendation judge a Mac by unified memory (strata_control/mac_fit.py, one definition shared with metal/setup_mac.py); setup.py in Control mode installs a Mac model with no source build and no setup_mac.py and writes component:strata-engine with the Metal flags 2026-10-17 10:42:27 +02:00
Niko1221 6c4dbe3362 launch: Pascal/Volta cards are admitted when they are the only NVIDIA cards, as setup does (V100 showed 'no usable graphics card' and no Cards step) 2026-10-17 10:41:48 +02:00
Niko1221 8df9e0afd0 Linux app: one real python in the bundle (the other names become launchers), GStreamer bundled in the AppImage, a driver too old for every engine gets one plain line naming the version to install 2026-10-17 10:33:22 +02:00
Niko1221 395ed1ac54 bundled engine: CUDA 13 and CUDA 12 builds of one version install side by side, only the ones the NVIDIA driver can run; fix the duplicate 'ready' parameter that made native-control.mjs a syntax error 2026-10-17 10:30:37 +02:00
Niko1221 13ded2d662 Merge branch 'f-split-tester' into f-split-macapp 2026-10-17 10:22:38 +02:00
Niko1221 60c4d8de1e Merge branch 'f-split-apps' into f-split-macapp 2026-10-17 10:20:35 +02:00
Niko1221 e288308b46 Installer can carry the engine: the host installs a bundled engine package (manifest hashes checked, never over a newer one, no network) before the first model list 2026-10-17 10:14:19 +02:00
Niko1221 3618b48030 stage-harness: drop musl and other-CPU prebuilds on Linux (linuxdeploy stops on koffi's musl build) 2026-10-17 09:47:07 +02:00
Niko1221 59cee65da3 serve tests: the refused-prompt test runs in strict mode (fit_max_tokens off) so a huge max_tokens is refused, not shortened 2026-10-17 07:00:49 +02:00
Niko1221 9a58a4b73a Merge f-145-int (477ac13cb, the 0.1.45 release tree) into f-split-145 2026-10-17 06:33:36 +02:00
Niko1221 f202d08c5f Merge commit '872bcd197' into f-split-int 2026-10-17 06:16:06 +02:00
Niko1221 872bcd1974 launch: see an Intel Arc card (from the kernel's device list) and recommend a context that fits its memory 2026-10-17 05:52:15 +02:00
Niko1221 6ad5596b9d tests: the Intel Arc flags follow the kernel driver in the launch settings 2026-10-17 05:12:09 +02:00
Niko1221 982f43dee5 Regenerate launch previews after the model-list change 2026-10-17 05:11:26 +02:00
Niko1221 87fbbdaaa9 Merge branch 'f-split-agent8' into f-split-int 2026-10-17 05:06:46 +02:00
Niko1221 e3bf0ad04d Time-skip: final 'Both versions are ready' line, no stale warning after B finished 2026-10-17 05:02:30 +02:00
Niko1221 b8c1602715 launch: Intel Arc recipe for prepared files (i915: --ple-io ram; xe: --stream-experts; VRAM reserve) 2026-10-17 04:35:15 +02:00
Niko1221 08297223e4 launch: on a Mac, use prepared model files with the Metal defaults (the Terminal has no Mac model installer) 2026-10-17 04:34:33 +02:00
Niko1221 64fff9e149 launch: model list shows the app's RAM+VRAM figure and the SSD part; strata monitor gets the Connect view (7); Mac env names 2026-10-17 04:31:34 +02:00
Niko1221 b799ef5f6d launch tests: top-folder install case 2026-10-17 04:06:23 +02:00
Niko1221 8cc02778e4 launch: firewall line, overview GPU name and legacy width cap, install.sh accepts an archive with a top folder, tool-call check budget, plain wording for adopted files 2026-10-17 04:05:47 +02:00
Niko1221 87efc9ace5 launch tests: macOS fixes (platform names, python 3.9 tar, no setsid/timeout/cp -l), feed platform macos-arm64, p2006 engine path 2026-10-17 04:01:23 +02:00
Niko1221 c1533d6d5f update: an engine without --version that lists its options passes the test start (with a plain note) 2026-10-17 04:00:17 +02:00
Niko1221 a693660a15 launch: fix the download question from the start screen 2026-10-17 03:41:25 +02:00
Niko1221 436fabc466 launch: ask before downloading a model that is not here; tuning table labels 2026-10-17 03:39:30 +02:00
Niko1221 7c2f878051 launch: a Mac's setup gets only the options it knows 2026-10-17 03:36:59 +02:00
Niko1221 a2bab5c8c2 launch: no choices and no terminal starts nothing 2026-10-17 03:36:37 +02:00
Niko1221 dc1aa5030a launch: docs (TERMINAL_LAUNCH.md), installer tests, plain GGUF issue words 2026-10-17 03:36:03 +02:00
Niko1221 463d6780a9 launch: write the run settings from prepared files when the installer cannot set up the card (Intel Arc) 2026-10-17 03:21:39 +02:00
Niko1221 9330989efa launch: say why the engine could not be installed 2026-10-17 03:16:46 +02:00
Niko1221 69f9cc7b9e launch: environment settings also apply to the engine's test start 2026-10-17 03:12:10 +02:00
Niko1221 b3ff0c2ebf launch: environment names keep their case in --set (IGC_..., ZES_...) 2026-10-17 03:07:25 +02:00
Niko1221 4fbd0b3fcb connect check: responses answer text excludes reasoning items 2026-10-17 03:02:20 +02:00
Niko1221 d247802094 launch: install.ps1 child scope and stderr note, tuning result line, Windows watch hint 2026-10-17 02:57:51 +02:00
Niko1221 1fe683e951 Picture input for a server on this network only when its own model list says it reads pictures (a server without a vision encoder refused whole requests) 2026-10-17 02:53:59 +02:00
Niko1221 eaa3a58172 launch: monitor tests, quit hint, check result note 2026-10-17 02:50:03 +02:00
Niko1221 7ce4b6df37 Time-skip step limit 15 minutes 2026-10-17 02:41:28 +02:00
Niko1221 f548cfd843 Time-skip: a step that runs longer than 20 minutes is stopped and left as it is with a plain note; one shared step runner for A and B 2026-10-17 02:41:20 +02:00
Niko1221 6a29939806 launch: connect --check, tester stage scripts (phaseA-tests/split/launch-tests are outside the repo) 2026-10-17 02:33:31 +02:00
Niko1221 ec06637d48 launch: installer placeholder fix, quieter plain run, flags next to --preset do not rewrite it 2026-10-17 02:26:28 +02:00
Niko1221 4e6a1b46be launch: text previews of every screen, advanced table widths 2026-10-17 02:15:12 +02:00
Niko1221 efa38a99ca monitor: the approved Overview as view 1 (state bar, 8 tiles with sparklines, bars, recent requests) from /metrics; b opens the browser 2026-10-17 02:14:32 +02:00
Niko1221 4e788fbf3e connect: the 8 tested harness configs (limits in plain words, context-aware), Connect view shows them 2026-10-17 02:11:28 +02:00
Niko1221 da4a3a4765 Merge commit 'e0a932a32' into f-split-launch 2026-10-17 02:07:56 +02:00
Niko1221 4c9423e194 launch: one-line installers (install.sh, install.ps1), local test feed builder, package_terminal installer step 2026-10-17 02:07:47 +02:00
Niko1221 4fac421d14 Time-skip sample copy only when the host cannot be reached at all; packaged test follows the Read pictures install 2026-10-17 01:55:37 +02:00
Niko1221 7b1405e104 Time-skip: view-in-browser steps never hang or fail a version (no-window rule, skip with a plain note); steps carry the unattended note 2026-10-17 01:48:50 +02:00
Niko1221 97e4dcaa1f Merge branch 'f-split-ui6' into f-split-int 2026-10-17 01:47:30 +02:00
Niko1221 c473d97109 Merge branch 'f-split-agent8' into f-split-int 2026-10-17 01:47:28 +02:00
Niko1221 4a786e03d6 Time-skip: a step that does not finish is tried once more in a fresh session; B says why it stopped 2026-10-17 01:42:17 +02:00
Niko1221 e0a932a32d Merge branch 'f-split-harness' into f-split-int 2026-10-17 01:06:34 +02:00
Niko1221 f31b6efd68 serve: shorten max_tokens to the room left instead of answering 400 (all three dialects)
Claude Code asks for max_tokens 32000 on every request and the Harness for 16384; on a 32K context the
prompt fits but prompt + max_tokens does not, and the server answered 400 "request exceeds the context
window ... requests are never truncated" before the first token.

Service.prepare() (the one place chat/completions, messages and responses all pass through) now clamps
max_tokens to ctx - CTX_SLACK - prompt when the prompt fits, logs one line, and the answer_room thinking
budget (3/4 of max_tokens) is taken from the clamped value. A prompt that alone does not fit is still the
plain 400 ("no room to answer"). --no-fit-max-tokens / "fit_max_tokens": false brings back the strict
llama.cpp 400; --fit-max-tokens stays accepted.

Tests: serve/test_server.py MaxTokens (shortened by default in the three dialects, exact fit and one over,
prompt-too-long 400, thinking budget follows, strict mode), serve/test_context_overflow.py (default
service), serve/test_monitor.py (error record uses the strict mode).
2026-10-17 00:55:18 +02:00
Niko1221 f11768e873 Time-skip: each step runs in its own agent session (the folder carries the work), so a small window never fills up across a long plan 2026-10-17 00:41:02 +02:00
Niko1221 af34f0a4b7 Reply room 30 percent of the window and an eighth headroom: a 32K window no longer compacts every few steps 2026-10-17 00:04:36 +02:00
Niko1221 64034f1674 launch: terminal archive carries the installer files (shared with the app's Control staging), legacy-console glyph and box fallback, install from a bundle runs in control mode 2026-10-16 23:43:01 +02:00
Niko1221 8251089c1f launch: pilot tests for every screen, run screen state fix, no auto focus 2026-10-16 23:40:55 +02:00
Niko1221 af4a7e5638 Merge branch 'f-split-fp4' into f-split-int 2026-10-16 23:38:52 +02:00
Niko1221 5fbd539628 launch: TUI screens, runner events, monitor Connect view, harness check, tests for flags and rules 2026-10-16 23:32:37 +02:00
Niko1221 e61e90e555 launch: settings, catalog (setup.py tables via strata_mcp, fitOn port), presets store, access, runner, flags, connect, harness drafts 2026-10-16 23:25:34 +02:00
Niko1221 17ba2d71ad A model server on this network keeps half its window for the reply (long Time-skip steps overflowed the window); Time-skip says why A failed 2026-10-16 23:18:34 +02:00
Niko1221 76707b230b Round 8: neutral Open result button, pictures uploaded to /api/chat/upload and sent as imageIds, picture-reading files note on the Models tab 2026-10-16 23:00:11 +02:00
Niko1221 0a6eced5cc Merge branch 'f-split-agent8' into f-split-ui6 2026-10-16 22:50:27 +02:00
Niko1221 490fda2657 Round 8: project folder chip above the message box, Time-skip as two agents (plan, lever with its why, A and B progress, Keep A / Keep B), working view reachable while a run has no files yet 2026-10-16 22:50:05 +02:00
Niko1221 30a367e5f4 Image limits in the validation test follow the new 4 pictures / 6 MB 2026-10-16 22:49:22 +02:00
Niko1221 c3e9480592 catalog: one plain line each for NVFP4 (best quality of the NVFP4 versions) and NVFP4-QAD (NVIDIA's FP4, trained for FP4, slightly faster on short prompts) 2026-10-16 22:44:58 +02:00
Niko1221 d615708a7b Time-skip v2: plan, lever, agent A to the end, agent B forked at the lever with an improvement plan, A vs B, keep one 2026-10-16 22:37:12 +02:00
Niko1221 4d7fede4ed Merge branch 'f-split-agent8' into f-split-ui6 2026-10-16 22:36:21 +02:00
Niko1221 9e2d698c9e Round 8: Models navbar family dropdown, deliverable cards with Open menu, Edited summary, grouped activity lines, text in order, drop zone, image generation hidden 2026-10-16 22:35:54 +02:00
Niko1221 7af6b6c152 Vision end to end: vision files are downloaded when asked, pictures through the agent and the plain path, uploads by multipart/data address/raw bytes, dropped files named in the next message 2026-10-16 22:32:11 +02:00
Niko1221 917d2fc345 Deleting a model: a file that is already gone counts as removed; the host verifies the files are really gone 2026-10-16 22:26:44 +02:00
Niko1221 0ac98f80ee Merge branch 'f-split-agent8' into f-split-ui6 2026-10-16 22:26:01 +02:00
Niko1221 8509870849 Round 8: compact segmented Computer use, version chevron and panel gap, download details only when useful, immediate download progress, Models screens no longer follow every monitor tick 2026-10-16 22:26:01 +02:00
Niko1221 aa936f59f0 Deliverable events, grouped activity lines, file preview/raw/attach routes 2026-10-16 22:25:51 +02:00
Niko1221 ef3902477e Per-chat project folder (agent cwd and sandbox), opening files by path in the browser, app or folder 2026-10-16 22:23:38 +02:00
Niko1221 ddfed500a9 A turn always ends: watchdog for a silent model or a tool that never returns; launches return at once 2026-10-16 22:21:57 +02:00
Niko1221 ade3e6adb5 nvfp4_local_pack ple: accept a folder with only the shards that hold the table 2026-10-16 22:02:03 +02:00
Niko1221 4736639a7e merge 5e7a965ac into f-split-int 2026-10-16 21:57:35 +02:00
Niko1221 dbbe79aae8 merge bdb04dd8d into f-split-int 2026-10-16 21:57:34 +02:00
Niko1221 bdb04dd8d6 batch-mtp on automatically for one big card (24 GB+, several slots, MTP on); off on small cards and layer splits; Expert mode MTP per request overrides 2026-10-16 21:57:18 +02:00
Niko1221 48b87d42e1 docs: NVIDIA FP4-Local in NVFP4.md 2026-10-16 21:10:27 +02:00
Niko1221 e7f7dad3e0 FP4-Local engine floor 0.1.45 (the split release's engine has the NVFP4 n-gram table) 2026-10-16 21:04:27 +02:00
Niko1221 0da73d9159 FP4-Local prepare: separate step marks (the pack's own files were overwritten), scale tensors in the checkpoint-direct pack; docs section 2026-10-16 20:06:19 +02:00
Niko1221 5e7a965ac1 Queued-message test: ask Stop again when it answers still stopping 2026-10-16 20:06:04 +02:00
Niko1221 19d16c7dbe ui-flows: Save logs is inside the View logs pop-up now 2026-10-16 19:54:56 +02:00
Niko1221 d465e37bc1 setup: NVFP4-QAD, NVIDIA's FP4-Local checkpoint downloaded pinned and prepared on this PC (nvfp4_local_pack prepare), with its own draft layer 2026-10-16 19:40:17 +02:00
Niko1221 c163678f7d merge f-split-agent6 0b871e2d7 (compaction, /research, response settings, browser and skills checks, time-skip file order, step summaries) into f-split-int 2026-10-16 19:38:02 +02:00
Niko1221 0b871e2d79 Time-skip: a big file that did not change is not listed as changed 2026-10-16 19:34:49 +02:00
Niko1221 4cca90e757 nvfp4_local_pack pack: the experts straight from the checkpoint through iq_pack (no 74 GB GGUF in between) 2026-10-16 19:32:05 +02:00
Niko1221 87d70909a8 Time-skip: the made page first in the compare list, housekeeping files (browser profile, caches) last and unread, never the whole result 2026-10-16 19:29:57 +02:00
Niko1221 7f9e719c8a nvfp4_local_pack: slim (the dense-only GGUF), convert --dense-only 2026-10-16 19:27:08 +02:00
Niko1221 f37abb1365 Tests follow the new reply room; the queued-message test waits for the goal to be running 2026-10-16 19:25:32 +02:00
Niko1221 2b54cf4fbd Model servers on this network get the thinking vocabulary of a local model (the Reasoning menu did nothing for them); effort and sampling drivers 2026-10-16 19:23:03 +02:00
Niko1221 b6b546ae13 Response settings reach agent turns (thinking budget, Expert sampling) through a local forwarding endpoint; step summaries from what the step changed; plan title heading 2026-10-16 19:09:54 +02:00
Niko1221 d42319b6c9 ple_reader_test: NVFP4 shares the I8 type with FP8; the metadata picks it 2026-10-16 19:05:11 +02:00
Niko1221 c937aeb1df /research gathers first (notes hidden), nudges once when no page was opened, then writes the answer in the fixed shape; /research in the slash menu 2026-10-16 19:04:25 +02:00
Niko1221 9c6255cfce NVIDIA Qwen3.8-Flash-Next-FP4-Local: converter (CSF scale decode, MXFP8 to Q8_0/BF16, MTP, NVFP4 PLE table) and the NVFP4 PLE row format
tools/nvfp4_local_pack.py convert|ple|mtp|verify reads NVIDIA's ModelOpt checkpoint and writes what the existing NVFP4 path takes:
the CSF block scales are decoded to plain E4M3 (experts then repack bit for bit), MXFP8 weights are dequantized exactly and take the
Q8_0/BF16 policy of nvfp4_convert, the n-gram table becomes a one-tensor GGUF of 90-byte rows, the MTP head the folder mtp_pack reads.
ngram.cpp reads that table as a new PLE format (I8 bytes, strata.ple.format = nvfp4x16).
2026-10-16 19:02:17 +02:00
Niko1221 998e9df959 /research: web search and page reading with a structured answer and sources; http driver commands for plan, mode switch and long chats 2026-10-16 18:52:45 +02:00
Niko1221 c6a86ce168 Local models compact automatically: reply room is half the window and compaction headroom fits the window 2026-10-16 18:52:45 +02:00
Niko1221 743a6b3c61 merge f-split-ui5 e9a2ca44a (round-7 UI, time-skip fixes, mode switch mid-goal, /plan, queued messages, uninstaller icon; includes agent6 eecb29593) into f-split-int 2026-10-16 18:49:39 +02:00
Niko1221 e9a2ca44a7 UI round 7: plan card, Plan sidebar, goal step and queued messages built on agent6's backend contract 2026-10-16 18:49:13 +02:00
Niko1221 89d0965408 Merge branch 'f-split-agent6' into f-split-ui5 2026-10-16 18:43:25 +02:00
Niko1221 8da1d3f8c1 Installer: the uninstaller uses the Strata icon, and the shortcuts name it explicitly 2026-10-16 18:43:25 +02:00
Niko1221 eecb29593c Plan mode (/plan) with approve/edit/reject/start-as-goal routes, goal steps with per-step events, queued messages during a goal 2026-10-16 18:42:14 +02:00
Niko1221 83c8f0b120 UI round 7: /plan card, Plan sidebar tab with step states, queued messages with cancel, floating round arrow, plain Privacy without a box 2026-10-16 18:37:30 +02:00
Niko1221 c751c23965 Merge branch 'f-split-agent6' into f-split-ui5
# Conflicts:
#	apps/host/src/components/TimeSkipPanel.tsx
2026-10-16 18:17:44 +02:00
Niko1221 2e1fcd15d9 Time-skip: run the page's own script in the A/B preview, working view in the sidebar, retry the file list; mode change applies to the next tool call mid-run 2026-10-16 18:16:56 +02:00
Niko1221 e849aef2ae UI round 7: live chat numbers with tween, fixed status-line columns, Subagents counts on title row, plain Privacy text, round latest arrow, time-skip never empty 2026-10-16 17:59:07 +02:00
Niko1221 50f973c9f9 merge f-split-agent5 60bfe0693 (agent-children stability: agent-bridge race, request-flow test wait) into f-split-int 2026-10-16 16:06:10 +02:00
Niko1221 60bfe0693a stability: helper rows always show working before completed (the held row is sent first), agent-children tests order frames by arrival not by the clock and use generous bounds, request-flow waits for a slow host start 2026-10-16 16:05:42 +02:00
Niko1221 9550318e34 merge f-split-ui4 2812c939f (Use your own GGUF: the refusal reaches the page, a second preparation no longer fails) into f-split-int 2026-10-16 15:20:24 +02:00
Niko1221 2812c939fe local-gguf-e2e.mjs: the Use your own GGUF flow through the installed layout on a box (preview, refusal, prepare, load, answer, unload) 2026-10-16 15:19:41 +02:00
Niko1221 87b90a7882 Preparing an adopted GGUF folder a second time no longer fails on the data folder
An adopted model reports the parent of the person's own GGUF folder as its data folder; the next preparation sent that as the folder for the packs and Control refused it ('a new data folder must be named Strata'). A preparation from a GGUF folder now uses Strata's own download folder. Found by the real-box check on P2003; test fails without the fix.
2026-10-16 15:17:11 +02:00
Niko1221 fa25086125 Use your own GGUF: an unsupported or incomplete GGUF folder is shown in words, not as a generic failure
Control answers a refused folder with exit 5 and the plan (ok:false, issues); the host threw that away and the page said only 'The files could not be checked'. The plan with its reasons now reaches the page, which says 'Strata can't run these files yet' and keeps the checker's codes under Details. Found by the real-box check (a Gemma vocab GGUF on P2003).
2026-10-16 14:53:25 +02:00
Niko1221 7c4b3d743e merge f-split-ui4 a7cd2b38e into f-split-int 2026-10-16 14:33:50 +02:00
Niko1221 a7cd2b38e6 Tune strings that already live in the Tune catalog are not duplicated in the main one 2026-10-16 14:33:24 +02:00
Niko1221 9cede7db08 ui-flows: Models one-view check, outcome card and an empty task navbar 2026-10-16 14:30:31 +02:00
Niko1221 35c72bcd10 Models tab in one view with the family switch, one result card for every tune outcome, the tuned values in Expert mode, glow instead of a spinning ring
- Models: the family switch (Qwen3.8-Flash-Next, Qwen3.6-35B-A3B) is back in the navbar without the word Model; version, context and computer use, the tuning line and Load/Tune sit in one card beside This computer, so the tab fits the first view; image generation moves into Expert mode
- Tune: one result card for faster, already the fastest, couldn't measure (busy PC, with plain copy and Try again), failed and stopped, driven by the host's code, reason_code and values; the same Tune dialog frame for all of them
- Tune running: the step icon glows and a small dot beeps instead of a ring spinning around a square tile; reduced motion respected
- Expert mode: Context memory and Low-RAM mode show the active tuning's values, the engine settings list what the tuning sets, and loading does not re-prepare the model because of them
- the tuning line (Using your tuning / Switch off) is a calm line with an icon
- strings in 8 languages; tests in tune-outcome.test.mjs
2026-10-16 14:30:31 +02:00
Niko1221 99cc9829a0 Other local models: the back link says Back; the running version's group can be collapsed 2026-10-16 14:30:29 +02:00
Niko1221 837f992bd9 Owner round 6: short model names, one logs button, plain Monitor sentence, Privacy notes, Tasks cleanup
- Settings > Storage: downloaded optimized models show family + quantization with the full name as a tooltip, size aligned, Delete at the end
- Engine > Diagnostics: one View logs button; Save logs lives in the logs view
- About > Privacy: six short lines across the box instead of one long paragraph
- Monitor: the request summary is a plain sentence ('In 18 requests your average output was 47.4 tok/s and prompt reading 1,332 tok/s, reusing 91K tokens.'); the screenshot leaves out Model state and Conversation cache
- Tasks: Bypass permissions replaces 'Allowed to make changes' and is the default for a new task; an opened task has an empty left navbar and no Model section, its name in the page, and a readable Markdown last answer; no deleted-tasks list, restore or suggestions; + icon opens the editor modal (Draft it with AI inside it); removing says 'Task removed'
- strings in 8 languages; tests in ui4-fixes.test.mjs
2026-10-16 14:30:29 +02:00
Niko1221 19ea30f6ab merge f-split-agent4 4b5cc08d1 (skills end to end, New skill button, agent skills folder, goal-status race fix) into f-split-int 2026-10-16 14:26:57 +02:00
Niko1221 4b5cc08d1a a goal-step status is never swallowed by the 300 ms gap (fixes the flaky /goal test); skills strings re-added to the merged catalog 2026-10-16 14:24:21 +02:00
Niko1221 ca34357b08 skills: plain card words for loading a skill (8 languages), Skills page render script 2026-10-16 14:21:33 +02:00
Niko1221 4818b1f4a1 Skills: write one in the Skills page (New skill), saved as a folder in the app's data; the whole list is written out for the agent, which scans it (customSkillDirs) and watches it, so a saved skill reaches the next message without a restart; tests 2026-10-16 14:21:32 +02:00
Niko1221 0c165b48bf merge f-split-s5 dab63e9c3 (tuning presets on Models, effective values, outcome codes, apply_saved_tuning) into f-split-int
native-control.mjs: this tree's publicState/localPreview kept, s5's tuneEffective/tuneOff added to the exported API. NativeModels.tsx: the loaded-model Unload/Ready actions kept, s5's NativeTuneBadge added above them. NativeTune.tsx: ui2's result view kept; s5's busy_pc sentence and the steady-aware answer notice added.
2026-10-16 14:19:31 +02:00
Niko1221 dab63e9c38 tuning feedback: structured outcome (faster / already_fastest / busy_pc / failed / stopped + values), tuned values readable (presets effective, settings carry them), which tuning is active and off/on, applied at every start (restart and unload/load tests) 2026-10-16 14:16:07 +02:00
Niko1221 a79e653036 merge f-split-agent3 df52a2854 (API connection output limit) into f-split-int 2026-10-16 14:12:08 +02:00
Niko1221 f9bfe8bc12 merge f-split-agent3 c99896ae6 (status, goal and time-skip events and backend, time-skip keepalive and stop, atomic.py) into f-split-int
Conflicts: locales (agent3's 52 keys renumbered s2937-s2988 in all 8 files), timeskip.mjs (ui3's image() kept, agent3's stop()/whenEnded kept, imports unioned), store.ts (ui3's comparison-open rule plus agent3's markTurn), Chat.tsx (ui3's working card and chat card kept, agent3's timeSkipTab dropped), time-skip.ts (duplicate activity field removed).
2026-10-16 14:12:08 +02:00
Niko1221 df52a28548 API models: answer room is the connection's own maximum output when the provider lists one, else a generous 32K bounded by the window minus the prompt (was a quarter of the window, at most 8,192, which cut thinking models off with no answer) 2026-10-16 14:09:37 +02:00
Niko1221 334dde73a2 merge f-split-ui3 088f8685a (live status line, reasoning panel, goal bar, tool rows, time-skip comparison) into f-split-int 2026-10-16 14:09:13 +02:00
Niko1221 c99896ae66 the real-browser test is opt-in (STRATA_TEST_BROWSER=1); HTTP driver goal bar command 2026-10-16 14:08:27 +02:00
Niko1221 04cca3bca8 status sentences carry their catalog template and values and exist in 8 languages; goal tools have plain words; per-request thinking count 2026-10-16 14:05:21 +02:00
Niko1221 20b377d30d Time-skip and tune job records are replaced with a wait for a reader (Windows 'Access is denied' when the host reads the file at that moment: it ended jobs at random) 2026-10-16 13:29:12 +02:00
Niko1221 088f8685a7 Time-skip in chat: working view with Stop, comparison sidebar only when there is an update, A versus B that adapts to the kind of file
- while a real run works, an animated A/B card shows what the assistant is doing (host activity), elapsed time and Stop (TimeSkipWorking); the sidebar no longer opens at the start and opens only when the finished run changed files; a run that changed nothing says so in the chat and has no tab
- the finished A versus B (TimeSkipCompare) keeps one window size whatever was made: text and Markdown rendered with changed words marked; HTML as two sandboxed previews (no permissions, no scripts, no outside requests) with a View code toggle; pictures side by side, click to enlarge; code and other files as a folded line diff; several files through a compact file switcher
- host: changed pictures are listed as pictures and served from the run's own folders by GET /api/timeskip/file (images only, inside the run, size-capped)
- strings in 8 languages; tests for the file kinds, word and line diffs, the HTML policy, the activity words and the picture route
2026-10-16 13:20:20 +02:00
Niko1221 3e6c354504 status test no longer depends on timing; resume following a Time-skip after a reload keeps the Stop button 2026-10-16 13:19:19 +02:00
Niko1221 b085d54b94 status tests (plain chat, real Harness file write), HTTP driver for real-model checks 2026-10-16 13:16:59 +02:00
Niko1221 ee8b23bcc7 Time-skip: streamed revision answer with a beat (a slow turn no longer ends in 'could not be reached'), Stop ends the job tree and deletes the copies, activity in the status route and SSE, sidebar only when there is an update, Stop button while it runs 2026-10-16 13:15:12 +02:00
Niko1221 7c4092dabe Chat feels alive: live status line, one-line reasoning, goal bar
- status line under the newest message: pulse, a rotating playful word only while it thinks, the real action once a tool runs, elapsed time and tokens; only the newest status frame is ever shown and it is cleared on idle, done, error and stream end (src/lib/live-status.ts)
- reasoning is one quiet line (what it is thinking about, then 'Thought for 38s'); opened it follows the newest line unless the person scrolls up
- goal bar above the input: small, icon-only pause/resume, edit, remove, from the goal frame, the SSE goal event and /api/goal (src/lib/goal.ts); resume sends goalAction:'resume' with no user bubble
- all strings in 8 languages; tests in tests/live-chat.test.mjs
2026-10-16 13:14:01 +02:00
Niko1221 965f22e3db live status frames for agent, goal and plain chat turns (thinking tokens, composing a file call, running tool, waiting for approval, helpers, goal step), goal state frames and goal bar backend (state, pause, edit, clear via the Goal Remotes; resume through the chat stream) 2026-10-16 13:11:40 +02:00
Niko1221 44a12d78be Tool rows in chat: compact lines, no dividers 2026-10-16 13:05:20 +02:00
Niko1221 aaf37247da integration after ui2: a second-try failure is shown once (details keep the first stop), error Details box is height-capped so the small tuning pop-up never scrolls, ui-flows expects the real tuner's Tune model button 2026-10-16 11:55:14 +02:00
Niko1221 c5755ed837 merge f-split-ui2 baeb4533b (approved UI designs, context fix, workspace default with rename, autotune progress) into f-split-int
Locales: both sides added keys from s2676, so ui2's 88 keys are renumbered s2849-s2936 in all 8 files. events.mjs: ui2's verbsFor target plus agent2's ask card. index.mjs: the node:path import takes both sides' names (relative, sep). The workspace default is ui2's (Documents/Strata/Workspace).
2026-10-16 11:27:06 +02:00
Niko1221 baeb4533be Approval cards in plain words, Tasks open view, This computer card, version list, and the Tune steps with a live speed
- approval cards: one plain question per tool call (server/agent/ask.mjs maps commands and files to sentences; src/lib/ask.ts holds the translated templates), a compact calm card, the exact command under Details
- Tasks: the opened task shows schedule, what it may do, last result, instructions and last answer; back link before the title like the chat's navbar; pause/resume and delete icons, Edit, Run now; pause and play icons added to the sprite
- Models: 'This computer' shows graphics memory and RAM bars with what the chosen version takes, and one plain line about the SSD (src/lib/native-fit.ts, from the catalog's memory_gb and the hardware report; the n-gram table is not counted)
- Models: one-line version list: Recommended (the fixed four), Swift, GSQ-RCO, NVFP4, Gyro, Unsloth groups, Use your own GGUF; RAM+VRAM in bold, download size muted, 'Uses SSD N GB' only where it applies
- Tune: five steps with their own animated icons and states, a live speed readout and sparkline, a result card with the gain and what it was saved as; reduced motion respected; the tuner now reports each measurement's speed
- catalog: memory_gb and mem_total per version (tools/strata_mcp.py)
- tests: ask, native-fit, tune-phase, autotune speeds, memory figures; headless checks (ui-flows ui2/ui2d, scripts/approval-flow.mjs), light and dark
2026-10-16 11:24:09 +02:00
Niko1221 ea2d565e60 merge f-split-agent2 c7b65c7f7 (/loop, /goal, /time-skip, keyless search, system-browser tool, Settings Tools, Plan refuses connected tools) into f-split-int 2026-10-16 11:08:59 +02:00
Niko1221 c7b65c7f7b tools flow script restores the settings it changes 2026-10-16 11:05:57 +02:00
Niko1221 d146b0da29 plain browser card sentences (waiting, scripts, tabs, back, close), no empty braces on cards; tools flow script for the real model 2026-10-16 11:01:55 +02:00
Niko1221 9f24934663 tuner: a run that could not measure any candidate is inconclusive, not default confirmed 2026-10-16 10:58:33 +02:00
Niko1221 70f8268dab Time-skip refuses the app's own folder and a folder too big to copy, in plain words in all 8 languages 2026-10-16 10:52:01 +02:00
Niko1221 0848864a03 cards for the browser and the person's own tools say what happened in plain words (8 languages) 2026-10-16 10:48:49 +02:00
Niko1221 8075af7c2a calmer Time-skip wording (one note, no repeated reassurance), plain Manual reason for the person's command tools, agent README for Settings Tools 2026-10-16 10:47:46 +02:00
Niko1221 533f6b713f browser tool (Playwright MCP on the person's Chrome or Edge, separate empty profile; Manual asks before opening, clicking, typing, not before looking; Plan refuses), staged into the installer; Tools screen strings in all 8 languages; Time-skip result view without the sample success check; /loop words translated 2026-10-16 10:45:44 +02:00
Niko1221 c20c4aed11 tuner: say so when the default itself is not repeatable (RTX 3060 resident mode, A750); exact knobs are still auto-picked there 2026-10-16 10:36:37 +02:00
Niko1221 24e0157b53 Settings Tools behind the screen: tool groups on/off reach the agent (restart on change), web search with no key (DuckDuckGo, polite, cached) or the person's own Exa/Perplexity/DeepSeek key in the environment only, the person's own tools (instructions, commands, tool servers) through the app's tool server, Plan refuses connected tools, keyless API connections work in the agent; browser finder 2026-10-16 10:35:36 +02:00
Niko1221 f662759046 Workspace rename strings in all 8 languages (after rebase onto df13760cf) 2026-10-16 10:23:59 +02:00
Niko1221 0d4e0dde8c Settings Tools tab (screen only, mock-up state), Time-skip trial runs on the chat's API model too 2026-10-16 10:22:09 +02:00
Niko1221 db9c322bce slash commands: /goal and /loop in the menu with plain descriptions, /loop repeats a message while the chat is open, /time-skip runs a real job on a copy of the chat folder (assistant revises, files listed, update kept or original restored on request, rollback copy); apply of a reviewed attempt in Strata Control 2026-10-16 10:22:09 +02:00
Niko1221 a655bf22b2 Context from cached prompts for agent chats, and a default workspace that is never the app folder
- agent context: the Harness reports a cached prompt as cacheReadTokens, apart from inputTokens; the context now counts input + cache read + cache write + reply of the last request (a long agent chat showed 0-3%; checked against a real Swift IQ2_XS 64K run: 8,152 tokens, matching the engine's own 8,042-token prompt + 110 reply)
- first workspace: Documents\Strata\Workspace (display name Strata) unless STRATA_WORKSPACE names one, never the app folder; a saved first workspace that is the app folder moves there and its chats follow through an alias
- workspaces can be renamed (display name only, path kept): PATCH /api/workspaces/:id, pencil in the sidebar, 8 languages
- tests: workspaces.test.mjs, cache-aware context in functional-fixes.test.mjs
2026-10-16 10:22:08 +02:00
Niko1221 df13760cf4 files made by a run as administrator no longer lock the user out: explicit user-SID entry and owner on create (private_mkdir, restrict_to_current_user), process default owner/DACL set to the user, self-repair of Strata's own per-user folders (APPDATA, LOCALAPPDATA, ~/.strata) on read, at host start and by 'strata repair-access', plain message when it cannot; tests; tune_session loopback allowed in the no-acquisition audit 2026-10-16 10:10:43 +02:00
Niko1221 e1dca4bff5 merge f-split-gyro b15b209a0 (Step B: Gyro trellis types through MMQ in the pinned ggml via an overlay) into f-split-int 2026-10-16 07:08:30 +02:00
Niko1221 95e4f27f17 tuner knobs from TUNING-SPEC draft 2: kv-resident 65536 at long contexts and the prefill chunk as exact candidates (prompt metric), pin-by-need/host-core/kv-grow last, no workers two below, single best knob 2026-10-16 06:42:20 +02:00
Niko1221 1ea66f60af owner decisions: goal, skill, workflow and fork tools back in the agent (short plain prompt kept); agent and chat default reasoning effort Medium, with a test per level (Off/Low/Medium/High/unset) on the chat_template_kwargs the server receives; Tune dialog switch reads 'Allow settings that change answers very slightly (same quality)' in all 8 languages, off by default, with a test 2026-10-16 06:30:44 +02:00
Niko1221 394de92915 Functional-test fixes: agent tok/s and context, plain tool rows, Thinking Off proof, profile used, no download wording for installed models, plain chat, smaller agent prompt
- agent path: live stats frames and a final tok/s from the stream timing; the context is the last request (one source)
- tool rows say what happened in words (Wrote a file, Ran a command, Couldn't write the file) with the file or command; approval cards show it too
- a launch that returned without error reports 'Started.' instead of 'nothing to show'
- agent system prompt replaced by a short plain one; goal, skill, workflow and fork tools left out (first message about 7.5k -> about 4.3k tokens)
- Models: the running model reports the profile it was started with and the page shows it; Reasoning popover names the loaded native model
- installed model with other settings: 'Apply settings and load', no size, no download wording
- plain chat: agent:false or tools:false answers directly and never runs a tool
- tests: stats/context/verbs/launch/prompt size, server-side enable_thinking=false with the agent's request shape, plain chat over HTTP, ui-flows profile scenario
2026-10-16 04:20:20 +02:00
Niko1221 b15b209a03 Step B: Gyro's trellis MMQ prompt kernels on an upstream ggml, so the packaged engine reads Gyro prompts as fast as rc2
cmake/strata_ggml_tq_overlay.cmake writes, at configure time, a copy of the pinned ggml-cuda headers (and ggml.h, quantize.cu) into
the build folder with TQ2_T / TQK6 / TQK7 routed the way IQ2_XS is: same Q8_0_16 tile layout, dot kernels and tile configs (the
fork's own configs for them are copies of IQ2_XS's), three template instances, the tile loader and tq.cuh from third_party/ggml-tq.
Only the strata_mmq target sees the copy; every other type is compiled from the untouched files. Anchors are checked: another
llama.cpp turns the overlay off with a message (FP16 prompt path). moe_mmq.cu knows the trellis block geometry itself.

The tile loader and the trellis tables are agentionai/llama.cpp's (MIT, ggml authors; Laurent Zuijdwijk), ported from
agentionai/llama.cpp 93a24e5 ggml/src/ggml-cuda/{tq.cuh,mmq-load-tiles.cuh,tq-lut-i8.cuh}.

Checked: Gyro-S and Gyro-M exact_pp 4096/20000 residual dumps IDENTICAL to rc2 (R9700 gfx1201); Q2_0 and IQ3_XXS IDENTICAL to f-split
on gfx1201 and sm_86; ctest ggml_tq_overlay.
2026-10-16 03:51:27 +02:00
Niko1221 76cdc271c0 merge f-split-s4b 0398e09af (README docs on top of 0edf5cb9f) into f-split-int 2026-10-16 03:22:26 +02:00
Niko1221 15937b7e80 tuner measures the engine a load starts: the app's request-slot choice (and fit_max_tokens) moves into buildConfig, which both the load and the tune use; test that a tune's saved config equals a load's for the same model and profile, on a 12 GB and a 48 GB card 2026-10-16 03:20:28 +02:00
Niko1221 a161098036 merge f-split-s4b 0edf5cb9f (Harness in the app chat, Manual approvals, request slots) into f-split-int
index.mjs: createNativeControl gets control, appUpdater and hardwareProbe; the agent bridge gets componentsDirectory() and the bundled Harness folder.
native-control.mjs: this tree's file kept, s4b's parallel choice, fit_max_tokens, instance parallel/mcpServers re-applied after buildConfig; its own retry around control.start is folded into load-host's: ONE second try in all, which carries the memory change and/or the drop of several-at-once, with load-host's plain sentence (FALLBACK_NOTE only when no memory change applies). Test added.
locales: both sides had added keys s2571.., so s4b's seven task keys are renumbered s2615-s2621 in all 8 files.
2026-10-16 03:17:17 +02:00
Niko1221 0398e09af5 agent README: thinking and output budget, tasks 2026-10-16 03:12:53 +02:00
Niko1221 004e22c1de merge f-split-s5 (real tuner and presets) into f-split-int
native-control.mjs: this tree's file kept (S6 updates, load retry, remove), s5's buildConfig split out of loadEngine, auto-tuner, tuneConfig/tuneStatus/autotune, autotune and recheck operations and presets re-applied.
NativeModels.tsx: s5's Tune model button (the real tuner) and NativeTune dialog; the old short-benchmark Tune button stays out. native.ts: job progress field.
2026-10-16 03:11:50 +02:00
Niko1221 a4d20310f1 Gyro needs engine 0.1.45: it ships in the split release's engine (GYRO_ENGINE, docs, test) 2026-10-16 03:05:27 +02:00
Niko1221 0b22b8c736 tuner: fix the RAM-floor rule syntax 2026-10-16 02:48:56 +02:00
Niko1221 a3dd4b702d tuner: a candidate is refused for low free RAM only if it is worse than the base (a RAM-bound base can sit below the floor itself) 2026-10-16 02:48:30 +02:00
Niko1221 5fb8fa95b3 load retry: the same stop twice is reported once (the second try stays in the job's details); fixes the 1280x700 tuning-error pop-up that scrolled in ui-flows 2026-10-16 02:33:25 +02:00
Niko1221 bcbf2caf3f merge f-split-gyro (Gyro-S/M trellis port and catalog entries) into f-split-int 2026-10-16 02:22:37 +02:00
Niko1221 0edf5cb9fc tasks: the host's task event reaches the page (toast); real-clock task and mode flows for a real model 2026-10-16 01:49:30 +02:00
Niko1221 14acfc3b3c desktop test: the stable-origin test no longer assumes port 47831 is free (an installed Strata holds it on this PC) 2026-10-16 01:42:15 +02:00
Niko1221 529a7019ae install job: the finished job keeps the merged progress (engine part and parts); test that the packaged install job fetches the engine through Control and that a failed engine keeps the model 2026-10-16 01:36:22 +02:00
Niko1221 fd0d7fd875 tuner: the quick budget reserves only the extra base rounds and the final base 2026-10-16 01:36:19 +02:00
Niko1221 0bf5fe1ebe tests: the packaged-mode fixture allows Control's installer tool process (fr runs strata_mcp.py there); still no other process 2026-10-16 01:33:54 +02:00
Niko1221 07fe758ac9 merge f-145-int 194c26881 (model-card sampling defaults, web sampling, answer room, thinking budget) into f-split-int
serve/server.py: the model_arch line joins the existing engine == strata block next to requested_slots.
2026-10-16 01:31:17 +02:00
Niko1221 90a5d082bc merge f-split-load (pin_budget reserves on Windows) into f-split-int 2026-10-16 01:30:58 +02:00
Niko1221 fa0d99e5b7 merge f-split-load-host (one safer second try when a load fails) into f-split-int 2026-10-16 01:30:58 +02:00
Niko1221 8820fa433d merge f-split-fr (owner app UI fixes, rounds 1-5) into f-split-int
native-control.mjs: fr's file kept, S6's engine step, component updates, plain update messages and app updater re-applied on it;
native.ts: fr's types plus capabilities.componentUpdates; SettingsUpdates: fr's per-row update buttons, Update engine enabled without a model when component updates exist.
2026-10-16 01:30:48 +02:00
Niko1221 781690f397 tuner: per-request points get a control and a fresh-engine 5-round confirmation (the first real run showed sending any setting reads ~10% faster) 2026-10-16 01:25:47 +02:00
Niko1221 20e0bf2e9d direct chat: Thinking budget in the Reasoning effort menu (reasoning_budget_tokens); Off reaches the server as none 2026-10-16 01:25:09 +02:00
Niko1221 3571ceb1e4 do not track generated Tauri schemas 2026-10-16 01:22:20 +02:00
Niko1221 a4a0da0d5d app updater: the installer starts outside the app's Job Object (found by the end-to-end run), plain message when no update key exists 2026-10-16 01:22:14 +02:00
Niko1221 22265c6ac7 tuner: fix the killed-engine pattern 2026-10-16 01:16:07 +02:00
Niko1221 e62e6bf2f5 tuner: a killed engine (no message) is an out-of-memory 2026-10-16 01:15:37 +02:00
Niko1221 84ca0b6c21 tuner: five decodes per run, early drop only after a 5% loss, noisy default-confirmed says so 2026-10-16 01:11:57 +02:00
Niko1221 daa45b1e37 thinking: the Strata server reads chat_template_kwargs, not a top-level enable_thinking (Off had no effect); plain output for read-only PowerShell noise 2026-10-16 01:11:17 +02:00
Niko1221 ff62ce3fbe app: Tune strings without the duplicate Check again 2026-10-16 01:11:15 +02:00
Niko1221 7d43470f31 app: Tune dialog tidy, short Check again label; headless UI fixture for it 2026-10-16 01:10:56 +02:00
Niko1221 14168890c9 app catalog: a test for the Gyro sizes in the Qwen3.8 group 2026-10-16 01:08:57 +02:00
Niko1221 10ede85dd1 tests: skip the real-install step test when the disk is too small for setup's own check 2026-10-16 01:05:23 +02:00
Niko1221 5f054d2a2c tests: resident-mode detection 2026-10-16 01:04:17 +02:00
Niko1221 aab3810b54 tuner: the GPU cache's 'resident experts' wording is not the low-RAM resident mode 2026-10-16 01:03:41 +02:00
Niko1221 bd773bbd77 app: Tune model dialog (real tuner, saved tunings, undo) reusing the Auto-tune look; its strings in separate catalogs 2026-10-16 01:02:17 +02:00
Niko1221 35cfd187cf host: autotune and recheck operations, saved-tuning routes (Strata Control's tuner and presets, run on the config a load would use) 2026-10-16 00:58:17 +02:00
Niko1221 eaf8745697 docs/GYRO.md: setup is the install path, our measurements, credit and licenses; docs/MODELS.md lists Gyro-S and Gyro-M; tools/gyro_setup.sh (rc2's script) is replaced by setup 2026-10-16 00:56:04 +02:00
Niko1221 c0616f06be tuner: AMD architecture from the kernel for the never list 2026-10-16 00:54:20 +02:00
Niko1221 e6b3af5889 tuner: waiting for the box lock does not count against the time budget; session tests 2026-10-16 00:53:30 +02:00
Niko1221 8e2460d18c owner feedback round: unified local-GGUF page and Manage models, no Tune model, Engine settings reduced with working Save logs, update buttons only when available, GPU data in Monitor with no model, thinking dots stop, Latest spacing, calmer native status reads, local GGUF adoption via --gguf-dir, no Contrast option or request-log button 2026-10-16 00:53:05 +02:00
Niko1221 33120935a3 tuner: a queued kernel flock for the box lock, so other streams' waiters are served in order 2026-10-16 00:52:06 +02:00
Niko1221 bb33349149 tuner: memory and quiet checks run inside the box lock right before each start 2026-10-16 00:49:59 +02:00
Niko1221 f23eda8c95 auto-tuner: single-knob candidates from the measured spec, interleaved rounds with noise and drift guards, memory guards, saved presets (one store for CLI and app), re-check, strata tune auto / strata presets 2026-10-16 00:48:42 +02:00
Niko1221 326e391188 merge f-split into f-split-s6 2026-10-16 00:33:37 +02:00
Niko1221 158a42d8b6 tasks run as the agent in their own session under a per-task mode, with a plain result and a notification; task editor and toast strings in all eight languages 2026-10-16 00:33:05 +02:00
Niko1221 be1a871349 agent: output budget for a local model is the window (was 8192), the Reasoning menu reaches the request, fit_max_tokens for app-loaded models, plain outcome for silent commands 2026-10-16 00:30:13 +02:00
Niko1221 5e33c4e85d Gyro: the MCP recommendation asks 4 GB of room beside the memory figure; tests for the Gyro catalog 2026-10-16 00:16:01 +02:00
Niko1221 b74d6ad3d3 stage-harness: flatten pnpm's links into one real folder per package (the installer copies files) 2026-10-16 00:15:08 +02:00
Niko1221 375f76fef0 setup: a tip for Gyro installs on the ready-made engine (the fork's prompt kernels need a source build) 2026-10-16 00:14:27 +02:00
Niko1221 02bbf51fb7 Gyro: the Hadamard row kernels go up in slices of 65535 rows (gridDim.y limit): the FP16 prompt path rotates T*K rows, which a 16K-token chunk takes past it (invalid configuration argument on a build without the fork's MMQ) 2026-10-15 23:46:50 +02:00
Niko1221 945971648a desktop: ship the staged Harness as resources/harness (built by stage-harness.mjs) 2026-10-15 23:28:08 +02:00
Niko1221 365c60e7a4 Metal: stubs for the Gyro Hadamard entry points (the engine does not run trellis experts; native_expert_supported refuses them at load) 2026-10-15 23:02:00 +02:00
Niko1221 34204c4a31 Gyro: the CPU trellis rows build for arm64 (scalar kernel; the AVX units are x86-only) and the Metal build lists hadamard.cpp and tq_cpu.cpp 2026-10-15 22:47:19 +02:00
Niko1221 805a308d2e pin budget: CUDA-on-Windows default only (the HIP build keeps its pinning), docs/DETAILS.md 2026-10-15 22:45:55 +02:00
Niko1221 ff14364de0 packaging: recipe and table row for the gfx1103 (Radeon 780M) Linux HIP engine package
Built on P2006 from the f-split tree and run through the Terminal archive: resolve (kfd gfx1103), tune, serve and client, TUI, Ctrl+C/SIGHUP, saved preset, memory back to baseline.
2026-10-15 22:35:46 +02:00
Niko1221 4e67f1f045 host: one automatic safer second try when the engine dies while loading (smaller pin / RAM budget), said in plain words and not saved; Leave 20% also sets the engine's own RAM headroom 2026-10-15 22:31:15 +02:00
Niko1221 e1d11fe727 Gyro: native_fmt knows the trellis types' geometry itself, so a ggml built from upstream (no such types; its type table is indexed by the id) is never asked about them 2026-10-15 22:28:55 +02:00
Niko1221 4ff5cd4050 pin budget: test knobs (simulated ceiling, 'driver took everything'), report lines, smaller default reserve, pin_budget_test 2026-10-15 22:27:30 +02:00
Niko1221 4f74de9cdb Keep pinned-memory headroom while the expert arena pins; retry the engine's required pinned buffers once after handing it back (Windows load failure: verify mapped staging allocation failed) 2026-10-15 22:13:37 +02:00
Niko1221 ffbc606682 delete a downloaded native model: strata_remove (own files and run config, shared files kept), Delete in Models and Storage, real disk use per model 2026-10-15 21:48:51 +02:00
Niko1221 eca3aef3a2 Gyro: the CPU trellis kernels run on every CPU (scalar without AVX2: upstream ggml has no trellis vec_dot to fall back to); the SYCL build lists the new CPU sources 2026-10-15 21:47:32 +02:00
Niko1221 ba64156fd4 Catalog: Gyro-S and Gyro-M (agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) in setup, the Terminal, the app and the MCP server
Both files are one GGUF with the n-gram table inside: 58.5 GB and 92.0 GB downloads for 27.6 GB and 35 GB of VRAM + RAM together.
Neither is a default; the MCP recommendation names them only where the 2-3-bit sizes and the Coder do not fit. Pinned to the Hub
revision 1ada08422e40 (sizes and SHA-256 from its LFS pointers). Model license: Qwen Community License 1.0.
2026-10-15 21:44:02 +02:00
Niko1221 ec53cae11f request slots: fit to the expert cache the card would have (measured), hardware from the host probe, safe default with no read 2026-10-15 21:43:30 +02:00
Niko1221 697cbc6376 stage-harness: peers, safe pruning; profile: office converter off 2026-10-15 21:41:06 +02:00
Laurent 26da97bde0 docs/GYRO.md: clone the rc2 branch (Strata v0.1.40.3 + Gyro)
Ported from agentionai/Strata rc2 9ff02c9 (rc2 = 9ff02c9), author Laurent.
2026-10-15 21:31:35 +02:00
Laurent Zuijdwijk 701f31964b docs/GYRO.md: the CPU trellis kernels and their switches
Ported from agentionai/Strata rc2 6bfb1e4 (rc2 = 9ff02c9), author Laurent Zuijdwijk.
2026-10-15 21:31:35 +02:00
Laurent Zuijdwijk 5ebea67b79 native_expert_parity: check the CPU trellis kernels in the synthetic modes
For tq2_t / tqk6 / tqk7 experts (--synthetic, --synthetic-hadamard), the
gate and down rows on every ISA the CPU has against the scalar kernel bit
for bit, and the scalar kernel against ggml-cpu's vec_dot on the same
Q8_0 activations. The CPU path of check (b) already runs through the new
kernels via native_gu_rows / native_down_rows.

Ported from agentionai/Strata rc2 7d0ecd0 (rc2 = 9ff02c9), author Laurent Zuijdwijk.
2026-10-15 21:31:35 +02:00
Laurent Zuijdwijk d0a02fe426 CPU trellis kernels: AVX2 / AVX-512 decode-and-dot for TQ2_T / TQK6 / TQK7
Expert-cache misses of the Gyro files run on the CPU workers. Until now those
rows went through ggml-cpu's vec_dot one token at a time. Strata now has its
own kernels for the three trellis types (tq_cpu.hpp), chosen at run time like
the i-quant ones:

- the 32768 x 4 int16 state table (one 8-byte lookup per four weights, built
  from the codebook in third_party/ggml/ggml-common-tq.h, now also visible to
  host code), scalar state extraction, four table rows per YMM by broadcast
  loads + VPBLENDD, VPMADDWD (AVX2) or VPDPWSSD (AVX-512 VNNI);
- the weights of a block are decoded once for all the tokens routed to the
  expert (a verify window), then each token is a few instructions;
- one fixed arithmetic for every ISA: exact int32 sums per Q8_0 sub-block,
  four float FMA chains, a fixed final sum. AVX2, AVX-512 and the scalar
  reference give the same bits, and a token gives the same bits alone or in a
  group of any size (no group-size rule needed, cf. #152).

native_gu_rows / native_down_rows take them for types 144-146 on AVX2 and
AVX-512 CPUs. STRATA_NO_TQ_KERNELS=1 restores ggml-cpu's vec_dot,
STRATA_TQ_ISA=scalar|avx2|avx512 caps the ISA. A CPU without AVX2 keeps
ggml-cpu.

tq_cpu_parity (CPU only, ctest tq_cpu_parity and tq_cpu_parity_avx2):
AVX2 / AVX-512 vs the scalar kernel bit for bit for groups of 1-8 tokens,
vs ggml-cpu's vec_dot (rel ~1e-7, float order) and vs to_float in double
(rel ~1e-5, the int16 table), plain and Hadamard-rotated, for Gyro-S
(tqk6/tqk7) and Gyro-M (tq2_t) experts through the engine entry points.
`tq_cpu_parity --bench` is the micro-benchmark (clock-calibrated cycles).

Zen 5, one thread, weights streamed from DRAM, core cycles per 128-weight
block (one token / per token in a group of three):
  ggml-cpu vec_dot, AVX2 build:     108-117 / 108-116
  ggml-cpu vec_dot, AVX-512 build:   64-66  /  61-62
  Strata AVX2:                       47-49  /  22-23
  Strata AVX-512:                    47-49  /  22
  (Q2_0's AVX2 kernel: ~20; the repacked AVX-512 Q2_0 kernel: ~7)

Ported from agentionai/Strata rc2 7c85f1d (rc2 = 9ff02c9), author Laurent Zuijdwijk.
2026-10-15 21:31:35 +02:00
Laurent 25a1f6e046 APR on 0.1.40: the sub-16 gate/up rows assume 80 dots per row; trellis types (qk 128) take the full-warp kernel
v0.1.40's row_dot_80_sub16 covers (n_embd / qk) * ipb == 80, true for the 256-value i-quants but not for
TQ2_T / TQK6 / TQK7 (160 dots at n_embd 2560): half of every gate/up row was dropped (native_expert_parity
gpu rel 0.87, garbage output). Restores gpu rel 2.13e-2 / decode-once bitwise.

Ported from agentionai/Strata rc2 c4ba288 (rc2 = 9ff02c9), author Laurent.
2026-10-15 21:31:26 +02:00
Laurent Zuijdwijk 84507d7179 hadamard_rows: say what is wrong with a bad block before exiting
hadamard_rows / hadamard_rows_f16 still exit on an invalid block, like
check() does on a failed launch, but now through one helper whose message
names the entry point, the block, the row width and the requirement (a power
of two in [2, 1024] dividing the width), and where the mismatch comes from.

Ported from agentionai/Strata rc2 48ff8b9 (rc2 = 9ff02c9), author Laurent Zuijdwijk.
2026-10-15 21:31:26 +02:00
Laurent Zuijdwijk 86faf035b6 prefill: say why the FP16 path rotates H in place in fp16
Not changed to an fp32 rotation: hadamard_rows_f16 already does its
arithmetic in fp32 and rounds once on the store, so the only extra error is
the fp16 rounding of the SwiGLU output before the rotation, while an fp32
copy would cost a T*K*640 float buffer or a fused kernel on the non-MMQ
fallback. The comment states this, the precision cost and the parity bound
(relative L1 < 2e-3 against the float rotation); the gate/up input refers to it.

Ported from agentionai/Strata rc2 50a1972 (rc2 = 9ff02c9), author Laurent Zuijdwijk.
2026-10-15 21:31:26 +02:00
Laurent Zuijdwijk 4557f68053 iq_pack: refuse to mark a native_experts.txt other than v3/v4 as v5
The v5 rewrite for Hadamard-folded experts was a re.sub on "v[34]": on any
other header it changed nothing and still wrote hadamard.txt, an
inconsistent pack the engine refuses at load. native_experts_v5() now
rewrites v3 and v4 only and otherwise returns an error naming the version
found and the supported ones; main() prints it and exits 1 before any file
of the pack is written. Tests: both rewrites, the refusal for v2/v5/v6/v10
and a missing header, and a full pack run that must leave no
native_experts.txt or hadamard.txt behind.

Ported from agentionai/Strata rc2 235787c (rc2 = 9ff02c9), author Laurent Zuijdwijk.
2026-10-15 21:31:26 +02:00
Laurent Zuijdwijk 8fd8e6da3c prefill: resolve the APR sign vectors once per device, not per layer and chunk
run_impl called hadamard_device_signs for every folded layer of every chunk,
which takes a process-wide mutex and may cudaMalloc. Prefill::init now
resolves the per-layer gate/up and down sign pointers on its device into
Impl::had_sx / had_sh, as peer_had_signs does for the peer and the layer
split's helper (P.had_sh), and run_impl only reads them. carve no longer
warms the device table on every relayout (init's lookup does that now, once);
the comments on peer_had_signs and the new members say what is resolved when.

Ported from agentionai/Strata rc2 3d191fa (rc2 = 9ff02c9), author Laurent Zuijdwijk.
2026-10-15 21:31:26 +02:00
Laurent Zuijdwijk dd32704d4d prefill: look up the APR sign vectors at the layout's widths, not 640 / N
run_impl asked hadamard_device_signs for the down projection's signs at a
literal 640 (and gate/up's at N), while carve and the peer path resolve them
at lay.fmt[l].n_ff / n_embd. The widths are part of the device copy's key, so
the two only shared an entry because the widths happen to match. All lookups
now use the layout's widths.

The other 640s next to the rotations are the row width of the prompt path's
expert buffers and products (swiglu, quantize, GEMM), fixed for this model
like N; Prefill::init now refuses a native layout whose experts are not
N x 640 instead of assuming it. No other sign or rotation width in the tree
is hardcoded (verify, peer and remote experts use the geometry).

Ported from agentionai/Strata rc2 ff463c8 (rc2 = 9ff02c9), author Laurent Zuijdwijk.
2026-10-15 21:31:20 +02:00
Laurent Zuijdwijk 390da5d905 Trellis numerics: say which codebook each path reads and what is bitwise
Decode reads the int8 codebook tq_lut_i8 (one global scale, 0.85% relative
RMS); the FP16 prompt path reads the fp16 codebook and the MMQ path is the
GGML fork's tile loader. The comment over tq_load / tq_apply now says that
only the decode-once and per-entry kernels are bitwise equal to each other,
and that native_expert_parity bounds the decode path against ggml's float
dequantizer at relative L1 error < 3e-2 (the prompt dequantizers at < 1e-6).
The same goes into a short "Numerics" section of docs/GYRO.md and the parity
test's header.

Ported from agentionai/Strata rc2 457047e (rc2 = 9ff02c9), author Laurent Zuijdwijk.
2026-10-15 21:31:20 +02:00
Laurent Zuijdwijk 99581ed5e3 verify: carve the rotated expert input only for Hadamard-folded packs
nat_xr_ (T x N floats) was taken from the verify arena for every pack, though
only a layer whose gate/up is Hadamard-folded reads it. It is now carved only
when some layer of the native layout has had_gu, as the prompt path does with
its rotated copy; the one reader is under that layer's f.had_gu, so it never
sees the nullptr.

Ported from agentionai/Strata rc2 687b7e6 (rc2 = 9ff02c9), author Laurent Zuijdwijk.
2026-10-15 21:31:20 +02:00
Laurent Zuijdwijk 294c95ebbc APR packs: drop the STRATA_APR opt-in; warn only on unvalidated setups
The GPU path for Hadamard-folded experts is now validated end to end on
CUDA sm_86, sm_89 (also a two-GPU layer split and peer decode) and sm_120,
and on HIP gfx1151, so `strata generate` no longer exits without
STRATA_APR=1 (the variable is ignored if set). It logs a warning instead for
the two setups that have not been run end to end yet: a HIP GPU other than
gfx1151, and the P2P prompt peer (Prefill::set_peer).

docs/GYRO.md and tools/gyro_setup.sh no longer set STRATA_APR=1; the setup
script's AMD note now says what was validated on gfx1151.

Ported from agentionai/Strata rc2 0fabd20 (rc2 = 9ff02c9), author Laurent Zuijdwijk.
2026-10-15 21:31:20 +02:00
Laurent Zuijdwijk 7709a0e2c8 docs/GYRO.md: read as part of Strata (no fork clone step); render-group note for HIP containers
Ported from agentionai/Strata rc2 f9a40f8 (rc2 = 9ff02c9), author Laurent Zuijdwijk.
2026-10-15 21:31:20 +02:00
Laurent Zuijdwijk cce9b16a72 docs/GYRO.md: RTX 30-series setup line, RTX 4090 validation and the two-GPU notes
Ported from agentionai/Strata rc2 98125c0 (rc2 = 9ff02c9), author Laurent Zuijdwijk.
2026-10-15 21:31:20 +02:00
Laurent 926171c779 docs/GYRO.md: 2026-10-03 results (5090 #2, Gyro-M, 3090, AMD smoke); fix Gyro-S size
Gyro-S is 58.5 GB (54.5 GiB). Gyro-M and AMD have now run end to end;
long-prompt prefill is measured. The measured section now has the int8
decode numbers (rc1 includes them and --prefill auto), the server
protocol, Gyro-M and the 24 GB RTX 3090; the cache-size rows stay,
labelled as the 2026-10-02 build.

Ported from agentionai/Strata rc2 c494121 (rc2 = 9ff02c9), author Laurent.
2026-10-15 21:31:20 +02:00
Laurent Zuijdwijk f5b7628a7f APR port: Split<144..146>::load takes upstream's STAGE_GRID form
Upstream 0.1.39 calls the decode-once loaders as
S::template load<STAGE_GRID>(row, kbx, iqs, s_grid) (the IQ types can stage
their grid table in shared memory). The trellis loader has no grid to stage;
it takes the template parameter and the table pointer and ignores them.
Found by the first CUDA build of rc1 (CUDA 12.4, sm_89).

Ported from agentionai/Strata rc2 07e9afd (rc2 = 9ff02c9), author Laurent Zuijdwijk.
2026-10-15 21:31:20 +02:00
Laurent Zuijdwijk 28651a4079 APR port: multi-GPU prompt offload (peer, layer split helper) for Hadamard-folded packs
set_peer and set_stage_helper no longer decline an APR pack. Their MMQ
share now takes the same rotations as the primary's MMQ path:

- gate/up input: the primary sends its rotated copy (m.Xr = H (s * x), made
  once for its own MMQ quantize) instead of `mixed` to the peer / helper
  card, over P2P or the mapped host route. One rotation serves both cards,
  bit for bit the same input; `mixed` still feeds the shared expert.
- down input: on the peer / helper card, hadamard_rows on H between
  mmq::swiglu and mmq::quantize, in the compact (per group) and the
  layer-sized buffers, with that card's own copy of the layer's sign
  vector (PeerPrefill::had_sh, made at setup on the peer's device).
- The fused int8 prompt kernels never take a folded layer (fused_nat), and
  bind_stage_helper already refuses layers on the fused kernels, so the
  helper's folded layers are always on MMQ with the separate
  swiglu -> rotate -> quantize passes.
- The trellis types need nothing more there: the peer gathers the blobs
  with mmq::gather_native and runs the same MMQ instances (STRATA_MMQ_TQ).

Not run on a GPU yet. Needs a 2-GPU box: Gyro-S with --peer-device (and
separately a layer split with STRATA_PREFILL_HELP=1), the first token's
logits and a greedy generation against the single-GPU run.

Ported from agentionai/Strata rc2 c64e1ec (rc2 = 9ff02c9), author Laurent Zuijdwijk.
2026-10-15 21:31:19 +02:00
Laurent Zuijdwijk 72f21aca93 docs/GYRO.md: clone the rc1 branch
Ported from agentionai/Strata rc2 87fc901 (rc2 = 9ff02c9), author Laurent Zuijdwijk.
2026-10-15 21:31:04 +02:00
Laurent b4e477c014 APR trellis decode: int8 codebook + dp4a in tq_load / tq_apply
Port of the agention llama.cpp CUDA change (perf/tq-cuda-decode 0005): the 2048-point trellis
codebook as int8 pairs with one global scale (max |point| 3.984 -> 127; relative RMS error 0.85%
of the codebook std). A trellis step's 4 weights are two 2-byte entries = one int, so a decoded
lane (TqLane) is 4 ints instead of 16 floats (5 registers instead of 17) and tq_apply is 4 dp4a
instead of 16 int8->float conversions + 16 FMAs. load/apply stay split: decode-once and per-entry
kernels remain bitwise equal. The dequantizers (prompt path, embedding) keep the fp16 codebook.

native_expert_parity --synthetic tqk6/tqk7 tq2_t/tq2_t and --synthetic-hadamard: 0 failures (RTX
3090, CUDA 12.4); GPU rel vs reference 1.18e-2 -> 2.13e-2 (tqk6/tqk7), 2.20e-2 (tq2_t), within the
3e-2 bound; gpu decode-once vs per-entry: bitwise equal.

Ported from agentionai/Strata rc2 87bad56 (rc2 = 9ff02c9), author Laurent.
2026-10-15 21:31:04 +02:00
Laurent 81aef750d2 gyro_setup.sh: --prefill auto, like setup.py writes for the GSQ-RCO models
The Gyro config used --prefill 512, so the prompt path borrowed only 364 cache slots. Measured on
an RTX 3090 (24 GB, cache auto = 15,756 slots, ctx 131k), same prompts: 4K-token prompt 767 ->
1,325 tok/s, 16K 817 -> 1,870 tok/s, on par with ISTA IQ2_XS under setup.py's config (1,401 /
2,058); decode unchanged within noise.

Ported from agentionai/Strata rc2 a4d70dd (rc2 = 9ff02c9), author Laurent.
2026-10-15 21:31:04 +02:00
Laurent Zuijdwijk 75d9b260da gyro_setup.sh: export STRATA_GGUF_PY for every step (mtp_pack.py needs it too)
Ported from agentionai/Strata rc2 0d0f4b0 (rc2 = 9ff02c9), author Laurent Zuijdwijk.
2026-10-15 21:31:04 +02:00
Laurent Zuijdwijk 2fce1e1e61 gyro_setup.sh: point the HIP build at ROCm (ROCM_PATH or /opt/rocm)
Ported from agentionai/Strata rc2 73c3106 (rc2 = 9ff02c9), author Laurent Zuijdwijk.
2026-10-15 21:31:03 +02:00
Laurent Zuijdwijk e05565e223 gyro_setup.sh: set up the venv first and build with its cmake/ninja (distro cmake can be < 3.24)
Ported from agentionai/Strata rc2 9a89f63 (rc2 = 9ff02c9), author Laurent Zuijdwijk.
2026-10-15 21:31:03 +02:00
Laurent Zuijdwijk 21aa723a2a gyro_setup.sh: skip the download when the model file is already there
Ported from agentionai/Strata rc2 ffad2f0 (rc2 = 9ff02c9), author Laurent Zuijdwijk.
2026-10-15 21:31:03 +02:00
Laurent Zuijdwijk ffbb367b8a docs/GYRO.md: clone from the agentionai fork
Ported from agentionai/Strata rc2 0802c90 (rc2 = 9ff02c9), author Laurent Zuijdwijk.
2026-10-15 21:31:03 +02:00
Laurent Zuijdwijk 5f48f323da docs/GYRO.md: drop the agent-client note
Ported from agentionai/Strata rc2 e466545 (rc2 = 9ff02c9), author Laurent Zuijdwijk.
2026-10-15 21:31:03 +02:00
Laurent Zuijdwijk 0ca3752ea6 Gyro: setup guide and script for the published agentionai Gyro-S/Gyro-M files
docs/GYRO.md: build against the agentionai llama.cpp fork, pack the published GGUF unchanged,
Strata's MTP runtime, server config, RTX 5090 measurements (full and 16/8 GB-sized expert caches)
and known limits. tools/gyro_setup.sh does every step on Linux (CUDA or HIP).

Ported from agentionai/Strata rc2 759675e (rc2 = 9ff02c9), author Laurent Zuijdwijk.
2026-10-15 21:31:03 +02:00
Laurent Zuijdwijk f4abf43154 APR port: count the prompt path's rotated-input buffer in moe_set_bytes
carve() takes m.Xr (T x 2560 floats, the Hadamard-rotated copy of `mixed` for
the MMQ path) from the MoE set of the shared region, but moe_set_bytes, which
sizes that region (and bytes_needed / the borrowed slots), did not count it.
With an APR pack and MMQ on, the MoE set is the region's largest, so its last
take overflowed and init failed with "device buffers for a chunk of N tokens
do not fit" at every chunk and cache size, borrowed or own buffers alike
(reported on an RTX 5090 with the published Gyro-S). Workaround on an older
build: STRATA_PREFILL_MMQ=0 (the FP16 prompt path takes no Xr).

Ported from agentionai/Strata rc2 09042dd (rc2 = 9ff02c9), author Laurent Zuijdwijk.
2026-10-15 21:31:03 +02:00
Laurent Zuijdwijk 62bb21a2d7 APR port: run the published Gyro files unchanged (Q6_K embedding, Q8_0 PLE table)
- iq_kernels.cu: dq_q6_k (llama.cpp's dequantize_block_q6_K, two halves per
  thread) in dq_dispatch; embed_type_supported and iq_row_bytes take Q6_K
  (embedding only) - Gyro-S's token_embd.
- ngram.cpp: PleTable reads Q8_0 rows (170 B; PLE_ROW_BYTES_MAX raised), mapped
  and direct SSD I/O - Gyro-M's per_layer_token_embd.
- native_expert_parity: --gguf-embd (random token_embd rows via iq_embed_rows
  vs ggml to_float), --gguf-ple (random PLE rows via PleTable, both I/O modes),
  --load-check (the engine's start-up checks for a pack + GGUF, no GPU load).

gfx1151: Gyro-S token_embd (Q6_K) and Gyro-M token_embd (Q8_0): 300 rows,
0 values differ; Gyro-M PLE (Q8_0) and Gyro-S PLE (IQ4_NL): 320 rows, 0
differ, mmap and direct. --load-check on packs of both published files:
0 failures. (Measured before the rebase onto 0.1.39.)

Ported from agentionai/Strata rc2 cc287a6 (rc2 = 9ff02c9), author Laurent Zuijdwijk.
2026-10-15 21:30:56 +02:00
Laurent Zuijdwijk f7f21bc823 APR port: decode-once verify kernels and MMQ prompt path for TQ types
- iq_kernels.cu: the TQ lane dot split into tq_load (16 codebook values) and
  tq_apply; vec_dot_tq_q8_1 = apply(load), Split<144..146> + kSplit, so
  verify windows decode each weight lane once for all entries (bitwise
  equal to the per-entry kernels).
- MMQ: with STRATA_GGML_DIR = the agention fork, the fork's
  mmq-instance-{tq2_t,tqk6,tqk7} are built and moe_mmq takes the types
  (STRATA_MMQ_TQ). prefill.cpp rotates a copy of `mixed` (Xr) before
  mmq::quantize and H between swiglu and quantize; device sign vectors are
  made at prefill setup.
- hip_prefill_mmq_parity: TQ gate/up (1280x2560) and down (2560x640, odd
  blocks per row) passes; MMQ_PARITY_REPEAT; the output sentinel is now set
  with hipMemsetAsync on the test stream (a null-stream hipMemset could land
  after the MMQ kernel and make every output look unwritten).

- Rebased onto upstream 0.1.39: the fused int8 prompt kernels (STRATA_PF_FUSED)
  never take a folded layer. The multi-GPU prompt paths (set_peer,
  set_stage_helper) decline a Hadamard-folded pack here; a later commit
  gives their MMQ share the rotations.

gfx1151 (ROCm 7.2.1, STRATA_PREFILL_MMQ=ON): MMQ parity passed 3 runs x 10
repeats (rel_l2 1.4e-3..3.2e-3); native_expert_parity --synthetic and
--synthetic-hadamard tqk6/tqk7 tq2_t/tq2_t: 0 failures, decode-once bitwise
equal. CUDA 12.8 sm_120 full build compiled (incl. TQ MMQ instances) before the
rebase onto 0.1.39.

Ported from agentionai/Strata rc2 dc19786 (rc2 = 9ff02c9), author Laurent Zuijdwijk.
2026-10-15 21:30:50 +02:00
Laurent Zuijdwijk bda227817e APR port: TQ row bytes on the GPU (iq_row_bytes returned 0 for 144-146)
native_expert_layout takes gu_row / d_row / up_off / down_off from
iq_row_bytes, which had no cases for the trellis types and returned 0:
every gate, up and down row read row 0 of the blob, and up/down read the
gate matrix. The dequantizers (flat superblock indexing) were unaffected,
which is why they matched ggml bit for bit while the expert matmul did not.
native_expert_supported now also refuses a type without row bytes.

gfx1151 (ROCm 7.2.1): native_expert_parity --synthetic and
--synthetic-hadamard tqk6/tqk7 tq2_t/tq2_t: 0 failures (gpu rel = cpu rel,
cpu-gpu 5e-5..1e-3).

Ported from agentionai/Strata rc2 dae1f66 (rc2 = 9ff02c9), author Laurent Zuijdwijk.
2026-10-15 21:29:42 +02:00
Laurent Zuijdwijk fbf045eb6a APR port, stage 4: trellis decode and Hadamard rotation on the GPU paths
- third_party/ggml/ggml-common-tq.h: the fork's TQ2_T/TQK6/TQK7 block
  structs and 2048-point codebook (device table).
- iq_kernels.cu: Fmt<144..146> (the fork's tq.cuh lane decode against q8_1,
  4 q8_1 blocks per 128-block), dq_tq in dq_dispatch (prompt FP16 path,
  embedding), is_iq / row bytes; hadamard_rows / hadamard_rows_f16
  (shared-memory FWHT in the CPU's butterfly order) and
  hadamard_device_signs; native_expert_grouped rotates h when
  NativeExpertLayout::had_block is set.
- verify.cpp (local GPU), remote_experts.cpp (helper GPUs), prefill.cpp
  (FP16 fallback; folded layers never take MMQ) rotate x / h into their
  own buffers; the shared expert and the CPU doorbell copy keep x.
- native_expert_parity --synthetic-hadamard; ctests for tqk6/tqk7 and
  tq2_t when ggml is the agention fork.
- generate.cpp: APR packs run only with STRATA_APR=1 until validated end
  to end on a GPU.
- hip_backend.cmake: gfx1151 as an unvalidated architecture.

Rebased onto upstream 0.1.39: native_expert_grouped takes the separate
SwiGLU / rotate / q8_1 passes when the down weights are folded (the one-pass
swiglu_q8_1 kernel has no h to rotate); the new decode peer GPU
(peer_experts.cpp) rotates its private input copy and passes the down signs,
as remote_experts.cpp does.

Ported from agentionai/Strata rc2 6c7abde (rc2 = 9ff02c9), author Laurent Zuijdwijk.
2026-10-15 21:29:38 +02:00
Laurent Zuijdwijk f47b0385f3 APR port, stage 3: Hadamard-folded experts on the CPU path
- hadamard.{hpp,cpp}: hadamard.txt parser, process-wide spec, in-place
  signs + normalized FWHT per block.
- expert_layout_load reads native_experts.txt v5 + hadamard.txt and sets
  NativeFmt::{had_block, had_gu, had_d, gu_signs, d_signs}; inconsistent
  packs are refused.
- native_quant_act / native_quant_h rotate before quantizing, which covers
  every CPU consumer (expert_source's doorbell copy, the pool's h).
- generate.cpp refuses Hadamard packs until the GPU paths rotate too.
- native_hadamard_test: Strata's CPU expert vs the fork's ggml graph
  (ggml_mul(signs) + llama_mul_mat_hadamard + ggml_mul_mat): bit-identical
  on real Gyro-S (TQK6/TQK7) and Gyro-M (TQ2_T) experts, also through
  ExpertPool::run_split_multi_native; synthetic Q8_0 reproduces the
  unrotated float expert to 1.2%.

Built with -DSTRATA_GGML_DIR=<agention llama.cpp> (ggml-cpu supplies the
trellis vec_dot kernels).

Ported from agentionai/Strata rc2 1587897 (rc2 = 9ff02c9), author Laurent Zuijdwijk.
2026-10-15 21:28:58 +02:00
Laurent Zuijdwijk 89febede8b APR port, stages 1-2: map, trellis type tables, prism.hadamard packing
- Type tables: PTQ1_0/TQ2_T/TQK6/TQK7 (143-146, 128-value blocks) in the C++
  and Python GGUF readers.
- iq_pack.py: validates prism.hadamard.* and writes hadamard.txt (block,
  sign vectors, folded roles per layer); native_experts.txt becomes v5 for
  such models so an engine that cannot rotate refuses the pack. Anything
  it cannot apply (dense folded weights, inverse tables, gate without up,
  Q2_0 experts, missing sign widths) is refused. 3 new unit tests.
- embd_bf16_pack.py --gguf: a GGUF's token_embd dequantized to BF16, for
  embedding types without a GPU dequantizer (Gyro-S: Q6_K).

Ported from agentionai/Strata rc2 d864b6a (rc2 = 9ff02c9), author Laurent Zuijdwijk.
2026-10-15 21:28:47 +02:00
Niko1221 31556cd1a0 helpers wait for their turn visibly; batch-mtp policy off; helper cap max(slots,4) 2026-10-15 21:25:37 +02:00
Niko1221 322e3fdfb3 Terminal: a failed runtime start names the engine log; doctor warns when the render group is missing
- runtime start/restart/verify that fail print the run config's engine log path and its last lines (the line was only 'server_exited')
- doctor: on Linux with a SYCL or HIP run config, a /dev/dri/renderD* (and /dev/kfd) the user cannot open is GPU_DEVICE_ACCESS (warning) naming the render group;
  seen on P1006 where the engine only said 'No device of requested type available'
2026-10-15 21:14:37 +02:00
Niko1221 8401c1cdb5 stage-harness: simpler prune of files 2026-10-15 21:14:08 +02:00
Niko1221 486555d4f1 scripts: agent-flow-s4b (helpers, one-card edit, web call, Stop) for a real model 2026-10-15 21:13:23 +02:00
Niko1221 3152013186 agent README: tools, helper agents, /goal, plain words, Manual gate 2026-10-15 21:12:33 +02:00
Niko1221 1e73f53160 host: tool servers (MCP) set up for the model are connected to the agent, secrets only through the environment 2026-10-15 21:09:32 +02:00
Niko1221 b9c6c65482 engine component: a HIP package is not chosen for a card it has no code for
P2006 (Radeon 780M, gfx1103) resolved the gfx1200/gfx1201 package as usable: HIP detection looked at the PCI vendor only.
The Linux kfd topology (gfx_target_version) names each card's gfx target; a package whose archs list none of them is skipped with the reason.
Unknown targets (Windows, no kfd) are not a refusal, as with CUDA.
2026-10-15 21:08:57 +02:00
Niko1221 37cab2c76f tests: /goal over HTTP, plain unavailable notice 2026-10-15 21:07:26 +02:00
Niko1221 14d0ec7ac8 tests: request slots default, sizing, retry with one 2026-10-15 21:05:55 +02:00
Niko1221 5a00c8fb64 host: agent tools, helper agents, one approval card in Manual, /goal, plain agent states, request-slot sizing
- strata-gate plugin: Manual asks before a change (no blocked attempt first); children of a Manual chat reach the same card
- preset.mjs: the Harness standard preset minus scheduler, search without a key, unanswerable question tool, plan mode
- helper agents projected into the existing agent rows; Stop stops the helpers; the run ends when they have reported
- /goal through the Harness goal Remotes; goal rounds stream into the chat
- native-parallel.mjs: request slots by default in the installed app (fit policy, one-line switch), batch-mtp, one-slot retry
- plain wording for agent failures and tool errors, original text kept as details
- stage-harness.mjs: pruned production Harness for the installer (not run yet)
2026-10-15 21:05:15 +02:00
Niko1221 1696b5c959 installer: header text, branding line, no details pane 2026-10-15 20:29:43 +02:00
Niko1221 0db5d17102 chat speed is decode speed with reasoning, context shown is the running model's, Models shows Unload while loaded, VRAM reserve floor, default text size 14, installer branding, icon 2026-10-15 20:25:35 +02:00
Niko1221 85c7bfda29 Terminal archive: calibrate refuses in words, models import works, tuner runs a packaged engine and boolean flags
Found qualifying the Terminal archive on real boxes:
- engine calibrate crashed with ModuleNotFoundError (tools.calibrate / setup are not in the archive): refuse with exit 5 naming STRATA_INSTALL_DIR and tune run --mode api; --plan reports tool_available
- models import --plan failed with "fingerprint catalog is unavailable": data/gguf_fingerprints.json is now in the archive
- tune plan/run --engine <package exe> could not load the package's libraries: engine_env puts the package folder first; --engine component:strata-engine accepted
- a boolean candidate (--kv-grow) was passed as "--kv-grow 0" ("unknown argument: 0"), and the protocol harness turned "0" into a bare flag (on): flag_argv
- an unmeasured candidate's reason showed the engine usage dump; the reason line comes first now
2026-10-15 20:13:16 +02:00
Niko1221 42ffb27951 stable window origin (fixed port with fallback), run configs repaired when Control moves, app installs to Programs\Strata and migrates an old install out of the data folder 2026-10-15 20:10:06 +02:00
Niko1221 bfe0798660 install progress step as a sentence; Model files label; ui-flows details check 2026-10-15 19:51:52 +02:00
Niko1221 5df75b1d4d first-run feedback 2: plain words for failures with Details, parts-based download progress, reading size reaches all conversation text, tuning pop-up fits its error, headless UI flows, installer mock-ups
ErrorNotice and toasts map known failures (server_exited, engine missing, network, integrity, Harness missing, Control) to a sentence and a next step with the original text under Details. The install progress sums a list of parts (model files, engine) into one count with a time left. The Read pictures block is hidden when the catalog has no pictures. Control reports the last server log lines of a failed start. Reading size scales headings, code, tables and reasoning. All eight locales.
2026-10-15 19:42:08 +02:00
Niko1221 844f91c606 app: every updater message in plain words, technical reasons in details 2026-10-15 19:23:38 +02:00
Niko1221 36c6ff200e host: the model install job also gets the engine for this PC (engine-step), one combined progress total, retry message on failure 2026-10-15 19:21:21 +02:00
Niko1221 7b2082dbb0 first-run feedback: combined download progress with an ETA, catalog without pictures for app installs, RAM budget for the 20% profile, human engine-start errors, installed model preferred in Models, system theme default, NSIS ignores a remembered temp install folder 2026-10-15 19:10:26 +02:00
Niko1221 8a9d724e6e host: look for the Harness in the user's component folder too, not only when STRATA_COMPONENTS_DIR is set 2026-10-15 17:53:22 +02:00
Niko1221 1b6ed0898d desktop: first-run-ui.mjs, headless Chrome on the packaged host's onboarding with the real catalog 2026-10-15 17:50:11 +02:00
Niko1221 0c37d0e285 update --check: do not print a note twice 2026-10-15 17:49:49 +02:00
Niko1221 2bd0cdc4f9 installer: home() instead of a constant (callers can still move ROOT); close cancels a running install; tests for the app's install mode 2026-10-15 17:46:28 +02:00
Niko1221 d949d600f8 updates: shell bridge tests, host test that a start never reaches the update step, install hint 2026-10-15 17:40:14 +02:00
Niko1221 5a8985620c start path: a signed package is verified with the keys this build trusts; engine-missing notices name Update engine and strata update 2026-10-15 17:38:20 +02:00
Niko1221 10a326a90c packaging: UPDATES.md, feed_tool url prefix and private key file, tests 2026-10-15 17:37:03 +02:00
Niko1221 5322caafe0 Merge branch 'f-split' into f-split-fr 2026-10-15 17:36:37 +02:00
Niko1221 11bf85d8a8 host: the packaged app gets the model catalog, the GPUs and the install job from Control's bundled installer
native-control runs Control's tools/strata_mcp.py on Control's Python (installerEnvironment: STRATA_NO_ACQUIRE stays for the
process; only the install job the user starts may fetch model files), in a writable per-user install folder. The catalog,
hardware (nvidia-smi) and recommendation come from it, the install plan and job go through it, and a failed catalog is an
empty one. Calibration stays off in the packaged app. packaged-control tests adjusted and extended.
2026-10-15 17:36:32 +02:00
Niko1221 ec42c93947 installer: setup.py and strata_mcp.py run under Control (STRATA_CONTROL_INSTALL) for the desktop app
In the app's mode the install downloads the model the user chose, prepares it with the bundled tools, installs no Python
package, fetches no engine and compiles nothing, and writes a run config naming component:strata-engine into a per-user
folder (STRATA_INSTALL_DIR). Only bundled tools run (run() refuses pip, git, cmake and anything else). The engine
component is looked up if present; a model can be downloaded before an engine is installed.
stage-control.py adds setup.py, the preparation tools (static import closure), ref/load.py and data/ to the staged Control.
2026-10-15 17:36:32 +02:00
Niko1221 5778a9ef17 update: Control bundles may carry relative links inside themselves (packer and unpacker); tests 2026-10-15 17:35:22 +02:00
Niko1221 4868f3234d updater: require the one-token probe (force the load of a lazy server), keep the test start's engine log, report what was installed when a step fails 2026-10-15 17:33:47 +02:00
Niko1221 d3c4bba4ca host: Settings update check, Update engine/Strata and rollback through Control's explicit update step; shell bridge client; updated Control is found 2026-10-15 17:28:33 +02:00
Niko1221 8ad2a62730 Merge f-split-life: engine always ends on close (app and Terminal), single app instance, Bypass is full access 2026-10-15 17:28:03 +02:00
Niko1221 e3de5044b2 procs: init adopting an orphan does not make init its starter (found on Linux: the orphan check found nothing) 2026-10-15 17:27:15 +02:00
Niko1221 13cd63567e tests: first-run update test ignores the machine's own installed models 2026-10-15 17:19:40 +02:00
Niko1221 1aa306d7ea update: the last check's result is shown as a one-line notice by components engine (no network) 2026-10-15 17:16:33 +02:00
Niko1221 912d7c1673 terminal: strata update, strata components update and rollback 2026-10-15 17:15:35 +02:00
Niko1221 bc9058a8d4 host UI: the first screen no longer crashes on an empty or failed model catalog
GuidedSetup read catalog.recommendation.family without checking that a catalog has a
recommendation; the packaged host's catalog had none and the app showed 'couldn't show this
screen'. GuidedSetup and NativeModels now tolerate no catalog, no recommendation, no families
and families without sizes. A test renders both components with each of those shapes.
2026-10-15 17:14:01 +02:00
Niko1221 c8698f6257 updates: signed feed, Ed25519 verifier, component updater with resume, staged test-start, switch and rollback 2026-10-15 17:13:41 +02:00
Niko1221 5ec49d7adf test_procs: the orphan's stdio is not the test's pipe 2026-10-15 17:10:14 +02:00
Niko1221 95654fe073 matrix: drain the pty a TUI paints to, give it a size, and tolerate an already-reaped child 2026-10-15 17:09:15 +02:00
Niko1221 921668da66 installed-flow: a killed shell takes the host and its engine with it 2026-10-15 16:49:47 +02:00
Niko1221 7d0b3e7090 tests: a real ssh disconnect ends the server and its engine on a Linux box 2026-10-15 16:44:12 +02:00
Niko1221 b11004671f app matrix: a Terminal-started engine survives the app's close 2026-10-15 16:43:07 +02:00
Niko1221 1d26952f72 serve: the engine's clean-stop deadlines are named, and a test shows an engine that ignores STOP, QUIT and SIGTERM is killed 2026-10-15 16:39:14 +02:00
Niko1221 871a242721 matrix: the TUI is quit with q (typed into its console or pty) and killed; the service it watches is untouched 2026-10-15 16:38:42 +02:00
Niko1221 e09733aa9d tuning: the engine and worker a tuning sweep starts end with the sweep, however it ends 2026-10-15 16:35:49 +02:00
Niko1221 d0e9cb3e37 docs: CLOSING.md, what ends the engine on each OS and how leftovers are found 2026-10-15 16:35:15 +02:00
Niko1221 896c66180e serve: a hangup that was ignored at start (nohup) stays ignored; the matrix proves it and resets inherited signal dispositions 2026-10-15 16:32:10 +02:00
Niko1221 f31f24c8dc matrix: STRATA_MATRIX_LOGS keeps the server and engine logs of each case 2026-10-15 16:29:47 +02:00
Niko1221 6b83f43dc9 matrix: the fake engine claims the family the OS supports (metal on macOS) 2026-10-15 16:24:34 +02:00
Niko1221 ec73f63f28 matrix: sample GPU memory while the engine runs, so 'freed' is measured against a loaded value 2026-10-15 16:21:05 +02:00
Niko1221 22fea7b767 desktop: close-path matrix for the installed app, self-test proves the shell is in the job, installed-flow checks a killed host takes its engine 2026-10-15 16:20:45 +02:00
Niko1221 e437bc472d matrix: graceful means the clean-stop line, the engine's marks only for the fake 2026-10-15 16:06:50 +02:00
Niko1221 6aa4663bf2 matrix: a real engine's log goes to the rig 2026-10-15 16:06:02 +02:00
Niko1221 651e64ff32 matrix: real-engine mode takes a component folder and waits long enough for a model load 2026-10-15 16:05:32 +02:00
Niko1221 322790aff0 app: one instance, and closing always ends the engines this app started
Tauri single-instance plugin: a second launch focuses the open window and starts nothing. On Windows the shell puts itself in a
kill-on-close Job Object, so killing it, a crash, logoff or shutdown ends the host, Control, the owner host, the server and the
engine. The host follows its parent (STRATA_EXIT_WITH_PARENT) and takes SIGHUP and SIGBREAK down the clean shutdown; every engine
it starts is tied to the host with runtime start --die-with-pid (drain 15 s, then ended); it runs runtime cleanup once at start.
The shell waits 60 s for the host's clean stop (the host's own deadline is 45 s) before ending it.
2026-10-15 16:04:43 +02:00
Niko1221 347d255eaa runtime start: check the engine package before any process exists; a changed file is refused with exit 5 and its name 2026-10-15 15:58:32 +02:00
Niko1221 48e6d64c4f serve, control: a server's engine ends with it however it ends, leftovers are found by PID, start time and command line, runtime stop/cleanup recover a lost owner
lifeline.popen ties every engine, image encoder, MCP server and sd-cli to the server that started them (kill-on-close job on Windows,
PR_SET_PDEATHSIG from one anchor thread on Linux, a pipe watcher elsewhere). Engines and encoders get their own process group so
Ctrl+C, a closing terminal and a console Break reach the server only, which stops the engine the clean way (STOP, QUIT, wait 20 s,
terminate 5 s, kill). SIGHUP, SIGBREAK and the Windows console close/logoff/shutdown events take Ctrl+C's path; the handlers are
installed before the model loads and the browser launch no longer holds the main thread (#1872). The owner guard escalates SIGTERM
to SIGKILL after a grace period. strata_control/procs.py records every launch and finds leftovers; the next engine start, runtime
start, runtime stop and the new runtime cleanup end them after re-checking identity. runtime start --die-with-pid for app-owned
instances. tests/terminal/lifecycle_matrix.py is the close-path matrix.
2026-10-15 15:58:04 +02:00
Niko1221 865b184a0f packaging: what each target's engine package holds 2026-10-15 15:36:59 +02:00
Niko1221 c5388c78cd tests: collect_libs arch filter, ELF check, system/driver split 2026-10-15 15:35:05 +02:00
Niko1221 1bd9746f53 collect_libs: name the libraries a dlopen'd library cannot find 2026-10-15 15:31:11 +02:00
Niko1221 7e469d797c tests: a schema-2 package made by package_engine.py starts a stub engine, and a flipped library byte refuses the start 2026-10-15 15:25:09 +02:00
Niko1221 dc1bc972ba metal: find the kernel sources, ggml-common.h and default tables beside the executable first, so an engine package can move away from the source tree 2026-10-15 15:18:53 +02:00
Niko1221 21da8442f5 collect_libs: dlopen extras are named by their soname, ELF files only 2026-10-15 15:17:00 +02:00
Niko1221 ac4770f2b1 host: Bypass mode is the Harness's full access with no prompts (danger-full-access + never) 2026-10-15 15:15:03 +02:00
Niko1221 d7895e1690 collect_libs: one copy per real library file 2026-10-15 15:13:43 +02:00
Niko1221 633a87577f collect_libs: --data-arch keeps only the wanted gfx kernels 2026-10-15 15:11:44 +02:00
Niko1221 2d784fa6ff packaging: collect_libs.py gathers an engine's shared libraries (ldd closure, dlopen extras, kernel data) for Linux packages 2026-10-15 15:07:14 +02:00
Niko1221 3e8b5161be engine component: optional variant (cuda13/cuda12 of one version), equal versions prefer the newer-driver build 2026-10-15 15:05:00 +02:00
Niko1221 480cfc5f3b engine component manifest schema 2: archs, min_driver, build_id, signature hook; start hashes every file; packager takes nested files 2026-10-15 15:04:15 +02:00
Niko1221 4c968e6199 gdn chunk / Volta recurrence: name the kernels in every device pass (wmma body guarded inside the kernel), not behind __CUDA_ARCH__ at the call sites: the dea9b6de5 guard left the host stubs undefined in a 60;70;86 build (link error) 2026-10-15 14:53:40 +02:00
Niko1221 be885026f9 Merge f-split-s3: Control bundled in the installer, local engine components, no runtime acquisition 2026-10-15 14:52:46 +02:00
Niko1221 dc87d45eb3 app: chat cards drop Demo wording, finished tools say Ran unless approved, and the Mode choice travels with the turn
Approval label is Needs your approval on every card; example-card results lose the Demo prefix. A finished agent tool shows Approved only when the person approved it. Other Demo labels (simulated-model toasts, estimates, preview test, time-skip, context panel, tasks, serving) become truthful Example/Illustrative/Simulated wording in every locale.
2026-10-15 14:49:07 +02:00
Niko1221 2496301c60 host: chat modes (Manual, Plan, Bypass) as Harness permission presets, applied per turn
The page sends the chat's mode with each turn. The bridge switches the session to our preset with the Harness's own /permission command, verifies it through the permissions projection, and fails the turn closed if it cannot. Manual is read-only with ask (a change needs a one-shot approval), Plan is read-only with never (refused, no card), Bypass is workspace-write with never. Tests run the three modes against the real Harness with the scripted worker.
2026-10-15 14:49:07 +02:00
Niko1221 24d28c3f41 packaging: document the Control component, the engine component folder and the no-acquire line 2026-10-15 14:46:06 +02:00
Niko1221 6aa3b99816 tests: the old-engine hint check accepts update.sh as well as UPDATE.bat (found running the suite on Linux) 2026-10-15 14:45:49 +02:00
Niko1221 e1ee39169d sm_60: IQ3 table staging is opt-in (STRATA_IQ_STAGE_TABLES=1), not the default on cc 6.0: no end-to-end gain on a Tesla P100 (#1424) 2026-10-15 14:33:21 +02:00
Niko1221 9546c3af26 fetch-node.sh: mktemp with an X template (GNU mktemp on Linux refused -t); installed-flow.mjs gets a real-engine mode 2026-10-15 14:33:00 +02:00
Niko1221 64d8488c67 serve: a component engine's folder goes first on the library path; with_component_engine helper + test 2026-10-15 14:30:02 +02:00
Niko1221 aecee58bdd host: Control runs without STRATA_API_KEY (an empty one is refused by the server); installed-flow.mjs drives the installed app with a stub engine 2026-10-15 14:28:32 +02:00
Niko1221 4736323d86 desktop: ship the Control component (bundled CPython + locked wheels) in the installer, set STRATA_NO_ACQUIRE and STRATA_CONTROL_DIR for the host, clean host stop through Control, self-test checks for Control and the no-fetch engine notice 2026-10-15 14:22:38 +02:00
Niko1221 2622762f89 host: Strata Control bridge (control-runtime, packaged native-control via owner runtime), acquisition.mjs gates the llama.cpp and cloudflared fetches, /api/control routes, acquire-spy tests 2026-10-15 14:18:13 +02:00
Niko1221 9ddebc1b11 control: run configs can name "component:strata-engine"; the server, doctor and calibration resolve it from the component folder; targets.py keeps the start path off the fetcher; start-through-owner-runtime tests with a fake engine package 2026-10-15 14:13:08 +02:00
Niko1221 dc859b26ff control: resolve the engine from a local component folder (no download path), strata components engine, package_engine.py; the component fetcher refuses under STRATA_NO_ACQUIRE 2026-10-15 14:05:36 +02:00
Niko1221 a7bb051154 docs: agent bridge overview 2026-10-15 13:55:47 +02:00
Niko1221 a51d311fb9 tests: pure agent bridge units (profile, environment scrub, event projection) 2026-10-15 13:55:30 +02:00
Niko1221 cfd45e0c4d host: answer the agent's own questions as unavailable so a turn never waits for a missing answerer 2026-10-15 13:54:08 +02:00
Niko1221 3df612f80a host: stop a leftover Harness from a dead host at the next start (PID record, command-line check) 2026-10-15 13:52:17 +02:00
Niko1221 2425a18705 scripts: agent-flow waits for visible output before Stop and compares tool cards across reload 2026-10-15 13:51:45 +02:00
Niko1221 178bde4ce6 scripts: agent-flow checks per-chat model and two chats at once 2026-10-15 13:49:09 +02:00
Niko1221 537333045a scripts: agent-flow drives the app through approve, Stop and reopen with screenshots 2026-10-15 13:46:03 +02:00
Niko1221 7f81445c02 ui: wire chat to agent frames, real approvals, per-chat Stop and per-chat model choice (no visual change) 2026-10-15 13:42:19 +02:00
Niko1221 a24e78f6f1 host: Stop during agent start cancels the start instead of leaking a turn 2026-10-15 13:42:18 +02:00
Niko1221 fad1ce5568 host: route agent chats through the Harness (stream, approval, stop, status) with a visible fallback 2026-10-15 13:36:58 +02:00
Niko1221 e4da566272 host: internal accessors for the agent bridge (connections with keys, ready instances) 2026-10-15 13:36:58 +02:00
Niko1221 597039b6cb host: agent bridge (sessions, approvals, Stop, resume) with scripted-worker tests 2026-10-15 13:26:08 +02:00
Niko1221 3e373187ea host: project Harness session events into the chat frames 2026-10-15 13:26:08 +02:00
Niko1221 bf63faab2d host: Harness runtime launcher, Gateway client and Strata profile generator 2026-10-15 13:26:08 +02:00
Niko1221 9627ee00ce Merge f-split-desk (Tauri Windows build: node.exe entry path, self-test layout, pinned tauri CLI) 2026-10-15 12:48:43 +02:00
Niko1221 b73ea23b68 a started server and a managed runtime's server are told not to acquire (STRATA_NO_ACQUIRE=1)
setup.start passes it to the server it spawns (subprocess.call env, or execve for Docker); the runtime host sets it in
the owned server's environment. The test checks the start's child environment.
2026-10-15 12:33:40 +02:00
Niko1221 236cca71bb host: no /usr/bin/tar, /usr/bin/env or HOME/.local/state on Windows
- engine-install.mjs extracts with tarBinary(): System32\tar.exe on Windows (bsdtar reads .zip and .tar.*), /usr/bin/tar
  elsewhere.
- util.mjs which() asks System32\where.exe on Windows (first match), /usr/bin/env which elsewhere.
- the tuning and Time-skip state default to %LOCALAPPDATA%\Strata\tuning and \time-skip on Windows
  (apps/host/server/platform-paths.mjs; POSIX keeps $XDG_STATE_HOME/strata). The Python side resolves the same folders
  (strata_control/paths.py, used by the runtime directory and the tune and timeskip commands), so the host and the
  terminal agree.
- tests: apps/host/tests/platform-paths.test.mjs (both platforms from either host) and tests/terminal/test_state_paths.py.
2026-10-15 12:31:49 +02:00
Niko1221 3e5e0c73b2 terminal: an existing install is found through settings.json and used in place
The terminal archive is a new folder with no run configs. models.resolve_install() now looks at the install folders
setup.py remembered in settings.json (installs, next to data_dir) when the archive's own folder has none, read-only,
and reports how it found them (explicit, here, remembered, default). The run configs name their engine and model
files by absolute path, so the old install and its data folder keep serving: nothing is copied, moved or created.

tests/terminal/test_migration.py builds today's layout in a temp dir (strata-<model>.json, engine/, run scripts,
.venv, a separate Strata-data with GGUF, packs and MTP, settings.json) and checks the listing, inspect, a profile and
serve --plan, a plain start and the explicit setup path leave every file, size, mtime and hash as they were.
2026-10-15 12:29:44 +02:00
Niko1221 2e324bbbe8 package_terminal: --wheelhouse builds with no internet; lock_wheels --download fetches exactly the locked wheels
A box with no DNS (p620) can build the Linux archive from a folder of wheels fetched and hash-checked elsewhere:
pip still runs --require-hashes against the lock, with --no-index --find-links.
2026-10-15 12:27:36 +02:00
Niko1221 d5be478937 runtime: the managed owner runtime on Windows (loopback TCP + token, job object, byte-range leases)
strata runtime start/status/stop/verify/restart now work on Windows. The owner transport (loopback-only TCP, 256-bit
token, state file in a private directory) was already there; the supervisor around it was fcntl/getuid/killpg only.

- strata_control/winproc.py: LockFileEx byte-range leases (shared and exclusive, on a byte beyond the data so the file
  stays readable) and process identity (OpenProcess + GetProcessTimes). Liveness never uses os.kill(pid, 0): on
  Windows that is TerminateProcess.
- runtime.py: state directory %LOCALAPPDATA%\Strata\runtime with a protected one-entry ACL (existing ones must be
  unshared); owner-only checks through private_files.is_unshared; the state file's address must be a loopback port;
  the owner host is detached with CREATE_NEW_PROCESS_GROUP|CREATE_NO_WINDOW; the server runs in a kill-on-close job
  object (serve/winjob.py, terminate() added), so the whole tree ends with the host however the host ends; a graceful
  stop closes the server's stdin pipe (--owner-fd 0, serve/owner_guard.py turns the EOF into the Ctrl+C drain path);
  the deadline kill ends the job; the ownership marker records the child's creation time, so a PID that was reused
  by another process is not mistaken for the recorded child. A PID from a state file is only compared, never signalled.
- A connect failure to a lost owner is a typed ProfileError (exit 3) instead of a raw OSError.
- `strata serve` and calibration take the activity lease on Windows too.
- tests/terminal/test_runtime.py runs on Windows (32 tests were skipped): a .cmd stands in for the fake engine, the
  signal tests have Windows variants; new tests: a second client attaches from another process, wrong/stale tokens
  cannot stop the owner, a recorded PID of an unrelated live process is never touched, a reused PID with another
  creation time is not the recorded child, the liveness check does not end its target, the directory ACL, shared and
  exclusive leases.
2026-10-15 12:25:58 +02:00
Niko1221 52d58ba32e desktop: pin the Tauri CLI, fix the packaged Windows start
The shell now builds and tests on Windows. Three fixes found by running the installed NSIS build:
Tauri's resource_dir() is a verbatim path (\?\C:\...) that node.exe cannot take as its entry script, so the host died at once and the window hung; the path is made plain.
The self-test only knew the macOS Resources layout; it now finds resources next to the exe (Windows) and under lib/Strata (Linux).
A host that exits early is reported at once instead of after the 60 s health wait.
@tauri-apps/cli 2.12.3 is a pinned devDependency with a lockfile (npm ci), and the npm scripts call it instead of cargo tauri.
2026-10-15 12:14:33 +02:00
Niko1221 66bc889072 terminal archive: bundled Python for Windows x64 and Linux x86_64, hash-locked wheels, launchers, smoke test
package_terminal.py builds the terminal archive for windows-x86_64 (.zip), linux-x86_64 and darwin-arm64 (.tar.gz)
from the pinned python-build-standalone CPython 3.12.15 (SHA-256 per target, from the release's SHA256SUMS) and a
per-target lock, packaging/locks/runtime-<target>-cp312.txt, installed with pip --require-hashes --no-deps
--only-binary=:all: at BUILD time only (pip is the interpreter's own, no upgrade; the result must equal the lock).
packaging/lock_wheels.py makes a lock. Console-script launchers that name the build path are removed; a VERSION
file and the read-only gguf_reader tool join the payload.

- bin/strata.cmd and bin/strata.ps1 (Windows), bin/strata (POSIX): the bundled runtime first, PYTHONNOUSERSITE=1,
  no PYTHONHOME/PYTHONPATH; a path with spaces works.
- START-HERE.bat, SETUP.bat, UPDATE.bat, setup.sh, update.sh use a python/ folder beside them when present (no
  Python lookup or install).
- strata --version (one line: version and interpreter), strata serve --engine mock, a typed exit-5 message when the
  server's packages are missing (strata.py preflight).
- packaging/smoke_terminal.py extracts an archive in a clean env (space in the path, PATH without Python, dead proxy)
  and runs --version, doctor, profile list, install, serve --engine mock (health, models, a chat answer).
- tests/terminal/test_packaging_pins.py: digests, lock shape and agreement with requirements.txt, launcher order.
2026-10-15 12:06:29 +02:00
Niko1221 2e3085a555 start and serve never acquire: one guard, a typed error, a test that fails on egress
setup.py mixed acquisition (pip, downloads, engine fetch and compile) with configuration and start. A start of an
installed model used to check the engine version against the release and download a newer one, and moved model
files into the data folder.

- strata_control/acquisition.py: forbidden() / require() / AcquisitionRefused (exit 5, code acquisition_refused,
  names SETUP / UPDATE / `strata install`). STRATA_NO_ACQUIRE=1 forbids a whole process.
- setup.py: start() runs inside forbidden(); download, pip_install, run, every urlopen (now _urlopen), the
  engine fetch/compile and CUDA-library installs call require() first. A plain start only says when the engine is
  older than expected (engine_update_note) and no longer touches the network; UPDATE / setup keep
  update_installed_engine. A plain start reads the data folder from settings.json and creates, moves and copies
  nothing (data_folder_in_place); setup, check, calibrate and update still gather files as before.
- strata install [--update] [setup options]: the explicit installer entry; a folder without setup.py says so.
- tests/terminal/test_no_acquisition.py: the start/serve import graph reaches no installer code, no serving module
  runs pip/git/cmake/npm or opens a URL (listed exceptions are request-time or loopback), and a real start with the
  network and every subprocess mocked makes no request and runs no installer.
2026-10-15 11:52:21 +02:00
Niko1221 9587a7a1e0 bench test: kill the lease holder's whole process tree on Windows (a venv python.exe is a launcher) 2026-10-15 11:40:42 +02:00
Niko1221 874b8850e8 host tests: the reliability and tuning-route tests use the hardware seam and a Python stub engine 2026-10-15 11:25:29 +02:00
Niko1221 135430b076 host: STRATA_TEST_HARDWARE stands in for the probes in tests; the two lifecycle tests no longer depend on the memory of the PC running them 2026-10-15 11:24:25 +02:00
Niko1221 4d988cb460 host tests: native-onboarding matches serve/server.py with either path separator 2026-10-15 11:23:23 +02:00
Niko1221 44d8858747 host tests: fileURLToPath replaces the regex that stripped the slash before a drive letter (replaces 439339ab1; the pathname form also left %20 in paths with spaces) 2026-10-15 11:22:56 +02:00
Niko1221 03d9f35ed5 components test: the execute-bit assertion is POSIX-only (NTFS has none); the setuid check stays everywhere 2026-10-15 11:21:28 +02:00
Niko1221 c24c426c06 connections test: nest deeper than any Python's JSON recursion limit (3.13 parses 1100 levels) 2026-10-15 11:21:22 +02:00
Niko1221 341228c718 calibration tests: use setup.settings_path() (APPDATA on Windows) instead of an XDG-shaped path 2026-10-15 11:21:01 +02:00
Niko1221 a0bd7b25ea control: Windows leases lock a byte past the marker so another process can still read the holder (benchmark and calibration worker leases; msvcrt locks are mandatory) 2026-10-15 11:20:43 +02:00
Niko1221 18bb5b793b bench tests: no real GPU in the telemetry fixture (its first reading raced the second run), Windows ACL check for the job receipt, lease marker size on Windows; private_files.is_unshared 2026-10-15 11:19:23 +02:00
Niko1221 b0e3b5add6 tuning: peak resident memory of the engine's process tree on Windows (Toolhelp + GetProcessMemoryInfo; ps does not exist there) 2026-10-15 11:16:30 +02:00
Niko1221 f7e12d23ce tuning tests: a Python stub engine (READY/PP/T/DONE) started as [sys.executable, stub.py] replaces the .sh stubs on every OS
strata_control.tuning.engine_command runs a .py engine through the current interpreter (the --help
inventory and the measurement harness both use it).
2026-10-15 11:16:01 +02:00
Niko1221 62413d835c time-skip: a check whose program does not exist is unknown on cmd.exe too (9009, or exit 1 with no such program on PATH); a timed-out check stops the whole process tree; tests no longer need coreutils 2026-10-15 11:14:20 +02:00
Niko1221 1ee7c7337d host: one Python resolver for every spawn (env, repo venv, py -3, python.exe on Windows; python3 elsewhere)
Tuning, Time-skip, the revise command, the native MCP client, setup and
calibration jobs and the native server all go through server/python-resolver.mjs;
the Microsoft Store alias is skipped because it must start.
2026-10-15 11:13:11 +02:00
Niko1221 01f309efef host tests: the native-control fixture fakes .venv/Scripts/python.exe on Windows 2026-10-15 11:13:11 +02:00
Niko1221 decb3f3f8d host: folder picker and skill-add work on Windows (PowerShell FolderBrowserDialog) and Linux (zenity, kdialog); a typed path replaces the dialog where none exists 2026-10-15 11:11:49 +02:00
Niko1221 8a11d125db desktop: Windows node.exe in the Tauri shell, hidden host console, and fetch-node.ps1 (win-x64, same SHA-256 pin as fetch-node.sh)
node_binary() looked for 'node' only. Not compiled here: no Rust toolchain on this PC.
2026-10-15 11:10:00 +02:00
Niko1221 f549c31d1d host: the API-key file is owner-only on Windows (a protected ACL for the current user), and the test asserts the effective ACL 2026-10-15 11:08:58 +02:00
Niko1221 5f051b83d0 control: owner-only files on NTFS (a one-entry protected ACL via ctypes) and the owner state-file check uses it on Windows
POSIX keeps 0600/0700. The address-stability test is skipped on Windows (the
placeholder address is constant by design; the listener picks the port).
2026-10-15 11:08:26 +02:00
Niko1221 515e045faf packaging: monitor rollback works on Windows (os.rename cannot replace the mkdtemp directory) 2026-10-15 11:05:28 +02:00
Niko1221 d442b0307f packaging: the monitor bundle's LICENSE check accepts CRLF; LICENSE is eol=lf in .gitattributes
A Windows checkout (autocrlf) has CRLF in LICENSE, and the bundle verify
required the file to start with b'MIT License\n'. Normalize line endings
before the check, pin the file to LF, and test a CRLF member.
2026-10-15 11:05:24 +02:00
Niko1221 439339ab14 apps/host tests: Windows file URLs in the esbuild entry points
14 test files took the path of `new URL(rel, import.meta.url).pathname`, which on
Windows is "/C:/..." and cannot be resolved (esbuild "Could not resolve"). Strip the
leading slash before the drive letter. No behavior change on macOS/Linux. Went from 14
whole-file failures to 6 real test failures (listed in the S0 notes). Easy to drop if the
owner would rather have a different fix.

Follow-up to the owner's rebuild branch strata/rebuild-2026-10 (small, test-only).
2026-10-15 10:58:10 +02:00
Niko1221 5389eb9db8 kv_hybrid_parity: the tensor-core prompt kernel is sm_70+ (base launch gate), so a refusal below sm_70 passes 2026-10-15 10:47:40 +02:00
Niko1221 31a3d5553e repo: restore the executable bit on the terminal and desktop scripts
The files were copied through Windows, which does not carry the mode; the modes are
as in the owner's rebuild branch strata/rebuild-2026-10.
2026-10-15 10:24:10 +02:00
Niko1221 e424cc940d docs/work: Phase A implementation status, qualification kit and evidence text
IMPLEMENTATION_STATUS.md, ACCEPTANCE-EVIDENCE.md, QUALIFICATION-KIT.md and the
evidence logs (text only, about 140 KB). The 170 MB of screenshots under
apps/host/docs/evidence and checkpoints are not in git; they are in
StrataSplit\evidence-archive. The Electron mentions in IMPLEMENTATION_STATUS.md are
history (Electron was replaced by Tauri); the superseded top-level desktop/ folder is
not carried over.

From the owner's rebuild branch strata/rebuild-2026-10.
2026-10-15 10:24:09 +02:00
Niko1221 cef6027dcb packaging: terminal distribution, portable monitor bundle and the acceptance matrix
packaging/package_terminal.py builds the self-contained terminal archive with its own
Python runtime, monitor_bundle.py the portable monitor, acceptance_matrix.py maps the 67
acceptance rows to evidence, README.md documents the target matrix. The built archive
(release/artifacts/*.tar.gz, 42 MB) is not in git.

From the owner's rebuild branch strata/rebuild-2026-10.
2026-10-15 10:24:09 +02:00
Niko1221 48fc04a425 tuning: job contract, history, apply/rollback and the real-API measurement harness
strata_control/tuning.py (the job contract and the HyperCore-Tune decision), tune_engine
(measures through the real API and records memory), tune_history, tune_apply (explicit,
reversible apply and rollback of a recommendation) and tune_api, with tests/tuning
against a stub engine. Review-only: nothing here starts a sweep from the UI, and the
sweep has the known gaps listed in the S0 notes (e.g. --kv-grow 0 / --expert-cache 0
command lines).

From the owner's rebuild branch strata/rebuild-2026-10.
2026-10-15 10:24:09 +02:00
Niko1221 486120787d timeskip: strata_control/time_skip.py and tests/time_skip
The Time-skip state machine: checkpoint, revise (a bounded Harness turn), check and
decide, with a durable job record, cancellation, and the "inconclusive means ask the
person" rule. The host routes are in apps/host; the CLI entry points are in strata_cli.

From the owner's rebuild branch strata/rebuild-2026-10.
2026-10-15 10:24:09 +02:00
Niko1221 090e45adf1 integrations/harness: the Strata profile and the agent bridge
A Harness profile for Strata, a bridge that drives a Harness agent session against a
Strata server (_bridge.py, bridge_e2e.py, bridge_tests.py), the tool-execution /
approval / cancel end-to-end test (tool_flow_e2e.py) and the bounded revision turn the
Time-skip job uses (revise_once.py). Tested only against a scripted mock worker; the
Harness is not wired into the app's chat yet.

From the owner's rebuild branch strata/rebuild-2026-10.
2026-10-15 10:24:09 +02:00
Niko1221 48ae834b58 apps/desktop: Tauri 2 shell for the Strata desktop app
The Tauri shell (src-tauri Rust, 400-odd lines) that starts the Node App Host and
opens the owner's UI, with the macOS entitlements, icons and generated schemas, and
the build scripts in apps/desktop/scripts that package the app around a private Node
sidecar (digest-verified fetch). The Node binary, Rust target/ and DMGs are not in git;
src-tauri/resources/node/ keeps only its .gitignore placeholder.

Windows and Linux packaging is not working yet (the sidecar lookup expects "node" and the
scripts are bash-only); see the S0 notes.

From the owner's rebuild branch strata/rebuild-2026-10.
2026-10-15 10:24:09 +02:00
Niko1221 0b3299a019 apps/host: the owner's UI and the Node App Host
The UI source from the 14 October handoff (src/, public/, index.html, vite and
tsconfig, tests, audit-fixes, design docs) and the App Host (server/*.mjs): model
import and monitor routes, API-provider catalog and streaming (OpenAI chat, OpenAI Responses,
the Messages API), serving, tunnel, tasks, plus the host routes for Time-skip
(server/timeskip.mjs) and the tuning plan/reports. src/lib/time-skip.ts renders a real
job's own record.

The UI design is unchanged. The rebuild routed #/tuning to a new TunePanel; that is
reverted: src/screens/ModelRun.tsx and src/main.tsx are the handoff's again (#/tuning
goes where the handoff sends it), and src/components/TunePanel.tsx and
src/styles/tuning.css, which only that route used, are not carried over. Left out of
git on purpose: dist/ (build output), node_modules, and docs/evidence + docs/checkpoints
(170 MB of screenshots, kept in StrataSplit\evidence-archive).

From the owner's rebuild branch strata/rebuild-2026-10.
2026-10-15 10:24:09 +02:00
Niko1221 a242537838 setup: split build helpers (cmake, ninja) out of the runtime requirements
requirements.txt keeps the runtime packages only; requirements-build.txt pins
cmake and ninja. setup.cmake_build installs only the helper that is missing, right
before a local engine or image-encoder build, so the ready-made engine path (and
serving) never runs pip for build tools. ram_gb() reads hw.memsize on macOS and no
longer crashes without /proc/meminfo. docs/INSTALL.md notes the split; two tests in
tools/test_setup_pins.py cover it.

From the owner's rebuild branch strata/rebuild-2026-10. setup.py merged onto 0.1.45
without conflicts (0.1.45's MIN_ENGINE and pins are unchanged).
2026-10-15 10:23:23 +02:00
Niko1221 c968d37a48 serve: owned-server contract for the terminal (ready file, owner guard, launch identity, queue deadline, stage telemetry)
New serve/ready.py, serve/owner_guard.py and serve/launch_identity.py; server.py
gains the queue wait deadline (QueueWaitExpired/QueueCancelled), the opt-in
STRATA_BATCH_TELEMETRY plumbing (BSTAT/PSTAT parsed off the engine's token stream,
dropped from request output), the engine stage-plan reader, a main(argv, ...)
entry the terminal can call, and per-replica selection hooks; telemetry.py reports
GPU names; the tests for all of it.

server.py merged by hand onto 0.1.45: one conflict, in StrataEngine._take_control, where
0.1.45's STRATA_ADMIT_SHORT_FIRST ordering and the rebuild's queue deadline both edit the
acquire step; both are kept (ordering check first, then the deadline-bounded acquire).
Every line 0.1.45 added to server.py (the #2518 pick_slot fix, the havok multi-user
switches, the Qwen3.6 parallel/vision work) is still present.

From the owner's rebuild branch strata/rebuild-2026-10.
2026-10-15 10:23:23 +02:00
Niko1221 79d8ea066a engine: opt-in batch telemetry (STRATA_BATCH_TELEMETRY=1 prints BSTAT/PSTAT lines)
include/strata/core/batch_telemetry.hpp (BatchWindowSampler, StageInFlightSampler),
its unit test, and the hooks in the generate.cpp batch loops and the --batch-groups
pipelined loop. Off unless STRATA_BATCH_TELEMETRY is exactly "1": no new output, no
new state beyond two disabled samplers, so the default output is unchanged.
Host-side C++ only (no CUDA/HIP/SYCL code), so it builds on every backend.

From the owner's rebuild branch strata/rebuild-2026-10. generate.cpp and
CMakeLists.txt merged onto 0.1.45 without conflicts.
2026-10-15 10:23:23 +02:00
Niko1221 64a91dcdc0 terminal: strata_cli, strata_control, bin/strata and the terminal tests
The command-line and TUI client with profiles, doctor, monitor, serve, calibrate,
bench, the runtime owner, component registry and offline policy, plus tests/terminal,
the component/offline tests, the smoke tools, the terminal CI workflows and the
terminal docs. Tuning and Time-skip modules of strata_control follow in their own
commits (strata_cli/cli.py imports them lazily).

From the owner's rebuild branch strata/rebuild-2026-10 (1e4171e2, based on 296b59d2).
README.md and tools/calibrate.py applied as small 3-way patches.
2026-10-15 10:23:23 +02:00
Niko1221 85b75dcbea repo: .gitignore entries for the terminal/desktop split
Keep node_modules, Tauri target/, DMGs, the Node sidecar, release archives, the
built App Host dist/ and the Mac evidence screenshots out of git. The two
.terminal-*venv entries are from the owner's rebuild branch strata/rebuild-2026-10;
the rest are new for the clean integration branch.
2026-10-15 10:19:47 +02:00
Niko1221 456ec79c0a docs/NVIDIA_V100.md: STRATA_GDN_REC_WARP (#1098) 2026-10-15 09:49:14 +02:00
Niko1221 4ef3855745 gdn_chunk_parity: skip (77) on a card whose shared-memory opt-in cannot hold the kkt stage (Pascal), instead of failing 2026-10-15 09:48:05 +02:00
Niko1221 dea9b6de5d gdn chunk / Volta recurrence: name the sm_70-only kernels only in the host pass and the sm_70 device pass (a 60;70 or 70;86 build failed in the other device passes) 2026-10-15 08:55:08 +02:00
Niko1221 036d473a3c gdn: the Volta chain-split recurrence is opt-in (STRATA_GDN_REC_WARP=2), not the sm_70 default: it changes the summation order 2026-10-15 08:40:13 +02:00
Niko1221 c0d02ba96a V100 fusions (#1097) behind STRATA_FUSE_DECODE=1; pf_wmma opt-in and only when every visible card is cc 7+; WMMA path skips NVFP4 layers; docs 2026-10-15 08:39:46 +02:00
Niko1221 a6bdc3fb5b pf_wmma: compile the WMMA kernel bodies only for sm_70+ device passes (a multi-arch 60;70 build failed in the sm_60 pass: no nvcuda::wmma there) 2026-10-14 07:14:39 +02:00
ruibeikaa a87fb80e13 sm_60: stage the IQ3 tables by default on compute capability 6.0 only
The staged IQ3_XXS / IQ3_S tables trade a per-block copy and barrier for
cheaper lookups, which pays only where the grouped expert kernels are
decode-bound. On GP100 (6.0: no __dp4a, 70-140 GB/s) it does; on an RTX 3090
Ti (sm_86), forced on with STRATA_IQ_STAGE_TABLES=1, the same real Swift 1.5
IQ3_S layers (20 experts, 24 entries) took 3-13% longer (71-76 -> 75-84 us;
those kernels read at 490-665 GB/s there) and IQ3_XXS was unchanged, the
output bitwise the same. 6.1 cards (P40, GTX 10) have __dp4a and are
unmeasured, so the default now asks for 6.0 rather than any 6.x;
STRATA_IQ_STAGE_TABLES=1 still turns it on anywhere.

docs/OLDER_GPUS.md says so, and that neither STRATA_Q8_SM60 nor the tables
is measured on 6.1: both build for sm_61, and tools/q8_sm60_check.cu passes
on an RTX 3090 Ti, where the kernel's dot is the hardware __dp4a.
2026-10-14 07:09:00 +02:00
ruibeikaa ac35eebf30 sm_60: stage IQ3_S's codebook and sign masks in shared memory too
IQ3_S was the one i-quant gate/up format whose codebook the grouped expert
kernels still read from global memory (IQ2_XXS / IQ2_XS / IQ2_S stage theirs
everywhere, IQ3_XXS on 6.x since the first commit of this branch). Its 512
codebook words are now staged per block with two 256-word tables of byte
masks - IQ3_S keeps one explicit sign byte per 8 values, so the tables hold
the masks of a sign byte's bits 0-3 and 4-7 - applied as
(g ^ mask) + (mask & 0x01010101): every iq3s_grid byte is odd, so a negated
byte never carries into the next. Bitwise the same output (iq_multi_parity
and native_grouped_parity on a GP100 die, with and without
STRATA_IQ_STAGE_GRID_MMVQ=1).

One GP100 die, a verify window's layer of real Swift 1.5 IQ3_S experts
(20 experts, 24 entries): 517 -> 393 us with Q2_0 down, 492 -> 369 us with
IQ4_NL down. Swift 1.5 IQ3_XXS (13 of 48 layers IQ3_S), four GP100 dies,
1M context, decode tok/s, medians of three interleaved engine starts:
36.0 -> 37.2 (10.7K document), 36.3 -> 37.4 (cached), 43.1 -> 44.5
(reasoning), 41.6 -> 43.0 (code), with the same drafts accepted.

The switch covers both formats and is renamed accordingly:
stage_tables_on(), STRATA_IQ_STAGE_TABLES=1|0 (was STRATA_IQ_STAGE_GRID18).
Still the default on compute capability 6.x only.
2026-10-14 07:08:59 +02:00
ruibeikaa 080abf86a4 tools/q8_dense_gguf.py: convert every shard that holds dense matrices
The tool converted only the shard it was given and hard-linked the others, which is enough for Flash-Next (every
natively served projection and the head are in shard 1) but not for Swift 1.5 IQ3_XXS, which keeps 78 of them in
shard 1 and 223 in shard 2: one run left three quarters of them in their K- / i-quants, silently, and the kernel
only runs Q8_0 matrices. Now any shard of the model may be named; every shard that holds such a matrix is written
as before (same tensor-info length, Q8_0 blocks appended, offsets checked) and the others are hard-linked.

Checked: Swift 1.5 IQ3_XXS in one run gives both shards byte-identical to two single-shard runs (301 tensors);
Flash-Next IQ3_XXS converts 301 tensors in shard 1 and links shard 2.
2026-10-14 07:08:59 +02:00
Niko1221andruibeikaa 6e29a5d314 sm_60: a Q8_0 decode GEMV for Pascal GP100 (STRATA_Q8_SM60=1, opt-in)
GP100 has no __dp4a, and on a GP100 die the verify window's dense projections read their weights at 40-80 GB/s
of ~730. With STRATA_Q8_SM60=1 every Q8_0 dense matrix (any shape) and the head get the packed planes of
STRATA_Q8_PACKED and run src/kernels/cuda/q8_sm60.cuh: a warp (half / quarter warp for narrow rows) per row,
16-byte weight loads through the read-only cache, the window's q8_1 activations copied once per workgroup into
shared memory as an int8 plane plus float scales, STRATA_DP4A (vmad on sm_60), a shuffle sum with no barrier per
row, persistent workgroups (4 per SM, STRATA_Q8_SM60_BPS). Shapes it declines (> 48 KiB of shared memory: a
6144-wide row with 7-8 columns) keep the usual kernel. Not bitwise the exact kernels (summation order); the
packed copies cost their size in VRAM (Flash-Next: 2.9 GiB of dense projections over the stages, 0.6 GiB head).

The GSQ-RCO packs have no Q8_0 dense matrices: tools/q8_dense_gguf.py writes a shard 1 whose natively served
projections and head are Q8_0, keeping the tensor-info block's length so every expert keeps its offset (the native
pack is reused) and hard-linking the other shards beside it.

PH402 SKU 200 (four GP100 dies), clocks locked at 1050 MHz, Flash-Next IQ3_XXS, 32K, layer split 12,24,36,
--prefill 2048, greedy, decode tok/s (medians, interleaved starts): GSQ-RCO shard 28.3, Q8_0 shard with
STRATA_Q8_PACKED 31.1, Q8_0 shard with STRATA_Q8_SM60 38.9 (+37%); per window dense 19.2 -> 11.2 ms, head
4.25 -> 2.1 ms; prompt reads unchanged. tools/q8_sm60_check.cu: 57 shape x column cases against a CPU reference,
max relative error < 1e-5. docs/OLDER_GPUS.md has the table, the conditions and how to use it.

Co-authored-by: ruibeikaa <[email protected]>
2026-10-14 07:08:58 +02:00
ruibeikaa 96cb96aced sm_60: stage IQ3_XXS's codebook and sign masks in shared memory (grouped expert kernels)
Ported from shinbunbun/llama-cpp-p100-patches (MIT), patches 29 `mmvq-iq3xxs-grid-smem` and 30
`mmvq-ksigns-smem`: the grouped expert kernels stage the 256-word iq3xxs_grid and two 128-word tables of per-byte
sign masks once per block, and apply a mask as (g ^ mask) + (mask & 0x01010101) instead of the per-lookup
unpack_ksigns / __vcmpne4 / __vsub4 chain (iq3xxs_grid's bytes are all >= 4, so no carry crosses a byte).

Bitwise the same output: checked end to end on a PH402 (GP100), greedy, three prompts x three interleaved rounds,
identical replies with and without. Measured only on GP100, where it is within noise (+0-4% decode), so it is the
default on compute capability 6.x only; STRATA_IQ_STAGE_GRID18=1 / 0 forces it on / off on any card.
2026-10-14 07:08:24 +02:00
Warabii1248 1327c0916c fused_gr: say in the comment that CUDA opts in to the register split norm 2026-10-14 07:08:24 +02:00
Warabii1248 fec7d1cae2 fused_gr: STRATA_GR_NORM_FAST=1 on CUDA too (opt-in; the register split norm is bitwise the default; decode +0.5-0.6% on a V100 + PG500 split) 2026-10-14 07:08:23 +02:00
Warabii1248 ab7c977216 verify: say in the comments that sm_7x takes argmax_rows with STRATA_ARGMAX_ROWS_SM70=1 2026-10-14 07:08:23 +02:00
Warabii1248 83081577e5 sm_70: the multi-block greedy pick (argmax_rows) as an opt-in, STRATA_ARGMAX_ROWS_SM70=1 (bitwise the one-block pick; decode +0.7-0.8% on a V100 + PG500 split) 2026-10-14 07:08:22 +02:00
Warabii1248 ab5ea838de pf_wmma: say in the comment where the WMMA2 tile layout comes from 2026-10-14 07:08:12 +02:00
Warabii1248 dca6a6532f pf_wmma: STRATA_PF_WMMA2=1 (opt-in), a second Volta prompt-expert kernel for IQ2_S gate/up and IQ4_NL down - the dequantization spread over the whole block from a raw stage in shared memory (bitwise dq_iq2_s / dq_iq4_nl; prompt +2.5-4.3% with the IQ3_XXS pack on a V100 + PG500 split) 2026-10-14 07:08:12 +02:00
Warabii1248 4dc3206402 pf_wmma: set the tiles shared-memory attribute on each card, not once per process (a layer split stopped on its second card: "the WMMA expert tiles could not launch") 2026-10-14 07:08:11 +02:00
Warabii1248 caa3c8425a pf_wmma_parity: run on every sm_70+ card in one process, in turn (a layer split launches the tiles on its second card after the first) 2026-10-14 07:08:11 +02:00
g0 2d9b1b348a pf_wmma: take the FP16 prompt path for IQ4_NL down too (mmq_dt == 20) 2026-10-14 07:08:10 +02:00
g0 cb73a0538a pf_wmma: IQ4_NL (type 20) WK=128 lane for the down product (K=640); parity cases 2026-10-14 07:08:10 +02:00
NoxestNoz 140f011030 bench/results: V100 prompt experts on FP16 tensor cores (+9.6% / +10.3% prompt speed); docs/NVIDIA_V100.md 2026-10-14 07:08:09 +02:00
NoxestNoz fb433c3a57 pf_wmma_parity: gemm_iq_f16_grouped against a double reference over the FP16 path's weights 2026-10-14 07:08:09 +02:00
Niko1221 cb2740a8c2 prefill: Volta runs the prompt experts on FP16 tensor-core tiles, a group per launch (STRATA_PF_WMMA)
gemm_iq_f16_grouped: one launch per MMQ expert group, a block = 128 weight rows of one
expert x up to 256 of its token rows, each weight superblock dequantized once into shared
memory by dq_dispatch (the FP16 path's formulas), FP16 activations, WMMA with FP32 sums,
the next activation tile prefetched into registers.  sm_70 defaults to it for the layers
MMQ takes and for IQ1_M (which rides the MMQ grouping); STRATA_PF_WMMA=0 keeps MMQ.

Co-authored-by: NoxestNoz <[email protected]>
(merge-resolved onto 0.1.44.1: the MMQ gather now zeroes the group tail itself and the quantizer takes a scale buffer)
2026-10-14 07:08:08 +02:00
ATIVX928 f1a9eaa47e gdn chunk: the smem opt-in and the workspace are per device (layer-split fix)
justxiami reported (#1098) a 4xV100 layer split dying with
STRATA_GDN_CHUNK=1.  Reproduced on 2xV100: the second card launched the
chunked kernels at the 48 KB default (a process-global std::call_once set
the opt-in on the first card only) and the engine exited with
"gdn_chunk wmma: invalid argument".

Two more per-process assumptions in the same path:
- gdn_chunk_cc() cached the first card's capability for the whole process
- the chunked workspace was thread_local with no destructor: the per-request
  stage threads leaked ~50 MB per card per request (the second card ran out
  of memory on the next prompt) and one card's pointers could reach another

Now: the opt-in and the capability are recorded per device (each attempt
clears the runtime error, and a refused wmma opt-in falls back to the FMA
path), and the workspace is one per (thread, device) with a destructor.
Verified on 2xV100 32K: STRATA_GDN_CHUNK=1/2 run to completion over repeated
rounds (1: 1900 tok/s prefill, 2: 1766), gdn_chunk_parity 0 failures.
2026-10-14 07:05:19 +02:00
ATIVX928 fd13adf248 bench (v100): gdn_chunk_bench - the GDN prompt recurrence variants and the chunked path, real shapes
CUDA events over one gdn_recurrence call per side (S=128, 16 q/k heads, 48 v heads, C=10240,
T=1024/4096/8192), all four paths and the max abs diff between the chunked wmma output and the
pipelined recurrence.  RESULTS.md records the table and the error bounds for
bench/results/2026-10-02-gdn-chunk (measured on the V100-SXM2-16GB, CUDA 12.8, sm_70 build).
2026-10-14 07:05:18 +02:00
ATIVX928 c0a4a9e657 tests (v100): gdn_chunk_parity - the chunked and Volta GDN recurrences vs a double-precision host reference
GPU, synthetic, no model.  Compares through the real dispatch (STRATA_GDN_CHUNK /
STRATA_GDN_REC_WARP A/B) at T=64/128/200/320 - 200 exercises the < 64-token tail - and asserts
measured bounds, not equality: the recurrence is the FP32 floor (y abs 1.1e-6, state 3.0e-8), the
chain-split and the chunked FMA path are FP32-level (~3e-6), the wmma chunked path is the
f32->f16 staging level (y abs 2.3e-3, state 8.9e-5).  The sync-free warp recurrence is asserted
BIT-EXACT against gdn_rec_cols_pipe (memcmp of y and the final state).  A NaN or inf anywhere
fails the check outright - an unordered comparison would otherwise leave the max at zero.

On main's 0.1.38 recurrence the out norm keeps its raw FP32 output in place and writes only the
FP16 copy, so the kernel comparisons use that raw output (ref_full's yraw_out) and the norm is
checked once through its FP16 store.
2026-10-14 07:05:18 +02:00
ATIVX928 7c4ff2de72 prefill (v100): the chunked GDN prompt recurrence (FlashQLA gdn-chunk) + Volta recurrence variants
The prompt GDN recurrence is a per-token chain; on a V100 that chain is what the 36 GDN layers
cost.  This adds the chunked form (cumsum / kkt / fwd over 64-token chunks, chunk-serial scan,
the state in registers - the gdn-chunk-sm70 structure of llama.cpp-v100, hand-written CUDA):
a chunk is a triangular solve and a batch of matmuls instead of 64 sequential rank-1 steps.
Two variants: an FP32-only FMA scan and an m16n16k16 wmma one with the reference's f32->f16
staging.  Both are FP32-level, NOT bit-exact (the sums are reordered; the wmma path stages
q/k/v and the state through f16) - gdn_chunk_parity bounds them.

Also two Volta variants of the recurrence itself: a sync-free warp-shuffle reduction (BIT-EXACT
vs gdn_rec_cols_pipe, but measured 2.6x slower - kept as the bitwise control) and a 4-accumulator
chain split of the pipelined kernel (FP32-level, measured +6-7% at T=4096/8192, the V100 default
now).  All of this is compiled only into the STRATA_EXPERIMENTAL_SM60 build and dispatched only
at cc==7.0; sm_80 and up keep the trunk's kernel bit-for-bit.  The env switches
(STRATA_GDN_CHUNK=1/2 chunked, STRATA_GDN_REC_WARP=0/1/2) still override.

Measured on the V100 (bench/gdn_chunk_bench, real shapes, one gdn_recurrence call): the chunked
path is correct but 4-6x SLOWER than the recurrence as implemented (4-warp CTAs underfeed the
tensor cores - see bench/results/2026-10-02-gdn-chunk/RESULTS.md), so it stays opt-in.
2026-10-14 07:05:18 +02:00
ATIVX928 d284809fb0 V100: operator fusions for the decode GEMV / MoE paths, bitwise
Fusions (all bit-exact vs the pipelines they replace; bench/micro/fusions_parity.cu
checks every one bitwise and times the before/after at decode shapes):

- gate/up dual GEMV + SwiGLU in ONE kernel (llama.cpp's mul_mat_gated):
  s_gemv_pair_silu, one read of the activation for both projections and the
  SwiGLU in the epilogue - the up intermediate never lands in memory.  Rows are
  accumulated by the shared row functions (s_rowdev.cuh) exactly as the
  standalone kernels accumulate them.  shared_expert uses it whenever both
  projections are S-family (bitwise checked for all three silu kinds).
- silu(gate)*up + the intermediate quantizer fused (was 2 kernels):
  swilu_quantize_q8_0/_scaled/_q8_K, warp-parallel swilu then the verbatim
  quantizer body.  Wired into the S2 expert entry points and shared_expert.
- every activation image of x in one pass (was 2-3 kernels): quantize_act_images
  produces block_q8_0 + block_q8_K + bf16 from one read of x.  Wired into
  gdn_layer, qsa_layer and moe_shared.
- multi-weight bf16 GEMV (llama.cpp's mul_mat_multi): bf16_gemv_pair computes
  two matrices from one read of x; used for ssm_alpha/ssm_beta and for the
  indexer's q/k projections (q and k now one launch).

The call sites are gated on STRATA_EXPERIMENTAL_SM60 (the Pascal/Volta build),
so the ready-made engine runs exactly the pipelines it did before; the fused
kernels are the same code either way.

Measured (V100, CUDA events, n_embd 2560 / FF 640, 200 reps):
  expert intermediate (swilu+q8_0)   15.7 -> 11.6 us  M=1,  18.8 -> 11.4  M=8
  activation images (q8_0+q8_K+bf16) 46.2 -> 30.4 us
  dual GEMV + swilu (gate,up)        30.4 -> 14.8 us
  bf16 pair (alpha,beta)             24.2 -> 14.4 us  M=1,  23.7 -> 14.1  M=8

Parity: fusions_parity 0 failures (bitwise), plus s_gemv/s_gemv_q8k/s2_gemv_q8/
s2_expert_grouped/shared_expert/quantize_act/bf16_gemv/elementwise/mmvq_multi/
iq_multi/native_expert parity suites all pass unchanged.
2026-10-14 07:05:13 +02:00
ATIVX928 1e14e6469e kernels (sm_70): the prompt attention's latency pipeline (mma884 kernel)
Port of oldlocal commit 4672cf9 ("kernels (sm_70): the prompt attention's
latency pipeline - 2.2x over the FP32 kernel at 32K") onto the #600
prompt_attn_v70_kernel, the m8n8k4 Volta kernel now inline in
qsa_prompt_attn.cu.  Still gated on STRATA_EXPERIMENTAL_SM60, so the
ready-made engine keeps exactly the code it had.

Two of the commit's three changes port; the third (splitting the dependent
WMMA chains into f0/f1 and fh/fl) is already inherent to the mma884 kernel -
its scores run two tg[rb] chains and its p.v four tmp[rb][nt] chains - so
porting it would only change the summation order.  The two ported, measured
on a V100-SXM2-16GB (CUDA 12.8):

* the staging loop issues every global load of a thread before any of its
  stores (2-4 in flight), instead of paying the HBM latency per piece;
* the next chunk's ids/page-table lookups double-buffer (row[2][CH]) and run
  just before the previous p.v, where their latency hides.

The original commit measured, on the #371 kernel, 2,048 queries of a 32K
selection: 34.4 -> 15.8 ms (2.18x) int8, 33.5 -> 15.4 ms (2.17x) fp16 KV,
35.6 -> 20.3 ms (1.76x) K8V4 (its q4_0 V dequantizes to FP16 at staging, the
sm_75/80 kernel's same trade).

This port, same 2,048 queries of a 32K selection (qsa_prompt_attn_sm70_bench,
which also gained the K8V4 pools): the Volta kernel goes int8 12.11 -> 10.25 ms,
fp16 KV 25.70 -> 17.39 ms, K8V4 25.71 -> 24.12 ms, so against the FP32 decode
kernel int8 rises 1.73x -> 2.05x and fp16 0.80x -> 1.18x (K8V4, 0.85x -> 0.91x,
keeps main's #600 dequant-to-FP16 trade).

qsa_prompt_attn_parity passes all four cases.  An A/B against the unpatched
kernel shows the kernel's output is unchanged - every FP64 and new-vs-old
figure is identical before and after (int8 5.08e-6, fp16 5.29e-6 vs FP64,
within the 4x gate).
2026-10-14 07:05:12 +02:00
ATIVX928 132faae7cf kv_q8: the gather decodes 8 int8 per thread (bitwise, +11% on the 32k microbench)
The experimental build (-DSTRATA_EXPERIMENTAL_SM60=ON, Pascal/Volta) gains a
kv_gather_q8_wide_kernel: one uint2 (8 B) load of codes, one uint4 (16 B) store of
the dequantized fp16 and one scale per thread instead of two.  The 8 values lie in
one 64-value group and the arithmetic is unchanged ((float) code * scale ->
f16_from_f32), so the output is byte-identical.  The ready-made engine keeps exactly
the 4-wide kv_gather_q8_kernel and launches it unchanged, so its gather is bit for
bit the release engine's.  On a device below sm_75 the wide kernel is selected at
run time inside the experimental build (STRATA_Q8_GATHER_WIDE=0 opts out).

Measured on 2x Tesla V100-SXM2-16GB (SM 7.0), CUDA 12.8, one card: +11.4% on the
32k gather microbench (15.85 -> 14.05 us/call, 401 -> 452 GB/s).  kv_q8_parity
stays OK at worst INT8-vs-FP16 error 0.590 quantization steps.
2026-10-14 07:05:04 +02:00
ATIVX928 6a5be385c6 audit: where the int8 KV read decodes, and whether it is vectorized 2026-10-14 07:05:04 +02:00
1422 changed files with 239716 additions and 3758 deletions

No files matched your search

+15
View File
@@ -4,9 +4,24 @@ bench/prompts/** -text
# launchers keep their platform line endings wherever the repository is cloned
*.sh text eol=lf
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
bin/strata text eol=lf
# copies of real packs' native_experts.txt, parsed byte for byte by tests/core/expert_layout_test.cpp
tests/data/native_experts/** -text
# the quant yardstick's text set: tokenized byte for byte (set.ids.json), never convert its line endings
data/quant_quality/** -text
# the license text is shipped and checked as LF text on every checkout
LICENSE text eol=lf
# container scripts run in Linux: always LF
docker/*.sh text eol=lf
Dockerfile text eol=lf
docker/Dockerfile text eol=lf
docker/compose/* text eol=lf
# Apple tools (codesign, plutil) read these: LF only, wherever the repository is cloned
*.plist text eol=lf
+26
View File
@@ -0,0 +1,26 @@
name: Benchmark
description: Share measurements from your machine, or report a problem with the benchmark tools
title: "[Benchmark] "
labels: ["benchmark"]
body:
- type: markdown
value: |
The title must start with **[Benchmark]**. Keep it, and add the GPU and model after it.
To share a full report, follow the [community benchmark guide](https://github.com/Niko1221/Strata/blob/main/docs/COMMUNITY_BENCHMARKS.md) and open a results-only pull request. The standard protocol is a [draft](https://github.com/Niko1221/Strata/blob/main/docs/BENCHMARK_PROTOCOL.md); existing tools do not implement it yet. Include poor results and failed requests.
- type: textarea
id: what
attributes:
label: What did you measure, or what went wrong
description: Hardware (GPU and VRAM, CPU, RAM, OS), Strata version, model and quantization, the exact command, and the numbers. Paste the report or link the pull request.
validations:
required: true
- type: dropdown
id: reproduction-status
attributes:
label: Reproduction status
options:
- Measured locally
- Unconfirmed (I did not run it)
validations:
required: true
+74 -10
View File
@@ -1,37 +1,101 @@
name: Bug report
description: Something in Strata doesn't work as documented
title: "[Bug]: "
title: "[Area] "
labels: ["bug"]
body:
- type: markdown
value: |
Thanks for the report.
Thanks for the report. Please read [CONTRIBUTING.md](https://github.com/Niko1221/Strata/blob/main/CONTRIBUTING.md) first.
Please search the [open issues](https://github.com/Niko1221/Strata/issues?q=is%3Aissue+state%3Aopen) first — this may already be tracked.
Search the [open and closed issues](https://github.com/Niko1221/Strata/issues?q=is%3Aissue) first: this may already be tracked, and new evidence belongs in that thread.
Write only what you saw. Do not guess at steps, errors, hardware or severity.
**The title must start with its area:** [Engine], [GUI], [TUI], [Benchmark], [Docs] or [Other]. Choose the same one below.
- type: dropdown
id: area
attributes:
label: Area
description: Start the issue title with the same area in square brackets, for example "[Engine] decode stops after 2 minutes". Pick Other if none fits.
options:
- "[Engine] (speed, kernels, models, the server and API)"
- "[GUI] (the desktop app)"
- "[TUI] (the Terminal: strata launch, connect, monitor)"
- "[Benchmark] (measurements and reports)"
- "[Docs] (documentation)"
- "[Other] (anything else, including setup and installation)"
validations:
required: true
- type: dropdown
id: how
attributes:
label: Desktop app, Terminal or source?
options:
- Desktop app (Windows, Linux or Mac)
- Terminal (the `strata` command)
- Source (START-HERE.bat, setup.sh or a build)
- Docker
- Other
validations:
required: true
- type: textarea
id: what
attributes:
label: What happened
description: What you did, what you got, and what you expected instead. Paste the exact error text if there is one. If there's a set of steps or commands that reproduces it, put them here too.
description: What you did, what you got, and what you expected instead. Paste the exact error text if there is one.
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: Smallest steps or command that reproduce it
description: The shortest list of steps that shows the problem. If you cannot make it happen again, say so here.
validations:
required: false
- type: dropdown
id: reproduction-status
attributes:
label: Reproduction status
options:
- Reproduced locally
- Unconfirmed static analysis (I read code or ran a scanner; I did not see it fail)
validations:
required: true
- type: input
id: version
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.1 · engine 0.1.46"
validations:
required: true
- type: input
id: hardware
attributes:
label: Strata version, GPU, OS
description: The engine version is on the About tab, or in `GET /v1/status` under `engine`.
placeholder: "0.1.31 · RTX 5070 12GB · Windows 11"
label: Graphics card, memory and system
description: The card and its VRAM, the RAM of the PC, and the operating system.
placeholder: "RTX 5070 12 GB · 64 GB RAM · Windows 11"
validations:
required: true
- type: input
id: model
attributes:
label: Model, size and settings
description: The model and size you started, the context, and whether pictures were on. Write what is relevant.
placeholder: "Qwen3.8-Flash-Next IQ2_XS · 128K context · pictures on"
validations:
required: false
- type: textarea
id: log
attributes:
label: Engine log
description: Drag in `strata-<model>.log` from your Strata folder. Drag and drop works directly into this box.
label: Logs
description: |
The app: Settings > Engine > Diagnostics > View logs > Save logs, then drag the file in. The Terminal: paste the output of `strata doctor`. From source: drag in `strata-<model>.log` from your Strata folder.
Remove API keys, passwords and private text first.
validations:
required: false
- type: markdown
value: |
A couple of extras that help a lot:
- Check the [troubleshooting table](https://github.com/Niko1221/Strata/blob/main/docs/DETAILS.md#troubleshooting) first, many reports are already solved there.
- Check the [troubleshooting page](https://github.com/Niko1221/Strata/blob/main/docs/TROUBLESHOOTING.md) first, many reports are already solved there.
+1 -1
View File
@@ -1,5 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: Troubleshooting and known issues
url: https://github.com/Niko1221/Strata/blob/main/docs/DETAILS.md#troubleshooting
url: https://github.com/Niko1221/Strata/blob/main/docs/TROUBLESHOOTING.md
about: Symptom-to-fix table for crashes, out of memory, hangs, and port problems.
+27 -4
View File
@@ -1,18 +1,41 @@
name: Feature request
description: Suggest something new or an improvement
title: "[Feature]: "
title: "[Area] "
labels: ["enhancement"]
body:
- type: markdown
value: |
Explain what you want in plain language.
Explain what you want in plain language. For a large feature, a change to a default or a refactor, please agree the scope here first. Read [CONTRIBUTING.md](https://github.com/Niko1221/Strata/blob/main/CONTRIBUTING.md).
**The title must start with its area:** [Engine], [GUI], [TUI], [Benchmark], [Docs] or [Other]. Choose the same one below.
- type: dropdown
id: area
attributes:
label: Area
description: Start the issue title with the same area in square brackets, for example "[Engine] decode stops after 2 minutes". Pick Other if none fits.
options:
- "[Engine] (speed, kernels, models, the server and API)"
- "[GUI] (the desktop app)"
- "[TUI] (the Terminal: strata launch, connect, monitor)"
- "[Benchmark] (measurements and reports)"
- "[Docs] (documentation)"
- "[Other] (anything else, including setup and installation)"
validations:
required: true
- type: textarea
id: idea
attributes:
label: What do you want to do?
description: Describe the goal rather than the implementation. If there's a workaround you use today, mention it.
description: Describe the goal rather than the implementation. Say who needs it and why.
validations:
required: true
- type: textarea
id: workaround
attributes:
label: What do you do today instead?
description: The workaround you use now, if there is one.
validations:
required: false
- type: markdown
value: |
All of this counts: model and architecture support, the server and API, the web app, multi-GPU and layer split, images and vision, MCP servers, setup and installation, and performance.
All of this counts: model and architecture support, the server and API, the app and the Terminal, multi-GPU and layer split, images and vision, MCP servers, setup and installation, and performance.
+26 -3
View File
@@ -1,11 +1,27 @@
name: Question
description: Ask how to use Strata, or share a result
title: "Question: "
title: "[Area] "
labels: ["question"]
body:
- type: markdown
value: |
Not sure how to do something, or want to share numbers from your machine? Ask here.
Not sure how to do something, or want to share numbers from your machine? Ask here. To share a full benchmark, see [CONTRIBUTING.md](https://github.com/Niko1221/Strata/blob/main/CONTRIBUTING.md#share-a-benchmark).
**The title must start with its area:** [Engine], [GUI], [TUI], [Benchmark], [Docs] or [Other]. Choose the same one below.
- type: dropdown
id: area
attributes:
label: Area
description: Start the issue title with the same area in square brackets, for example "[Engine] decode stops after 2 minutes". Pick Other if none fits.
options:
- "[Engine] (speed, kernels, models, the server and API)"
- "[GUI] (the desktop app)"
- "[TUI] (the Terminal: strata launch, connect, monitor)"
- "[Benchmark] (measurements and reports)"
- "[Docs] (documentation)"
- "[Other] (anything else, including setup and installation)"
validations:
required: true
- type: textarea
id: question
attributes:
@@ -13,10 +29,17 @@ body:
description: What you're trying to do. Include your version, GPU, and OS if it affects the answer.
validations:
required: true
- type: textarea
id: tried
attributes:
label: What you tried
description: What you already did and where you got stuck.
validations:
required: false
- type: textarea
id: numbers
attributes:
label: Results (optional)
description: Only if you ran a benchmark or measured speed. Model, quant, context, and the tok/s you get.
validations:
required: false
required: false
+27 -9
View File
@@ -1,12 +1,30 @@
## Title
<!-- If Applicable, reference the GitHub issue -->
Issue: Resolves #
**The title must start with its area: [Engine], [GUI], [TUI], [Benchmark], [Docs] or [Other], for example "[Engine] fix decode stop on long prompts" or "[GUI] keep the model list after a tab switch".**
## Summary
<!-- Quick Summary of changes -->
<!--
Read CONTRIBUTING.md first. Lead with the problem and the result, not a file-by-file list.
Placeholders below are not test results: replace them with what you observed, or write "none".
-->
## What changed
<!-- Specifics on files changed, and what changes were made there -->
## Problem and result
<!-- What triggered this change. How it behaves before and after. -->
## Extra Notes
<!-- Any extra notes, delete if there are none -->
## Related discussion
<!-- The issue URL and the maintainer's agreement on scope, when the change needs one (features, refactors, defaults). Write "none" if not applicable. -->
## Approach
<!-- Why this fix. Compatibility effects. Keep unrelated cleanup out of the PR. -->
## Validation
<!-- Exact commands, the revision and the environment tested, what you saw. Keep the three lists apart. -->
**Passed:**
<!-- command, revision/environment, observed result, log -->
**Failed:**
<!-- command and failure, or "none observed" -->
**Not run:**
<!-- check, why, and what stays unverified (for example CUDA, HIP, SYCL or Metal builds you could not do) -->
## Limits
<!-- Known limitations or remaining work. -->
+218
View File
@@ -0,0 +1,218 @@
'use strict';
// Format check for issues and pull requests (see CONTRIBUTING.md and .github/ISSUE_TEMPLATE/*.yml).
// Pure functions only: no network, no filesystem. Used by intake.yml and reopen.yml through reset-bot.js.
const AREAS = ['Engine', 'GUI', 'TUI', 'Benchmark', 'Docs', 'Other'];
const TITLE_RE = new RegExp('^\\[(' + AREAS.join('|') + ')\\] \\S');
const NO_ANSWER = /^(_no response_|none|n\/a|na|-|todo|tbd|\.|\?)$/i;
const NOT_RUN_LABELS = /^\*\*(passed|failed|not run)\s*:?\*\*\s*:?/gim;
// Version the reopened work must be tested on.
const CURRENT_VERSION_RE = /\b0\.2\.\d+|0\.2\.0-beta/i;
// Any version or commit.
const ANY_VERSION_RE = /\bv?\d+\.\d+(\.\d+)*(-[a-z0-9.]+)?\b|\b[0-9a-f]{7,40}\b|\bcommit\b/i;
const HARDWARE_RE = /\b(rtx|gtx|geforce|quadro|tesla|radeon|rx ?\d|instinct|arc|a\d{3,4}|b\d{2,3}|apple|m[1-5]( pro| max| ultra)?|nvidia|amd|intel|gpu|cpu|vram|\d+ ?gb|ram|windows|linux|ubuntu|debian|fedora|arch|macos|mac os|wsl)\b/i;
const REVISION_RE = /0\.2\.\d|beta|\bmain\b|\b[0-9a-f]{7,40}\b/i;
function stripComments(text) {
return String(text || '').replace(/<!--[\s\S]*?-->/g, '');
}
function hasAreaPrefix(title) {
return TITLE_RE.test(String(title || ''));
}
// Issue forms render as "### Label\n\nvalue". Returns { label: value } for those headings.
function parseSections(body) {
const out = {};
let cur = null;
for (const line of stripComments(body).split(/\r?\n/)) {
const m = /^#{1,6}\s+(.+?)\s*#*\s*$/.exec(line);
if (m) {
cur = m[1].trim();
out[cur] = '';
} else if (cur !== null) {
out[cur] += line + '\n';
}
}
for (const k of Object.keys(out)) out[k] = out[k].trim();
return out;
}
function realText(value) {
const t = String(value || '').replace(NOT_RUN_LABELS, '').replace(/\s+/g, ' ').trim();
return NO_ANSWER.test(t) ? '' : t;
}
function findSection(sections, re) {
for (const k of Object.keys(sections)) if (re.test(k)) return { key: k, value: sections[k] };
return null;
}
// Required parts of each issue form: [display name, heading regex, minimum characters of real text].
const FORMS = {
bug: {
detect: /what happened/i,
parts: [
['Area', /^area$/i, 2],
['Desktop app, Terminal or source?', /desktop app, terminal or source/i, 2],
['What happened', /what happened/i, 20],
['Reproduction status', /reproduction status/i, 2],
['Strata version', /strata version/i, 3],
['Graphics card, memory and system', /graphics card|memory and system/i, 3],
],
needsCurrentVersion: 'Strata version',
},
feature: {
detect: /what do you want to do/i,
parts: [
['Area', /^area$/i, 2],
['What do you want to do?', /what do you want to do/i, 20],
],
},
question: {
detect: /your question/i,
parts: [
['Area', /^area$/i, 2],
['Your question', /your question/i, 15],
],
},
benchmark: {
detect: /what did you measure/i,
parts: [
['What did you measure, or what went wrong', /what did you measure/i, 30],
['Reproduction status', /reproduction status/i, 2],
],
needsCurrentVersionInBody: true,
},
};
function checkIssue({ title, body, labels = [], mode = 'intake' }) {
const missing = [];
if (!hasAreaPrefix(title)) {
missing.push('Start the title with its area: [Engine], [GUI], [TUI], [Benchmark], [Docs] or [Other], then a short description. Example: "[Engine] decode stops after 2 minutes".');
}
const text = stripComments(body);
const sections = parseSections(text);
const labelSet = new Set(labels.map((l) => String(l).toLowerCase()));
const needCurrent = mode === 'reopen';
let formKey = null;
for (const [key, f] of Object.entries(FORMS)) {
if (findSection(sections, f.detect)) { formKey = key; break; }
}
if (formKey) {
const form = FORMS[formKey];
for (const [name, re, min] of form.parts) {
const s = findSection(sections, re);
if (!s || realText(s.value).length < min) missing.push('Fill in "' + name + '".');
}
if (needCurrent && form.needsCurrentVersion) {
const s = findSection(sections, /strata version/i);
if (s && realText(s.value) && !CURRENT_VERSION_RE.test(s.value)) {
missing.push('Re-test on Strata 0.2.0-beta and put that version in "' + form.needsCurrentVersion + '". If it no longer happens, say so in a comment instead.');
}
}
if (needCurrent && form.needsCurrentVersionInBody && !CURRENT_VERSION_RE.test(text)) {
missing.push('Say which Strata version you measured on. Re-measure on 0.2.0-beta if you can.');
}
return { ok: missing.length === 0, missing, kind: formKey };
}
// Free-form text (older issues, blank issues).
const plain = realText(text);
const lenient = labelSet.has('enhancement') || labelSet.has('question');
if (plain.length < (lenient ? 40 : 80)) {
missing.push('Describe it in the issue body: what happened, what you expected, and the exact error if there is one. The issue forms (New issue > Bug report) list what to include.');
}
if (!lenient) {
if (!ANY_VERSION_RE.test(text)) missing.push('Add the Strata version (Settings > About, or `strata --version`).');
else if (needCurrent && !CURRENT_VERSION_RE.test(text)) missing.push('Re-test on Strata 0.2.0-beta and name that version in the issue. If it no longer happens, say so in a comment instead.');
if (!HARDWARE_RE.test(text)) missing.push('Add your graphics card with its VRAM, your RAM and your operating system.');
}
return { ok: missing.length === 0, missing, kind: 'free-form' };
}
function checkPull({ title, body, mode = 'intake' }) {
const missing = [];
if (!hasAreaPrefix(title)) {
missing.push('Start the title with its area: [Engine], [GUI], [TUI], [Benchmark], [Docs] or [Other], then a short description. Example: "[Engine] fix decode stop on long prompts".');
}
const text = stripComments(body);
const sections = parseSections(text);
const problem = findSection(sections, /problem|result|summary|what.*change/i);
const validation = findSection(sections, /validation|testing|tests?\b|how.*tested/i);
let valText = '';
if (problem || validation) {
if (!problem || realText(problem.value).length < 20) missing.push('Fill in "Problem and result": what triggered the change, and how it behaves before and after.');
if (!validation || realText(validation.value).length < 20) {
missing.push('Fill in "Validation": the exact commands, the revision and environment tested, what you saw. Keep Passed, Failed and Not run apart.');
} else {
valText = validation.value;
}
} else {
const plain = realText(text);
if (plain.length < 150) {
missing.push('Describe the pull request: the problem and the result. The pull request template shows the parts (Problem and result, Related discussion, Approach, Validation, Limits).');
}
if (!/(problem|result|before|after|fix|because|bug)/i.test(plain)) missing.push('Add "Problem and result": what triggered the change, and how it behaves before and after.');
if (!/(validat|tested|passed|ran |built|build|not run)/i.test(plain)) missing.push('Add "Validation": the exact commands, the revision and environment tested, and what you saw.');
valText = plain;
}
if (mode === 'reopen' && valText && !REVISION_RE.test(valText)) {
missing.push('Say in Validation which revision you tested: rebase on main (or 0.2.0-beta) and re-run your checks.');
}
return { ok: missing.length === 0, missing, kind: 'pull' };
}
function checkItem({ isPull, ...rest }) {
return isPull ? checkPull(rest) : checkIssue(rest);
}
function checklist(missing) {
return missing.map((m) => '- [ ] ' + m).join('\n');
}
const CONTRIB = 'https://github.com/Niko1221/Strata/blob/main/CONTRIBUTING.md';
function intakeComment(isPull, missing) {
return [
'Thanks for the ' + (isPull ? 'pull request' : 'issue') + '. To keep the tracker readable, every item needs an area prefix in the title and the parts of the ' + (isPull ? 'pull request template' : 'issue form') + ' filled in. This one is missing:',
'',
checklist(missing),
'',
'Edit the ' + (isPull ? 'pull request' : 'issue') + ' (title and description) and this note goes away on its own. If it is still missing parts in 5 days it will be closed, which is not a rejection: fix it and comment `/reopen`.',
'',
'Details: [CONTRIBUTING.md](' + CONTRIB + ')',
].join('\n');
}
function reopenMissingComment(isPull, missing) {
return [
'Not reopened yet. Still missing:',
'',
checklist(missing),
'',
'Edit the ' + (isPull ? 'pull request' : 'issue') + ' and comment `/reopen` again. Details: [CONTRIBUTING.md](' + CONTRIB + ')',
].join('\n');
}
function reopenOkComment(isPull) {
return 'Reopened, thank you. A maintainer will pick it up from here.' + (isPull ? ' If it conflicts with main, rebase it first; that speeds up review.' : '');
}
function staleComment(isPull) {
return [
'Closing this ' + (isPull ? 'pull request' : 'issue') + ' because it still did not follow the format after 5 days. That is housekeeping, not a rejection.',
'',
'To bring it back: fix the title (area prefix) and the missing parts, then comment `/reopen`. A check runs and reopens it, or tells you what is still missing. See [CONTRIBUTING.md](' + CONTRIB + ').',
].join('\n');
}
module.exports = {
AREAS, TITLE_RE, hasAreaPrefix, parseSections, stripComments, realText,
checkIssue, checkPull, checkItem, checklist,
intakeComment, reopenMissingComment, reopenOkComment, staleComment,
};
+192
View File
@@ -0,0 +1,192 @@
'use strict';
const test = require('node:test');
const assert = require('node:assert');
const c = require('./item-check');
const bot = require('./reset-bot');
const bugForm = (over = {}) => {
const f = {
'Area': '[Engine] (speed, kernels, models, the server and API)',
'Desktop app, Terminal or source?': 'Terminal (the `strata` command)',
'What happened': 'Decode stops after two minutes with "CUDA error 700". I expected it to continue.',
'Smallest steps or command that reproduce it': '_No response_',
'Reproduction status': 'Reproduced locally',
'Strata version': 'Strata 0.2.0-beta · engine 0.1.45.3',
'Graphics card, memory and system': 'RTX 5070 12 GB · 64 GB RAM · Windows 11',
...over,
};
return Object.entries(f).map(([k, v]) => `### ${k}\n\n${v}`).join('\n\n');
};
test('title prefix', () => {
assert.ok(c.hasAreaPrefix('[Engine] fix decode'));
assert.ok(c.hasAreaPrefix('[Docs] x'));
assert.ok(!c.hasAreaPrefix('Engine fix'));
assert.ok(!c.hasAreaPrefix('[engine] fix'));
assert.ok(!c.hasAreaPrefix('[Engine]'));
assert.ok(!c.hasAreaPrefix('[Engine] '));
assert.ok(!c.hasAreaPrefix('[Area] foo'));
assert.ok(!c.hasAreaPrefix(''));
assert.ok(!c.hasAreaPrefix(undefined));
});
test('filled bug form passes in both modes', () => {
for (const mode of ['intake', 'reopen']) {
const r = c.checkIssue({ title: '[Engine] decode stops', body: bugForm(), labels: ['bug'], mode });
assert.deepStrictEqual(r.missing, []);
assert.strictEqual(r.kind, 'bug');
}
});
test('bug form: missing parts are named', () => {
const r = c.checkIssue({ title: 'decode stops', body: bugForm({ 'Strata version': '_No response_', 'What happened': 'bad' }) });
assert.ok(!r.ok);
assert.ok(r.missing.some((m) => /title/i.test(m)));
assert.ok(r.missing.some((m) => m.includes('What happened')));
assert.ok(r.missing.some((m) => m.includes('Strata version')));
});
test('reopen needs a 0.2 version, intake does not', () => {
const body = bugForm({ 'Strata version': 'Strata 0.1.44' });
assert.ok(c.checkIssue({ title: '[Engine] x y', body, mode: 'intake' }).ok);
const r = c.checkIssue({ title: '[Engine] x y', body, mode: 'reopen' });
assert.ok(!r.ok);
assert.ok(r.missing.some((m) => m.includes('0.2.0-beta')));
});
test('html comments and placeholders do not count', () => {
const r = c.checkIssue({ title: '[Engine] x y', body: bugForm({ 'What happened': '<!-- describe -->' }) });
assert.ok(r.missing.some((m) => m.includes('What happened')));
});
test('feature and question forms', () => {
const f = '### Area\n\n[GUI] (the desktop app)\n\n### What do you want to do?\n\nLet me pin a model to the top of the list in the app.';
assert.ok(c.checkIssue({ title: '[GUI] pin models', body: f, mode: 'reopen' }).ok);
assert.ok(!c.checkIssue({ title: '[GUI] pin models', body: '### Area\n\n[GUI]\n\n### What do you want to do?\n\n_No response_' }).ok);
const q = '### Area\n\n[Other]\n\n### Your question\n\nHow do I move the model folder to another drive?';
assert.ok(c.checkIssue({ title: '[Other] model folder', body: q }).ok);
});
test('free-form issue', () => {
const good = 'Strata 0.2.0-beta on an RTX 3060 12 GB with 32 GB RAM on Linux: the server hangs when I send two requests at once. Expected both to finish.';
assert.ok(c.checkIssue({ title: '[Engine] hang on two requests', body: good, mode: 'reopen' }).ok);
assert.ok(!c.checkIssue({ title: '[Engine] hang', body: 'it crashes', mode: 'intake' }).ok);
const old = good.replace('0.2.0-beta', '0.1.40');
assert.ok(c.checkIssue({ title: '[Engine] hang', body: old, mode: 'intake' }).ok);
assert.ok(!c.checkIssue({ title: '[Engine] hang', body: old, mode: 'reopen' }).ok);
assert.ok(c.checkIssue({ title: '[Other] idea', body: 'It would help if the app remembered my last model between runs, today I pick it again.', labels: ['enhancement'], mode: 'reopen' }).ok);
});
const prBody = (over = {}) => {
const p = {
problem: 'Decode stopped on prompts above 8k because the stop check read the wrong length. Now it continues.',
validation: '**Passed:** `ctest -R decode`, commit abcdef1, RTX 3060, 12 of 12.\n\n**Failed:** none observed\n\n**Not run:** HIP build',
...over,
};
return `## Problem and result\n<!-- hint -->\n${p.problem}\n\n## Related discussion\nnone\n\n## Approach\nFix the length.\n\n## Validation\n${p.validation}\n\n## Limits\nnone`;
};
test('filled PR passes', () => {
assert.deepStrictEqual(c.checkPull({ title: '[Engine] fix stop', body: prBody(), mode: 'reopen' }).missing, []);
});
test('untouched PR template fails', () => {
const tpl = '## Problem and result\n<!-- What triggered -->\n\n## Validation\n<!-- cmds -->\n\n**Passed:**\n<!-- x -->\n\n**Failed:**\n\n**Not run:**\n';
const r = c.checkPull({ title: '[Engine] fix stop', body: tpl });
assert.ok(!r.ok);
assert.ok(r.missing.some((m) => m.includes('Problem and result')));
assert.ok(r.missing.some((m) => m.includes('Validation')));
});
test('PR: empty body and bad title', () => {
const r = c.checkPull({ title: 'fix', body: null });
assert.ok(!r.ok);
assert.ok(r.missing.length >= 2);
});
test('PR reopen needs a revision', () => {
const body = prBody({ validation: '**Passed:** I tried it and it worked for me on my machine, looked fine.' });
assert.ok(c.checkPull({ title: '[Engine] x y', body, mode: 'intake' }).ok);
assert.ok(!c.checkPull({ title: '[Engine] x y', body, mode: 'reopen' }).ok);
});
test('free-form PR', () => {
const body = 'This fixes a crash when the context is above 8k, because the length was read from the wrong field. Before it aborted, after it continues. I built it on CUDA and ran the decode test, passed.';
assert.ok(c.checkPull({ title: '[Engine] fix crash', body }).ok);
assert.ok(!c.checkPull({ title: '[Engine] fix crash', body: 'fixes stuff' }).ok);
});
test('messages', () => {
const m = c.intakeComment(false, ['a', 'b']);
assert.ok(m.includes('- [ ] a') && m.includes('/reopen') && m.includes('CONTRIBUTING.md'));
assert.ok(c.staleComment(true).includes('/reopen'));
});
// handlers with a fake GitHub client
function fake(calls, extra = {}) {
const rest = {
issues: {
getLabel: async () => ({}),
createLabel: async (a) => calls.push(['createLabel', a.name]),
addLabels: async (a) => calls.push(['addLabels', a.labels]),
removeLabel: async (a) => calls.push(['removeLabel', a.name]),
createComment: async (a) => calls.push(['comment', a.body]),
update: async (a) => calls.push(['update', a.state]),
get: async () => ({ data: extra.live }),
listEvents: async () => {},
listForRepo: async () => {},
},
pulls: { get: async () => ({ data: { mergeable_state: 'clean' } }) },
};
return { rest, paginate: async (fn) => (fn === rest.issues.listEvents ? (extra.events || []) : (extra.items || [])) };
}
const core = { info() {} };
const repo = { owner: 'o', repo: 'r' };
test('intake labels and comments once', async () => {
const calls = [];
const item = { number: 1, title: 'bad', body: '', labels: [], user: { type: 'User' }, author_association: 'NONE' };
await bot.intake({ github: fake(calls), context: { repo, payload: { action: 'opened', issue: item } }, core });
assert.deepStrictEqual(calls.map((c) => c[0]), ['addLabels', 'comment']);
calls.length = 0;
item.labels = [{ name: 'needs-format' }];
await bot.intake({ github: fake(calls), context: { repo, payload: { action: 'edited', issue: item } }, core });
assert.deepStrictEqual(calls, []);
});
test('intake clears the label when edited into shape', async () => {
const calls = [];
const item = { number: 1, title: '[Engine] x y', body: bugForm(), labels: [{ name: 'needs-format' }], user: { type: 'User' } };
await bot.intake({ github: fake(calls), context: { repo, payload: { action: 'edited', issue: item } }, core });
assert.deepStrictEqual(calls.map((c) => c[0]), ['removeLabel', 'comment']);
});
test('reopen: ignores strangers, reopens the author when it passes', async () => {
const issue = { number: 5, state: 'closed', user: { login: 'amy' }, labels: [] };
let calls = [];
const comment = { body: '/reopen', user: { login: 'bob', type: 'User' }, author_association: 'NONE' };
await bot.reopen({ github: fake(calls), context: { repo, payload: { issue, comment } }, core });
assert.deepStrictEqual(calls, []);
const live = { number: 5, title: '[Engine] x y', body: bugForm(), labels: [] };
comment.user.login = 'amy';
await bot.reopen({ github: fake(calls, { live }), context: { repo, payload: { issue, comment } }, core });
assert.deepStrictEqual(calls.map((c) => c[0]), ['update', 'removeLabel', 'comment']);
assert.strictEqual(calls[0][1], 'open');
calls = [];
live.title = 'no prefix';
await bot.reopen({ github: fake(calls, { live }), context: { repo, payload: { issue, comment } }, core });
assert.deepStrictEqual(calls.map((c) => c[0]), ['comment']);
assert.ok(calls[0][1].includes('Not reopened'));
});
test('sweep closes only old flagged items', async () => {
const calls = [];
const old = new Date(Date.now() - 6 * 86400000).toISOString();
const fresh = new Date(Date.now() - 1 * 86400000).toISOString();
const mk = (n, created) => ({ number: n, title: 'bad', body: '', labels: [{ name: 'needs-format' }], created_at: created });
const gh = fake(calls, { items: [mk(1, old), mk(2, fresh)] });
await bot.sweep({ github: gh, context: { repo }, core });
assert.deepStrictEqual(calls.map((c) => c[0]), ['comment', 'update']);
});
+164
View File
@@ -0,0 +1,164 @@
'use strict';
// GitHub-side handlers for intake.yml, reopen.yml and the 5-day sweep.
// They take the actions/github-script objects ({ github, context, core }) and never touch PR code:
// everything comes from the event payload and the REST API.
const check = require('./item-check');
const LABEL = 'needs-format';
const STALE_DAYS = 5;
const MAINTAINER = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']);
// Items a maintainer closed on purpose stay closed.
const BLOCK_LABELS = new Set(['wontfix', 'invalid', 'duplicate', 'declined']);
function labelNames(item) {
return (item.labels || []).map((l) => (typeof l === 'string' ? l : l.name));
}
async function ensureLabel(github, context) {
const { owner, repo } = context.repo;
try {
await github.rest.issues.getLabel({ owner, repo, name: LABEL });
} catch (e) {
if (e.status !== 404) throw e;
await github.rest.issues.createLabel({
owner, repo, name: LABEL, color: 'fbca04',
description: 'Title or description does not follow CONTRIBUTING.md yet',
});
}
}
async function removeLabel(github, context, number) {
const { owner, repo } = context.repo;
try {
await github.rest.issues.removeLabel({ owner, repo, issue_number: number, name: LABEL });
} catch (e) {
if (e.status !== 404) throw e;
}
}
function itemFromPayload(context) {
const p = context.payload;
return p.pull_request || p.issue;
}
function isPullItem(item, context) {
return Boolean(context.payload.pull_request || item.pull_request);
}
// issues: opened/edited/reopened, pull_request_target: opened/edited/reopened
async function intake({ github, context, core }) {
const { owner, repo } = context.repo;
const item = itemFromPayload(context);
if (!item || (item.user && item.user.type === 'Bot')) return;
if (MAINTAINER.has(item.author_association) && context.payload.action === 'opened') {
// Maintainer items are not nagged on creation.
return;
}
const isPull = isPullItem(item, context);
const labels = labelNames(item);
const hasLabel = labels.includes(LABEL);
const res = check.checkItem({ isPull, title: item.title, body: item.body, labels, mode: 'intake' });
core.info('check ' + (isPull ? 'PR' : 'issue') + ' #' + item.number + ': ' + (res.ok ? 'ok' : res.missing.join(' | ')));
if (res.ok) {
if (hasLabel) {
await removeLabel(github, context, item.number);
await github.rest.issues.createComment({ owner, repo, issue_number: item.number, body: 'Thanks, this follows the format now.' });
}
return;
}
if (context.payload.action === 'edited' && !hasLabel) return; // edits only matter for items already flagged
if (hasLabel) return; // one note only
await ensureLabel(github, context);
await github.rest.issues.addLabels({ owner, repo, issue_number: item.number, labels: [LABEL] });
await github.rest.issues.createComment({ owner, repo, issue_number: item.number, body: check.intakeComment(isPull, res.missing) });
}
// issue_comment created, body starts with /reopen
async function reopen({ github, context, core }) {
const { owner, repo } = context.repo;
const comment = context.payload.comment;
const issue = context.payload.issue;
if (!/^\s*\/reopen\b/.test(comment.body || '')) return;
if (comment.user.type === 'Bot') return;
const isAuthor = comment.user.login === issue.user.login;
const isMaint = MAINTAINER.has(comment.author_association);
if (!isAuthor && !isMaint) {
core.info('ignored /reopen from ' + comment.user.login + ' (not the author or a maintainer)');
return;
}
const isPull = Boolean(issue.pull_request);
const say = (body) => github.rest.issues.createComment({ owner, repo, issue_number: issue.number, body });
const labels = labelNames(issue);
const wasOpen = issue.state === 'open';
if (labels.some((l) => BLOCK_LABELS.has(l.toLowerCase())) && !isMaint) {
await say('This one was closed on purpose (' + labels.filter((l) => BLOCK_LABELS.has(l.toLowerCase())).join(', ') + '). Please add your new evidence in a comment and a maintainer will decide.');
return;
}
if (wasOpen && !labels.includes(LABEL)) {
core.info('already open, nothing to do');
return;
}
// The bot reads the live item, not the possibly stale event copy.
const live = (await github.rest.issues.get({ owner, repo, issue_number: issue.number })).data;
const res = check.checkItem({ isPull, title: live.title, body: live.body, labels: labelNames(live), mode: 'reopen' });
core.info('reopen check #' + live.number + ': ' + (res.ok ? 'ok' : res.missing.join(' | ')));
if (!res.ok) {
await say(check.reopenMissingComment(isPull, res.missing));
return;
}
if (!wasOpen) {
try {
await github.rest.issues.update({ owner, repo, issue_number: live.number, state: 'open' });
} catch (e) {
await say('The check passed, but GitHub would not reopen it (' + (e.message || e.status) + '). ' + (isPull ? 'If the branch was deleted, restore it or open a new pull request and link this one.' : 'Please open a new issue and link this one.'));
return;
}
}
await removeLabel(github, context, live.number);
let extra = '';
if (isPull) {
try {
const pr = (await github.rest.pulls.get({ owner, repo, pull_number: live.number })).data;
if (pr.mergeable_state === 'dirty') extra = '\n\nIt has conflicts with main now: please rebase it.';
} catch (e) { /* best effort */ }
}
await say(check.reopenOkComment(isPull) + extra);
}
async function labelTime(github, owner, repo, number, fallback) {
const events = await github.paginate(github.rest.issues.listEvents, { owner, repo, issue_number: number, per_page: 100 });
let t = null;
for (const e of events) {
if (e.event === 'labeled' && e.label && e.label.name === LABEL) t = e.created_at;
}
return t || fallback;
}
// scheduled: close items that kept the needs-format label for more than 5 days
async function sweep({ github, context, core }) {
const { owner, repo } = context.repo;
const cutoff = Date.now() - STALE_DAYS * 86400 * 1000;
const items = await github.paginate(github.rest.issues.listForRepo, { owner, repo, state: 'open', labels: LABEL, per_page: 100 });
core.info(items.length + ' open items with ' + LABEL);
for (const item of items) {
const since = new Date(await labelTime(github, owner, repo, item.number, item.created_at)).getTime();
if (since > cutoff) continue;
const isPull = Boolean(item.pull_request);
// Re-check first: an edit that fixed it should have cleared the label already; do not close good items.
const res = check.checkItem({ isPull, title: item.title, body: item.body, labels: labelNames(item), mode: 'intake' });
if (res.ok) {
await removeLabel(github, context, item.number);
continue;
}
await github.rest.issues.createComment({ owner, repo, issue_number: item.number, body: check.staleComment(isPull) });
await github.rest.issues.update({ owner, repo, issue_number: item.number, state: 'closed', state_reason: 'not_planned' });
core.info('closed #' + item.number);
}
}
module.exports = { intake, reopen, sweep, LABEL, STALE_DAYS };
+114
View File
@@ -0,0 +1,114 @@
name: Docker images
# Builds the Strata container images of one release from its engine packages and pushes them to ghcr.io.
# Run by hand only (Actions tab -> Docker images -> Run workflow). Nothing starts it on its own.
#
# For each flavor the job downloads the engine package from the release and checks it against the signed update feed
# (docker/fetch-engines.py: the feed's Ed25519 signature, the archive's SHA-256, the manifest hash, the package signature,
# every file's hash), builds the image with docker/build.sh and, only when "push" is on, pushes
# ghcr.io/niko1221/strata:<version>-<flavor> and ghcr.io/niko1221/strata:latest-<flavor>
# The first run with "push" off builds everything without publishing it (a dry run).
on:
workflow_dispatch:
inputs:
release_tag:
description: 'Release tag whose engine packages go into the images (for example v0.2.1)'
required: true
type: string
flavors:
description: 'Flavors to build, comma separated (empty: every flavor the release has a package for)'
required: false
default: ''
type: string
push:
description: 'Push the images to ghcr.io (off: build only, nothing is published)'
required: true
default: false
type: boolean
latest:
description: 'Also move the latest-<flavor> tags (turn off for a pre-release or a test)'
required: true
default: true
type: boolean
source_ref:
description: 'Git ref of the Dockerfile and the Terminal sources (empty: the release tag)'
required: false
default: ''
type: string
permissions:
contents: read
env:
IMAGE: ghcr.io/niko1221/strata
jobs:
plan:
runs-on: ubuntu-24.04
outputs:
flavors: ${{ steps.list.outputs.flavors }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.source_ref || inputs.release_tag }}
- name: Which flavors does the release have a package for
id: list
env:
WANT: ${{ inputs.flavors }}
TAG: ${{ inputs.release_tag }}
run: |
set -eu
args="--release-tag $TAG --list --skip-missing"
[ -n "$WANT" ] && args="$args --flavors $WANT"
python3 docker/fetch-engines.py $args > list.tsv
cat list.tsv
flavors=$(awk -F'\t' '$3 != "SKIP" {printf "%s\"%s\"", (n++ ? "," : ""), $1}' list.tsv)
[ -n "$flavors" ] || { echo "the release has no engine package for any requested flavor"; exit 1; }
echo "flavors=[$flavors]" >> "$GITHUB_OUTPUT"
image:
needs: plan
runs-on: ubuntu-24.04
timeout-minutes: 120
permissions:
contents: read
packages: write
strategy:
fail-fast: false
max-parallel: 3
matrix:
flavor: ${{ fromJSON(needs.plan.outputs.flavors) }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.source_ref || inputs.release_tag }}
- name: Free disk space (the runner has about 14 GB; a package plus its image needs about 10 GB)
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL || true
docker system prune -af >/dev/null 2>&1 || true
df -h /
- name: Log in to ghcr.io
if: ${{ inputs.push }}
env:
REGISTRY_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REGISTRY_USER: ${{ github.actor }}
run: printf '%s' "$REGISTRY_TOKEN" | docker login ghcr.io -u "$REGISTRY_USER" --password-stdin
- name: Fetch, verify, build (and push when push is on)
env:
TAG: ${{ inputs.release_tag }}
FLAVOR: ${{ matrix.flavor }}
PUSH: ${{ inputs.push && '--push' || '' }}
LATEST: ${{ !inputs.latest && '--no-latest' || '' }}
run: |
docker/publish.sh --release-tag "$TAG" --flavors "$FLAVOR" --image "$IMAGE" $PUSH $LATEST
- name: Summary
if: ${{ always() }}
run: |
{
echo "### ${{ matrix.flavor }}"
echo "- release: ${{ inputs.release_tag }}, pushed: ${{ inputs.push }}"
echo "- image: \`$IMAGE:${INPUT_TAG#v}-${{ matrix.flavor }}\`"
} >> "$GITHUB_STEP_SUMMARY"
env:
INPUT_TAG: ${{ inputs.release_tag }}
+58
View File
@@ -0,0 +1,58 @@
name: Intake
# New issues and pull requests that do not follow CONTRIBUTING.md get the needs-format label and one friendly note.
# Items that keep the label for 5 days are closed by the daily sweep, with a note on how to /reopen.
#
# pull_request_target is used instead of pull_request on purpose: a pull request from a fork gets a read-only
# GITHUB_TOKEN under pull_request, so it could not be labelled or commented on. It is safe here because this
# workflow never checks out or runs the pull request's code. It checks out the default branch only (the bot
# scripts) and reads the title and description from the event payload and the REST API.
on:
issues:
types: [opened, edited]
pull_request_target:
types: [opened, edited]
schedule:
- cron: '17 4 * * *'
workflow_dispatch:
permissions:
contents: read
issues: write
pull-requests: write
concurrency:
group: intake-${{ github.event.issue.number || github.event.pull_request.number || 'sweep' }}
cancel-in-progress: false
jobs:
check:
if: github.event_name == 'issues' || github.event_name == 'pull_request_target'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.repository.default_branch }}
sparse-checkout: .github/scripts
persist-credentials: false
- uses: actions/github-script@v7
with:
script: |
const bot = require('./.github/scripts/reset-bot.js');
await bot.intake({ github, context, core });
sweep:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.repository.default_branch }}
sparse-checkout: .github/scripts
persist-credentials: false
- uses: actions/github-script@v7
with:
script: |
const bot = require('./.github/scripts/reset-bot.js');
await bot.sweep({ github, context, core });
+38
View File
@@ -0,0 +1,38 @@
name: Reopen
# An author comments /reopen on their closed issue or pull request. The check runs and the item is reopened,
# or the bot replies with what is still missing. GitHub does not let authors reopen items a maintainer closed,
# so this workflow does it for them.
#
# issue_comment always runs the workflow file from the default branch with a write-capable token, which is what
# we need for fork authors too. It never checks out or executes the pull request's code: it reads the live
# title and description through the REST API. Only the author of the item or a maintainer can trigger it.
on:
issue_comment:
types: [created]
permissions:
contents: read
issues: write
pull-requests: write
concurrency:
group: reopen-${{ github.event.issue.number }}
cancel-in-progress: false
jobs:
reopen:
if: startsWith(github.event.comment.body, '/reopen') && github.event.comment.user.type != 'Bot'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.repository.default_branch }}
sparse-checkout: .github/scripts
persist-credentials: false
- uses: actions/github-script@v7
with:
script: |
const bot = require('./.github/scripts/reset-bot.js');
await bot.reopen({ github, context, core });
+21
View File
@@ -0,0 +1,21 @@
name: Scripts test
on:
pull_request:
paths:
- '.github/scripts/**'
- '.github/workflows/intake.yml'
- '.github/workflows/reopen.yml'
permissions:
contents: read
jobs:
node-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: node --test .github/scripts/item-check.test.js
@@ -0,0 +1,65 @@
name: Minimal serving runtime
on:
push:
paths:
- .github/workflows/terminal-minimal-server.yml
- requirements.txt
- strata.py
- strata_cli/**
- strata_control/**
- serve/**
- tests/terminal/test_runtime.py
- tools/minimal_server_smoke.py
- tools/strata_tokenizer.py
pull_request:
paths:
- .github/workflows/terminal-minimal-server.yml
- requirements.txt
- strata.py
- strata_cli/**
- strata_control/**
- serve/**
- tests/terminal/test_runtime.py
- tools/minimal_server_smoke.py
- tools/strata_tokenizer.py
workflow_dispatch:
permissions:
contents: read
jobs:
fake-engine:
name: Linux / runtime requirements only
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-python@v7
with:
python-version: '3.11'
- name: Install only pinned runtime requirements
shell: bash
run: |
python -m venv "$RUNNER_TEMP/strata-runtime-venv"
"$RUNNER_TEMP/strata-runtime-venv/bin/python" -m pip install -r requirements.txt
- name: Check fake-engine serving without terminal packages
shell: bash
run: |
python - <<'PY'
import os
import subprocess
from pathlib import Path
report = Path(os.environ['RUNNER_TEMP']) / 'minimal-server-smoke.json'
python = str(Path(os.environ['RUNNER_TEMP']) / 'strata-runtime-venv' / 'bin' / 'python')
subprocess.run([python, 'tools/minimal_server_smoke.py',
'--report', str(report)], check=True)
print(report.read_text())
PY
- name: Run full server suite without terminal packages
shell: bash
run: |
"$RUNNER_TEMP/strata-runtime-venv/bin/python" -m unittest discover -s serve -p 'test_*.py'
@@ -0,0 +1,115 @@
name: Portable monitor client
on:
push:
paths:
- .github/workflows/terminal-portable-monitor.yml
- packaging/monitor_bundle.py
- strata.py
- strata_cli/**
- strata_control/**
- tools/minimal_cli_smoke.py
- tools/portable_monitor_smoke.py
- tools/portable_monitor_pty_smoke.py
- requirements-monitor.txt
- LICENSE
pull_request:
paths:
- .github/workflows/terminal-portable-monitor.yml
- packaging/monitor_bundle.py
- strata.py
- strata_cli/**
- strata_control/**
- tools/minimal_cli_smoke.py
- tools/portable_monitor_smoke.py
- tools/portable_monitor_pty_smoke.py
- requirements-monitor.txt
- LICENSE
workflow_dispatch:
permissions:
contents: read
jobs:
client:
name: ${{ matrix.os }} / Python ${{ matrix.python }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
python: ['3.10', '3.11']
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python }}
- name: Check source CLI without optional packages
shell: bash
run: |
python - <<'PY'
import os
import subprocess
import sys
from pathlib import Path
report = Path(os.environ['RUNNER_TEMP']) / 'minimal-cli-smoke.json'
subprocess.run([sys.executable, 'tools/minimal_cli_smoke.py',
'--report', str(report)], check=True)
print(report.read_text())
PY
- name: Build and smoke test without site packages
shell: bash
run: |
python - <<'PY'
import os
import subprocess
import sys
from pathlib import Path
bundle = Path(os.environ['RUNNER_TEMP']) / 'strata-monitor.pyz'
report = bundle.with_name('monitor-stdlib-smoke.json')
subprocess.run([sys.executable, 'packaging/monitor_bundle.py',
'build', str(bundle)], check=True)
subprocess.run([sys.executable, 'tools/portable_monitor_smoke.py',
'--bundle', str(bundle), '--report', str(report)], check=True)
print(report.read_text())
PY
- name: Install optional Textual dependency
if: matrix.python == '3.11'
run: python -m pip install -r requirements-monitor.txt
- name: Smoke test packaged interactive Overview
if: matrix.python == '3.11'
shell: bash
run: |
python - <<'PY'
import os
import subprocess
import sys
from pathlib import Path
bundle = Path(os.environ['RUNNER_TEMP']) / 'strata-monitor.pyz'
report = bundle.with_name('monitor-interactive-smoke.json')
subprocess.run([sys.executable, 'tools/portable_monitor_smoke.py',
'--bundle', str(bundle), '--interactive',
'--report', str(report)], check=True)
print(report.read_text())
PY
- name: Check POSIX terminal restoration
if: matrix.os == 'ubuntu-latest' && matrix.python == '3.11'
shell: bash
run: |
python - <<'PY'
import os
import subprocess
import sys
from pathlib import Path
bundle = Path(os.environ['RUNNER_TEMP']) / 'strata-monitor.pyz'
report = bundle.with_name('monitor-pty-smoke.json')
subprocess.run([sys.executable, 'tools/portable_monitor_pty_smoke.py',
'--bundle', str(bundle), '--report', str(report)], check=True)
print(report.read_text())
PY
+28
View File
@@ -0,0 +1,28 @@
name: Title area
# Checks that a pull request or issue title starts with its area: [Engine], [GUI], [TUI], [Benchmark], [Docs] or [Other].
# It reads the title only: no checkout, no labels, no comments, no write access.
on:
pull_request:
types: [opened, edited, reopened, synchronize]
issues:
types: [opened, edited, reopened]
permissions: {}
jobs:
title:
runs-on: ubuntu-latest
steps:
- name: Check the title
env:
TITLE: ${{ github.event.pull_request.title || github.event.issue.title }}
run: |
case "$TITLE" in
"[Engine] "?*|"[GUI] "?*|"[TUI] "?*|"[Benchmark] "?*|"[Docs] "?*|"[Other] "?*)
echo "Title OK: $TITLE" ;;
*)
echo "::error::The title must start with its area and then a short description. Allowed areas: [Engine], [GUI], [TUI], [Benchmark], [Docs], [Other]. Example: \"[Engine] fix decode stop on long prompts\". Your title: $TITLE"
exit 1 ;;
esac
+21
View File
@@ -48,6 +48,7 @@ build-vision-cuda12.bat
# setup.py outputs (engine, downloads) and release builds
/engine/
/engine-sycl/
/engine-cuda12/
/build-release*/
/dist/
@@ -64,3 +65,23 @@ build-vision-cuda12.bat
# local Windows HIP packaging output
dist-hip/
# Terminal / Desktop split: local venvs, build output and archives never enter git
.terminal-venv/
.terminal-test-venv/
node_modules/
*.dmg
/release/artifacts/
/apps/host/dist/
/apps/desktop/src-tauri/target/
/apps/desktop/src-tauri/resources/node/*
!/apps/desktop/src-tauri/resources/node/.gitignore
# the Control component (bundled CPython + wheels + strata_control) is built by apps/desktop/scripts/stage-control.py
/apps/desktop/src-tauri/resources/control/*
!/apps/desktop/src-tauri/resources/control/.gitignore
# the pruned DeepSeek Harness (MIT) is built by apps/desktop/scripts/stage-harness.mjs
/apps/desktop/src-tauri/resources/harness/*
!/apps/desktop/src-tauri/resources/harness/.gitignore
# screenshots and checkpoints from the Mac rebuild live outside git (StrataSplit\evidence-archive)
/apps/host/docs/evidence/
/apps/host/docs/checkpoints/
+65 -16
View File
@@ -1,35 +1,84 @@
# AGENTS.md
Strata runs the Qwen3.8-Flash-Next mixture-of-experts model (and its Coder, Swift 1.5 and Unsloth variants) on a
normal PC: one NVIDIA or AMD graphics card plus system RAM, on Windows or Linux. It has a C++/CUDA/HIP engine
(`src/`, `include/`), a Python server with an OpenAI- and Anthropic-compatible API and a web app (`serve/`), and a
one-click installer (`setup.py`, started by `START-HERE.bat` / `setup.sh`).
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.1, engine 0.1.46. It 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.
- **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.
## Installing Strata for a user
Follow **[docs/AI_SETUP.md](docs/AI_SETUP.md)**: check the PC, pick the model by RAM, run setup non-interactively,
start and verify the server, and connect the user's apps. Never expose the server beyond `127.0.0.1` without
`--api-key`. As an alternative to shell commands, Strata's MCP server ([docs/MCP_SERVER.md](docs/MCP_SERVER.md))
offers the same steps as tools.
Follow **[docs/AI_SETUP.md](docs/AI_SETUP.md)**. For an agent that has a shell, 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
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex # Windows PowerShell
```
On Windows, open a new terminal afterwards so that `strata` is on the PATH.
2. Check what would happen. Nothing is changed:
```sh
strata launch --plan --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
```
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
`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.
## Working on the code
- How the engine works, every measured number, the API and all settings: [docs/DETAILS.md](docs/DETAILS.md) and
the [paper](docs/paper/Strata-Paper.pdf).
- AMD (HIP) build and validation: [docs/AMD_HIP.md](docs/AMD_HIP.md); multi-GPU: [docs/MULTI_GPU.md](docs/MULTI_GPU.md).
- Setup's own tests run without a GPU or downloads: `python tools/test_setup_<name>.py` (for example
`tools/test_setup_amd.py`, `tools/test_setup_choices.py`).
- Keep the docs' style: plain words, measured numbers with what they were measured on, no claims without a
measurement.
- The Terminal: [docs/TERMINAL.md](docs/TERMINAL.md) and [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md). The app,
the packages and the update feed: [packaging/README.md](packaging/README.md) and [packaging/UPDATES.md](packaging/UPDATES.md).
- AMD (HIP) build and validation: [docs/AMD_HIP.md](docs/AMD_HIP.md); multi-GPU: [docs/MULTI_GPU.md](docs/MULTI_GPU.md);
Mac (Metal): [docs/MAC.md](docs/MAC.md).
- Tests: the Terminal and Control tests are in `tests/terminal` (`python -m pytest tests/terminal`). The app's UI
tests run with `npm test` in `apps/host`. Setup's own tests run without a GPU or downloads:
`python tools/test_setup_<name>.py` (for example `tools/test_setup_amd.py`, `tools/test_setup_choices.py`).
- Keep the docs' style: plain words, short sentences, written for non-technical users first. Give measured numbers
with what they were measured on. Make no claim without a measurement.
## Contributing a change or report
- Search the open issues and pull requests first, and add to a thread that already covers your point.
- Open an issue with the form that fits (bug report, feature request or question).
- One change per pull request. Say what it changes and what it leaves alone.
The rules are in **[CONTRIBUTING.md](CONTRIBUTING.md)**. Read it before you open an issue or a pull request, and
follow it whether a person or an agent writes the text. The short version:
- Start every issue and pull request title with its area: `[Engine]`, `[GUI]`, `[TUI]`, `[Benchmark]`, `[Docs]` or
`[Other]`, for example `[Engine] fix …`. A check on GitHub fails a pull request whose title has no area.
- Search the open and closed issues and pull requests first, and add to a thread that already covers your point.
- Agree the scope in an issue before a large feature, a refactor or a change to a default.
- One problem per pull request. Lead with the problem and the result, then the approach, then the validation
(passed, failed and not run, kept apart) and the limits.
- A new feature is opt-in, and the default path stays byte-identical to the last release. Say how you checked.
- Build every backend a file touches (CUDA, HIP, SYCL) before asking for review.
- Change a default only where you measured it faster, and show the numbers with what they were measured on.
- Never submit measurements, logs or reproduction steps you did not produce. Mark a finding from reading code as
unconfirmed static analysis.
- A report from hardware the maintainers do not have is welcome. Follow
[docs/COMMUNITY_BENCHMARKS.md](docs/COMMUNITY_BENCHMARKS.md), compare against a same-day run of the build you are
testing, and say what you did not test.
+3
View File
@@ -2,6 +2,7 @@
rem Benchmark this Strata install the way it is used and write a report you can share (docs/COMMUNITY_BENCHMARKS.md).
rem It starts the model with your settings (or uses the one already running), takes about 10 minutes and changes nothing.
rem Options: --clients 4 (several requests at once), --long (a 32K prompt), --compare <older results folder>.
rem This is the quick speed check. The standard benchmark (docs/BENCHMARK_PROTOCOL.md), whose results other people can compare with, is `strata bench run`.
setlocal
title Strata - benchmark
cd /d "%~dp0"
@@ -10,6 +11,8 @@ if not exist ".venv\Scripts\python.exe" (
pause
exit /b 1
)
echo Quick speed check. For results other people can compare with (the standard benchmark, 20 to 30 minutes), run: .venv\Scripts\python.exe strata.py bench run
echo.
".venv\Scripts\python.exe" tools\strata_bench.py %*
set "RC=%ERRORLEVEL%"
pause
+221 -8
View File
@@ -8,7 +8,7 @@
# below refuses anything older: silently building for a pre-Ampere arch would produce a binary that runs
# and is wrong about its own performance, which is the worst of both outcomes.
cmake_minimum_required(VERSION 3.24)
project(strata VERSION 0.1.45 LANGUAGES CXX)
project(strata VERSION 0.1.46.3 LANGUAGES CXX)
# EXPERIMENTAL: the Intel Arc engine, the SYCL port in sycl/ (PR #423; icpx + oneMKL, docs/INTEL_ARC.md). It is its
# own project that takes the unported sources from src/; with this on, nothing below is configured.
option(STRATA_ENABLE_SYCL "EXPERIMENTAL: build the Intel Arc (SYCL) engine from sycl/ - docs/INTEL_ARC.md" OFF)
@@ -48,6 +48,17 @@ if(STRATA_PORTABLE AND MSVC)
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded")
endif()
# #3202: Windows hands a program its command line in the system's ANSI code page (cp1251 / cp866 on a Russian Windows, cp1252 on an
# English one), but the engine reads every path as UTF-8 (the run configs and the server write UTF-8). A model folder with
# Cyrillic letters arrived as other bytes, or as "?" where the page has no such letter, and the model did not open. This manifest
# makes the process's ANSI code page UTF-8 (Windows 10 1903 and newer; older Windows ignores it and nothing changes), so the
# command line, fopen and std::ifstream all take UTF-8.
function(strata_utf8_manifest target)
if(MSVC)
target_sources(${target} PRIVATE ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/cmake/strata-utf8.manifest)
endif()
endfunction()
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
@@ -76,6 +87,7 @@ option(STRATA_PREFILL_MMQ "Build the optional GGML MMQ prefill path for HIP" OFF
# CUDA_MODULE_LOADING=EAGER, and these took ~3.8 MB of VRAM - 3 IQ3_XXS expert slots on a P100 - from every model); on
# HIP a code object loads on first use, so they cost other models nothing there (R9700: the same slots). Used at run
# time with STRATA_MMQ_KQ=1 (opt-in: it changes the prompt's rounding against the FP16 path).
option(STRATA_MMQ_TQ_OVERLAY "Gyro trellis MMQ prompt kernels on an upstream llama.cpp: an overlay of its ggml-cuda headers written at configure time (cmake/strata_ggml_tq_overlay.cmake)" ON)
option(STRATA_MMQ_KQUANTS "Build the MMQ prompt kernels for Q4_K / Q5_K / Q5_1 / Q6_K: the experts of Unsloth's K-quant files, and on HIP the dense GGUF projections (STRATA_DENSE_MMQ)" ON)
# Q6_K gate/up in the grouped expert kernels (Unsloth's UD-Q6_K_XL, and layer 2 of UD-Q5_K_XL; docs/QUANTS.md). On by
# default since MODEL-UPDATE Phase 1: one more instance per grouped kernel in iq_kernels.cu, +19 KB of sm_60 code,
@@ -254,6 +266,9 @@ target_link_libraries(strata-dequant PRIVATE strata_artifact)
# this file is now nothing but the CLI entry point, and its inherited guard spells that as a "selftest"
target_compile_definitions(strata-dequant PRIVATE STRATA_DEQUANT_SELFTEST)
if(STRATA_BUILD_TESTS)
add_executable(batch_telemetry_test src/core/batch_telemetry_test.cpp)
target_include_directories(batch_telemetry_test PRIVATE include)
add_test(NAME batch_telemetry_test COMMAND batch_telemetry_test)
add_executable(logit_bias_test src/core/logit_bias_test.cpp)
target_include_directories(logit_bias_test PRIVATE include)
add_test(NAME logit_bias_test COMMAND logit_bias_test)
@@ -289,6 +304,11 @@ if(STRATA_BUILD_TESTS)
target_include_directories(split_rules_test PRIVATE include)
add_test(NAME split_rules_test COMMAND split_rules_test)
# the prompt planner's host-memory model (#2824): header-only, no GPU
add_executable(prefill_plan_test tests/program/prefill_plan_test.cpp)
target_include_directories(prefill_plan_test PRIVATE include)
add_test(NAME prefill_plan_test COMMAND prefill_plan_test)
# the default CPU prefill share is not armed when every expert is resident (#1595): header-only rule, no GPU
add_executable(share_rules_test tests/prefill/share_rules_test.cpp)
target_include_directories(share_rules_test PRIVATE include)
@@ -356,6 +376,7 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
# reference is the whole point - a kernel checked against another copy of itself proves nothing.
add_library(strata_kernels STATIC src/kernels/cuda/dequant_s2.cu src/kernels/cuda/s2_gemv.cu
src/kernels/cuda/s_gemv.cu
src/kernels/cuda/s_gemv_pair.cu
src/kernels/cuda/s2_gemv_quads.cu
src/kernels/cuda/s2_gemv_fast.cu
src/kernels/cuda/router_top10.cu
@@ -424,6 +445,13 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
target_include_directories(qsa_prompt_attn_parity PRIVATE ${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES})
target_link_libraries(qsa_prompt_attn_parity PRIVATE strata_kernels CUDA::cudart)
endif()
# PR #600: the Volta (sm_70) prompt attention's latency pipeline against the FP32 kernel it replaces. Only in the
# experimental sm_60/sm_70 build, where prompt_attn_v70_kernel is compiled in.
if(STRATA_BUILD_TESTS AND STRATA_ENABLE_CUDA AND STRATA_EXPERIMENTAL_SM60)
add_executable(qsa_prompt_attn_sm70_bench bench/qsa_prompt_attn_sm70_bench.cpp)
target_include_directories(qsa_prompt_attn_sm70_bench PRIVATE ${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES})
target_link_libraries(qsa_prompt_attn_sm70_bench PRIVATE strata_kernels CUDA::cudart)
endif()
# #512: the top-k's active-block bound (Turing's dispatch, forced on any card by STRATA_TOPK_ACTIVE_ANY) against
# the capacity rule, selected ids bitwise
if(STRATA_BUILD_TESTS AND STRATA_ENABLE_CUDA)
@@ -553,7 +581,7 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
src/core/remote_expert_opt.cu
src/core/expert_cache.cpp src/core/native_head.cpp src/core/native_dense.cpp src/core/verify.cpp src/core/mtp.cpp
src/core/conversation_snapshot.cpp src/core/conversation_state.cpp src/core/conversation_memory.cpp src/core/peer_experts.cpp
src/core/arch_defaults.cpp src/core/vmm.cpp src/core/conversation_file.cpp)
src/core/arch_defaults.cpp src/core/nv_defaults.cpp src/core/vmm.cpp src/core/conversation_file.cpp)
target_include_directories(strata_engine PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_link_libraries(strata_engine PUBLIC strata_core strata_kernels strata_kernels_cpu ${_strata_gpu_runtime_target})
if(STRATA_BUILD_TESTS AND STRATA_ENABLE_CUDA AND NOT STRATA_HIP_GFX906)
@@ -683,6 +711,11 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
endif()
target_include_directories(strata_prefill PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include ${_strata_gpu_include_directories})
target_link_libraries(strata_prefill PUBLIC strata_engine ${_strata_gpu_blas_target} ${_strata_gpu_runtime_target})
if(STRATA_ENABLE_CUDA)
# v100/gdn-chunk: the chunked Volta GDN prompt recurrence. The dispatch (src/prefill/kernels.cu) is
# Volta-only (cc 7.0) and gated on STRATA_EXPERIMENTAL_SM60; the path itself stays opt-in (STRATA_GDN_CHUNK).
target_sources(strata_prefill PRIVATE src/prefill/gdn_chunk.cu)
endif()
if(STRATA_ENABLE_HIP AND STRATA_HIPBLASLT_AVAILABLE)
target_compile_definitions(strata_prefill PRIVATE STRATA_HIPBLASLT_AVAILABLE=1)
target_link_libraries(strata_prefill PRIVATE roc::hipblaslt)
@@ -700,8 +733,16 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
endif()
add_executable(strata src/program/generate.cpp)
strata_utf8_manifest(strata)
target_include_directories(strata PRIVATE ${_strata_gpu_include_directories})
target_link_libraries(strata PRIVATE strata_engine strata_prefill strata_spec ${_strata_gpu_runtime_target})
if(WIN32 AND STRATA_ENABLE_HIP)
# 0.2.1.2: the ROCm math libraries live in <package>\rocm\bin, which is on PATH only when the launcher starts the engine; load them on first
# use (main() adds that folder to the DLL search first) so `strata.exe --version` works from the package folder. amdhip64_7.dll and the
# C++ runtime sit beside the exe (tools/hip/package_windows.py) and stay normal imports.
target_link_libraries(strata PRIVATE delayimp)
target_link_options(strata PRIVATE "LINKER:/DELAYLOAD:hipblas.dll" "LINKER:/DELAYLOAD:libhipblaslt.dll" "LINKER:/DELAYLOAD:rocblas.dll")
endif()
# NEW-MODELS-PLAN.md T1: Qwen3.6-35B-A3B's reference path (one GPU, the model resident, plain kernels), checked
# against llama.cpp by tools/qwen35/; the engine itself does not run the family yet
@@ -710,7 +751,10 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
target_link_libraries(strata-q36ref PRIVATE strata_kernels strata_warnings ${_strata_gpu_runtime_target})
# the engine path for the same family (serve protocol, prompt chunks, decode graph): src/qwen35/q36_engine.cu, a
# library that `strata` switches to for a qwen35moe model (the family switch at the top of its main) and that
# strata-q36 wraps on its own
# strata-q36 wraps on its own. Not on gfx906 (#3172, #3271): it calls cuBLAS entry points the wave64 compat layer does
# not map (cublasSgemm, cublasGemmStridedBatchedEx) and its kernels assume 32-lane warps; main() guards every use with
# STRATA_HAVE_QWEN35, so a gfx906 build links and runs the Qwen3.8 family, and `strata --families` says so
if(NOT STRATA_HIP_GFX906)
add_library(strata_qwen35 STATIC src/qwen35/q36_engine.cu)
target_include_directories(strata_qwen35 PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include PRIVATE ${_strata_gpu_include_directories})
target_link_libraries(strata_qwen35 PRIVATE strata_prefill strata_kernels strata_warnings ${_strata_gpu_runtime_target})
@@ -720,9 +764,11 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
target_compile_definitions(strata_qwen35 PRIVATE STRATA_Q36_CPU_POOL=1)
endif()
add_executable(strata-q36 src/qwen35/q36_main.cpp)
strata_utf8_manifest(strata-q36)
target_link_libraries(strata-q36 PRIVATE strata_qwen35 ${_strata_gpu_runtime_target})
target_link_libraries(strata PRIVATE strata_qwen35)
target_compile_definitions(strata PRIVATE STRATA_HAVE_QWEN35=1)
endif()
if(STRATA_ENABLE_HIP)
enable_testing()
@@ -800,6 +846,9 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
add_test(NAME hip_prefill_hcd_exact_parity COMMAND hip_prefill_hcd_exact_parity)
set_tests_properties(hip_prefill_hcd_exact_parity PROPERTIES SKIP_RETURN_CODE 77)
list(APPEND _strata_hip_tests hip_prefill_hcd_exact_parity)
# the HC write-back kernel timed at the prompt path's shapes with an output checksum (not a ctest; STRATA_GRW_V picks a variant)
add_executable(hip_prefill_gr_write_bench tests/hip/prefill_gr_write_bench.cpp)
target_link_libraries(hip_prefill_gr_write_bench PRIVATE strata_prefill strata_hip_runtime)
# S6: the AMD fast router against the portable kernel, bitwise
add_executable(hip_router_fast tests/hip/router_fast.cpp)
target_link_libraries(hip_router_fast PRIVATE strata_kernels strata_hip_runtime)
@@ -943,6 +992,15 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
target_include_directories(iq_multi_parity PRIVATE ${_strata_gpu_include_directories})
target_link_libraries(iq_multi_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
add_test(NAME iq_multi_parity COMMAND iq_multi_parity)
set_tests_properties(iq_multi_parity PROPERTIES ENVIRONMENT "STRATA_KQ_SPLIT=1") # the Q4_K / Q5_K / Q8_0 Split path against the old kernels
# the same Split path on the other routes it can meet: the generic multi kernels (no V2K) and the fused gate/up + SwiGLU kernel, and the sub-16-row
# gate/up route switched off (a K-quant row has 160 dots, not the 80 the sub-16 rows assume: STRATA_KQ_SPLIT=1 must stay bitwise on all of them)
add_test(NAME iq_multi_parity_kq_nov2k COMMAND iq_multi_parity)
set_tests_properties(iq_multi_parity_kq_nov2k PROPERTIES ENVIRONMENT "STRATA_KQ_SPLIT=1;STRATA_EXPERT_V2K=0")
add_test(NAME iq_multi_parity_kq_swiglu COMMAND iq_multi_parity)
set_tests_properties(iq_multi_parity_kq_swiglu PROPERTIES ENVIRONMENT "STRATA_KQ_SPLIT=1;STRATA_GU_SWIGLU=1")
add_test(NAME iq_multi_parity_kq_nosub16 COMMAND iq_multi_parity)
set_tests_properties(iq_multi_parity_kq_nosub16 PROPERTIES ENVIRONMENT "STRATA_KQ_SPLIT=1;STRATA_NO_SUB16_GU=1")
# the prompt path's DeltaNet recurrence: gdn_rec_kh_kernel against gdn_rec_cols_pipe_kernel, bitwise (GPU,
# synthetic, no model; --bench adds timings). NVIDIA only: cp.async and the CUDA occupancy API (not on gfx906)
if(STRATA_ENABLE_CUDA AND NOT STRATA_HIP_GFX906)
@@ -998,6 +1056,14 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
add_test(NAME gfx906_gdn_rec_head COMMAND gfx906_gdn_rec_head)
set_tests_properties(gfx906_gdn_rec_head PROPERTIES TIMEOUT 120)
endif()
if(STRATA_ENABLE_CUDA)
# the Volta prompt experts (gemm_iq_f16_grouped, STRATA_PF_WMMA) against a double reference; skipped below sm_70
add_executable(pf_wmma_parity src/kernels/pf_wmma_parity.cpp)
target_include_directories(pf_wmma_parity PRIVATE ${_strata_gpu_include_directories})
target_link_libraries(pf_wmma_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
add_test(NAME pf_wmma_parity COMMAND pf_wmma_parity)
set_tests_properties(pf_wmma_parity PROPERTIES SKIP_RETURN_CODE 77)
endif()
add_executable(shared_expert_parity src/kernels/shared_expert_parity.cpp)
target_include_directories(shared_expert_parity PRIVATE ${_strata_gpu_include_directories})
target_link_libraries(shared_expert_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
@@ -1032,6 +1098,22 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
target_link_libraries(gdn_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
add_test(NAME gdn_parity COMMAND gdn_parity --selftest)
# v100/gdn-chunk: the chunked GDN prompt recurrence vs the token recurrence and a host reference (GPU,
# synthetic, no model). The chunked path changes the summation order - FP32-level, not bit-exact - so the
# test asserts measured error bounds, and --selftest pins them.
if(STRATA_BUILD_TESTS AND STRATA_ENABLE_CUDA)
add_executable(gdn_chunk_parity src/kernels/gdn_chunk_parity.cpp)
target_include_directories(gdn_chunk_parity PRIVATE ${_strata_gpu_include_directories})
target_link_libraries(gdn_chunk_parity PRIVATE strata_prefill ${_strata_gpu_runtime_target})
add_test(NAME gdn_chunk_parity COMMAND gdn_chunk_parity --selftest)
set_tests_properties(gdn_chunk_parity PROPERTIES SKIP_RETURN_CODE 77)
# v100/gdn-chunk: the micro benchmark (CUDA events, real GDN shapes): chunked vs recurrent prompt path.
add_executable(gdn_chunk_bench bench/gdn_chunk_bench.cpp)
target_include_directories(gdn_chunk_bench PRIVATE ${_strata_gpu_include_directories})
target_link_libraries(gdn_chunk_bench PRIVATE strata_prefill ${_strata_gpu_runtime_target})
endif()
add_executable(s2_gemv_q8_parity src/kernels/s2_gemv_q8_parity.cpp)
target_include_directories(s2_gemv_q8_parity PRIVATE ${_strata_gpu_include_directories})
target_link_libraries(s2_gemv_q8_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
@@ -1139,6 +1221,16 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
target_link_libraries(bf16_gemv_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
add_test(NAME bf16_gemv_parity COMMAND bf16_gemv_parity --selftest)
# the V100 fusion work: every fused kernel against its unfused pipeline, bitwise, and the before/after
# CUDA-event timings at decode shapes (`--bench`). It is a .cu because it carries the unfused reference
# kernels it compares against.
if(STRATA_BUILD_TESTS AND STRATA_ENABLE_CUDA)
add_executable(fusions_parity bench/micro/fusions_parity.cu)
target_include_directories(fusions_parity PRIVATE ${_strata_gpu_include_directories})
target_link_libraries(fusions_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
add_test(NAME fusions_parity COMMAND fusions_parity)
endif()
add_executable(s_gemv_q8k_parity src/kernels/s_gemv_q8k_parity.cpp)
target_include_directories(s_gemv_q8k_parity PRIVATE ${_strata_gpu_include_directories})
target_link_libraries(s_gemv_q8k_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
@@ -1241,6 +1333,11 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
target_link_libraries(platform_memory_test PRIVATE strata_core ${_strata_gpu_runtime_target})
add_test(NAME platform_memory_test COMMAND platform_memory_test)
# The pinned-memory headroom (pin_budget.hpp) against a simulated ceiling; skips without a GPU.
add_executable(pin_budget_test src/core/pin_budget_test.cpp)
target_link_libraries(pin_budget_test PRIVATE strata_core ${_strata_gpu_runtime_target})
add_test(NAME pin_budget_test COMMAND pin_budget_test)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
add_executable(pinned_shared_test src/core/pinned_shared_test.cpp)
target_link_libraries(pinned_shared_test PRIVATE strata_core ${_strata_gpu_runtime_target})
@@ -1359,7 +1456,7 @@ if(STRATA_BUILD_TESTS OR STRATA_BUILD_CONVERSATION_TESTS)
endif()
add_library(strata_kernels_cpu STATIC src/kernels/cpu/expert.cpp src/kernels/cpu/pool.cpp
src/kernels/cpu/expert_layout.cpp src/kernels/cpu/q2_avx2.cpp)
src/kernels/cpu/expert_layout.cpp src/kernels/cpu/q2_avx2.cpp src/kernels/cpu/hadamard.cpp)
target_include_directories(strata_kernels_cpu PUBLIC include)
# Plan v0.3 P6: only the kernel files get the AVX-512 / AVX2 flags; the pool and the probe run on any x86-64,
# and the engine picks the kernels at run time (cpu_avx512_ok).
@@ -1467,7 +1564,8 @@ if(STRATA_NATIVE_EXPERTS)
target_sources(strata_kernels_cpu PRIVATE src/kernels/cpu/native_expert.cpp src/kernels/cpu/iq_avx512.cpp
src/kernels/cpu/iq_avx2.cpp src/kernels/cpu/kq_avx2.cpp
src/kernels/cpu/kq_avx1.cpp src/kernels/cpu/q2_avx1.cpp src/kernels/cpu/iq_avx1.cpp
src/kernels/cpu/nvfp4_avx512.cpp src/kernels/cpu/nvfp4_avx2.cpp)
src/kernels/cpu/nvfp4_avx512.cpp src/kernels/cpu/nvfp4_avx2.cpp
src/kernels/cpu/tq_cpu.cpp src/kernels/cpu/tq_avx2.cpp src/kernels/cpu/tq_avx512.cpp)
target_include_directories(strata_kernels_cpu PRIVATE third_party/ggml)
if(MSVC)
set_source_files_properties(src/kernels/cpu/iq_avx512.cpp src/kernels/cpu/nvfp4_avx512.cpp
@@ -1475,6 +1573,8 @@ if(STRATA_NATIVE_EXPERTS)
set_source_files_properties(src/kernels/cpu/iq_avx2.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX2")
set_source_files_properties(src/kernels/cpu/kq_avx2.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX2")
set_source_files_properties(src/kernels/cpu/nvfp4_avx2.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX2")
set_source_files_properties(src/kernels/cpu/tq_avx2.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX2")
set_source_files_properties(src/kernels/cpu/tq_avx512.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX512")
# the router lookahead's AVX1 dot (older CPUs; called behind cpu_avx1_ok): no FMA, no 256-bit integer ops
set_source_files_properties(src/kernels/cpu/kq_avx1.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX")
# the older CPUs' Q2_0 rows and activation quantizer (q2_avx1.cpp, issue #1699): 128-bit integer lanes,
@@ -1488,6 +1588,10 @@ if(STRATA_NATIVE_EXPERTS)
set_source_files_properties(src/kernels/cpu/iq_avx2.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma;-mf16c")
set_source_files_properties(src/kernels/cpu/kq_avx2.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma;-mf16c")
set_source_files_properties(src/kernels/cpu/nvfp4_avx2.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma;-mf16c")
# the trellis kernels: AVX2 + FMA, and AVX-512 F/BW/VL/VBMI/VNNI (exactly what cpu_avx512_ok() checks)
set_source_files_properties(src/kernels/cpu/tq_avx2.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma;-mf16c")
set_source_files_properties(src/kernels/cpu/tq_avx512.cpp PROPERTIES COMPILE_OPTIONS
"-mavx512f;-mavx512bw;-mavx512vl;-mavx512vnni;-mavx512vbmi;-mavx2;-mfma;-mf16c")
set_source_files_properties(src/kernels/cpu/kq_avx1.cpp PROPERTIES COMPILE_OPTIONS "-mavx")
# the older CPUs' Q2_0 rows and activation quantizer (q2_avx1.cpp, issue #1699): SSE4.2 + AVX with
# AVX2/FMA/F16C explicitly off, so a -march=native build host cannot leak AVX2 code into the file that
@@ -1551,6 +1655,34 @@ if(STRATA_NATIVE_EXPERTS)
add_test(NAME q8k_quant_parity_avx COMMAND ${CMAKE_COMMAND} -E env STRATA_FORCE_ISA=avx $<TARGET_FILE:q8k_quant_parity>)
endif()
set(_strata_mmq_types q2_0 iq2_xxs iq2_xs iq2_s iq3_xxs iq3_s iq4_nl iq4_xs q8_0)
# APR: the agention llama.cpp fork has MMQ instances for its trellis expert types (f62fdead0); any other ggml
# tree leaves them to the FP16 prompt path
if(STRATA_GGML_DIR AND EXISTS "${STRATA_GGML_DIR}/ggml/src/ggml-cuda/template-instances/mmq-instance-tqk6.cu")
list(APPEND _strata_mmq_types tq2_t tqk6 tqk7)
set(_strata_mmq_tq ON)
elseif(STRATA_MMQ_TQ_OVERLAY AND (STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP))
# an upstream llama.cpp: the trellis MMQ instances come from an overlay of the pinned ggml-cuda headers written at
# configure time (cmake/strata_ggml_tq_overlay.cmake); off with a message if the llama.cpp tree is not the pinned one
if(STRATA_GGML_DIR)
set(_strata_tq_llama "${STRATA_GGML_DIR}")
else()
set(_strata_tq_llama "${strata_llamacpp_SOURCE_DIR}")
endif()
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/strata_ggml_tq_overlay.cmake)
set(_strata_tq_overlay_dir "${CMAKE_BINARY_DIR}/ggml-tq-overlay")
strata_ggml_tq_overlay("${_strata_tq_llama}" "${_strata_tq_overlay_dir}")
if(STRATA_TQ_OVERLAY_OK)
configure_file(third_party/ggml/ggml-common-tq.h "${_strata_tq_overlay_dir}/ggml-common-tq.h" COPYONLY)
configure_file(src/kernels/cuda/tq_lut_i8.cuh "${_strata_tq_overlay_dir}/tq_lut_i8.cuh" COPYONLY)
set(_strata_mmq_tq ON)
set(_strata_mmq_tq_overlay ON)
message(STATUS "trellis MMQ (Gyro prompts): overlay on the pinned ggml-cuda in ${_strata_tq_overlay_dir}")
if(STRATA_BUILD_TESTS)
add_test(NAME ggml_tq_overlay COMMAND ${CMAKE_COMMAND} -DLLAMA=${_strata_tq_llama}
-DOUT=${CMAKE_BINARY_DIR}/ggml-tq-overlay-test -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/ggml_tq_overlay_test.cmake)
endif()
endif()
endif()
# #296: OrcaRouter Q4_K_S's Q5_0 experts (Q4_K and Q5_1 come with STRATA_MMQ_KQUANTS on CUDA, S5's branch)
if(STRATA_ORCA_Q4KS_MMQ)
list(APPEND _strata_mmq_types q5_0)
@@ -1565,11 +1697,15 @@ if(STRATA_NATIVE_EXPERTS)
set(_strata_gg ${strata_llamacpp_SOURCE_DIR}/ggml)
endif()
set(_strata_mmq ${_strata_gg}/src/ggml-cuda)
set(_strata_quantize_cu ${_strata_mmq}/quantize.cu) # the overlay's copy when the trellis overlay is on: it includes mmq.cuh
if(_strata_mmq_tq_overlay)
set(_strata_quantize_cu ${_strata_tq_overlay_dir}/quantize.cu)
endif()
# #136: the Q2_0 pack's prompt experts on Strata's own int8 kernels (moe_fused.cu, opt-in STRATA_PF_FUSED=1; CUDA
# only: the HIP branch below keeps MMQ, and an sm_75 device or an sm_75-only build reports them unavailable)
add_library(strata_mmq STATIC src/prefill/moe_mmq.cu src/prefill/moe_fused.cu src/prefill/moe_fused_iq.cu
src/prefill/mmq_nvfp4_w4a8.cu
src/prefill/ggml_cuda_host.cu ${_strata_mmq}/quantize.cu)
src/prefill/ggml_cuda_host.cu ${_strata_quantize_cu})
# q8_0: the drafter's matrices (E-9); STRATA_ORCA_Q4KS_MMQ adds q5_0 (#296, in _strata_mmq_types), and
# STRATA_MMQ_KQUANTS q4_k q5_k q5_1 q6_k, the experts of Unsloth's UD-Q4/Q5/Q6_K_XL (docs/QUANTS.md; the dispatch
# follows eddoursul/Strata 7706c1b). MODEL-UPDATE Phase 1: on CUDA they are a library of their own, strata_kq
@@ -1587,6 +1723,13 @@ if(STRATA_NATIVE_EXPERTS)
foreach(_t IN LISTS _strata_mmq_cuda_types)
target_sources(strata_mmq PRIVATE ${_strata_mmq}/template-instances/mmq-instance-${_t}.cu)
endforeach()
if(_strata_mmq_tq_overlay)
foreach(_t tq2_t tqk6 tqk7)
target_sources(strata_mmq PRIVATE ${_strata_tq_overlay_dir}/template-instances/mmq-instance-${_t}.cu)
endforeach()
# first on the include path: the patched mmq.cuh / ggml.h shadow the pinned ones for this target only
target_include_directories(strata_mmq BEFORE PRIVATE ${_strata_tq_overlay_dir} ${_strata_tq_overlay_dir}/include)
endif()
# NVFP4: W4A8 (default) in mmq_nvfp4_w4a8.cu with the plain architectures; the opt-in W4A4 needs RTX 50's FP4 MMA,
# which exists only in the arch-specific target, so its one unit alone is built with 12x -> 12xa (the default
# binary is unchanged and runs on sm_121; a card without that image falls back to W4A8 at run time)
@@ -1690,6 +1833,9 @@ if(STRATA_NATIVE_EXPERTS)
if(STRATA_ORCA_Q4KS_MMQ)
target_compile_definitions(strata_mmq PRIVATE STRATA_ORCA_Q4KS_MMQ=1)
endif()
if(_strata_mmq_tq)
target_compile_definitions(strata_mmq PRIVATE STRATA_MMQ_TQ=1)
endif()
target_link_libraries(strata_prefill PUBLIC strata_mmq)
target_compile_definitions(strata_prefill PUBLIC STRATA_PREFILL_MMQ=1 STRATA_PREFILL_FUSED=1)
if(STRATA_BUILD_TESTS)
@@ -1743,6 +1889,10 @@ if(STRATA_NATIVE_EXPERTS)
set(_strata_gg "${strata_llamacpp_SOURCE_DIR}/ggml")
endif()
set(_strata_mmq "${_strata_gg}/src/ggml-cuda")
set(_strata_quantize_cu "${_strata_mmq}/quantize.cu") # the overlay's copy when the trellis overlay is on: it includes mmq.cuh
if(_strata_mmq_tq_overlay)
set(_strata_quantize_cu "${_strata_tq_overlay_dir}/quantize.cu")
endif()
if(NOT EXISTS "${_strata_mmq}/common.cuh" OR NOT EXISTS "${_strata_mmq}/quantize.cu")
message(FATAL_ERROR "The selected GGML source is missing the ggml-cuda MMQ sources: ${_strata_mmq}")
endif()
@@ -1753,7 +1903,7 @@ if(STRATA_NATIVE_EXPERTS)
src/prefill/moe_fused_iq.cu
src/prefill/moe_f16_kq.cu
src/prefill/ggml_cuda_host.cu
"${_strata_mmq}/quantize.cu")
"${_strata_quantize_cu}")
if(STRATA_MMQ_KQUANTS)
# the same K-quant instances as the CUDA branch: on HIP they serve the dense GGUF projections
# of Gemm::native (STRATA_DENSE_MMQ=1), the mixed-quant packs' attention/PLE tensors
@@ -1762,6 +1912,11 @@ if(STRATA_NATIVE_EXPERTS)
foreach(_t IN LISTS _strata_mmq_types)
list(APPEND _strata_mmq_sources "${_strata_mmq}/template-instances/mmq-instance-${_t}.cu")
endforeach()
if(_strata_mmq_tq_overlay)
foreach(_t tq2_t tqk6 tqk7)
list(APPEND _strata_mmq_sources "${_strata_tq_overlay_dir}/template-instances/mmq-instance-${_t}.cu")
endforeach()
endif()
set_source_files_properties(${_strata_mmq_sources} PROPERTIES LANGUAGE HIP)
# moe_fused.cu is Strata's own CUDA-shaped source: it takes the HIP compat headers, ggml's MMQ sources do not
set_source_files_properties(src/prefill/moe_fused.cu src/prefill/moe_fused_iq.cu src/prefill/moe_f16_kq.cu
@@ -1770,6 +1925,9 @@ if(STRATA_NATIVE_EXPERTS)
target_include_directories(strata_mmq
PRIVATE "${_strata_gg}/include" "${_strata_gg}/src" "${_strata_mmq}"
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
if(_strata_mmq_tq_overlay)
target_include_directories(strata_mmq BEFORE PRIVATE ${_strata_tq_overlay_dir} ${_strata_tq_overlay_dir}/include)
endif()
target_compile_definitions(strata_mmq PRIVATE GGML_USE_HIP=1)
if(STRATA_MMQ_KQUANTS)
target_compile_definitions(strata_mmq PRIVATE STRATA_MMQ_KQUANTS=1)
@@ -1777,6 +1935,9 @@ if(STRATA_NATIVE_EXPERTS)
if(STRATA_ORCA_Q4KS_MMQ)
target_compile_definitions(strata_mmq PRIVATE STRATA_ORCA_Q4KS_MMQ=1)
endif()
if(_strata_mmq_tq)
target_compile_definitions(strata_mmq PRIVATE STRATA_MMQ_TQ=1)
endif()
target_link_libraries(strata_mmq PUBLIC ggml-base hip::host roc::hipblas)
target_link_libraries(strata_prefill PUBLIC strata_mmq)
target_compile_definitions(strata_prefill PUBLIC STRATA_PREFILL_MMQ=1 STRATA_PREFILL_FUSED=1)
@@ -1825,7 +1986,8 @@ if(STRATA_NATIVE_EXPERTS)
if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
add_executable(native_expert_parity src/kernels/native_expert_parity.cpp)
target_include_directories(native_expert_parity PRIVATE ${_strata_gpu_include_directories})
target_link_libraries(native_expert_parity PRIVATE strata_kernels strata_kernels_cpu strata_artifact ${_strata_gpu_runtime_target})
# strata_engine: the --load-check mode runs the engine's start-up checks (NativeDense, check_experts_gguf)
target_link_libraries(native_expert_parity PRIVATE strata_engine strata_kernels strata_kernels_cpu strata_artifact ${_strata_gpu_runtime_target})
add_executable(native_expert_bench src/kernels/native_expert_bench.cpp)
target_include_directories(native_expert_bench PRIVATE ${_strata_gpu_include_directories})
target_link_libraries(native_expert_bench PRIVATE strata_kernels strata_kernels_cpu strata_artifact ${_strata_gpu_runtime_target})
@@ -1864,6 +2026,16 @@ if(STRATA_NATIVE_EXPERTS)
set_tests_properties(native_expert_parity_${_name} PROPERTIES TIMEOUT 120)
endforeach()
add_test(NAME native_expert_parity_q5_1_min COMMAND native_expert_parity --q5_1-min)
# APR: the trellis expert types and the Hadamard activation rotation, when ggml comes from the agention fork
# (the only tree with these types; marked by its ggml-cuda/tq.cuh)
if(STRATA_GGML_DIR AND EXISTS "${STRATA_GGML_DIR}/ggml/src/ggml-cuda/tq.cuh")
foreach(_pair tqk6/tqk7 tq2_t/tq2_t)
string(REPLACE "/" "_" _name "${_pair}")
add_test(NAME native_expert_parity_${_name} COMMAND native_expert_parity --synthetic ${_pair})
add_test(NAME native_expert_parity_hadamard_${_name} COMMAND native_expert_parity --synthetic-hadamard ${_pair})
set_tests_properties(native_expert_parity_${_name} native_expert_parity_hadamard_${_name} PROPERTIES TIMEOUT 300)
endforeach()
endif()
# #290: the BF16 token embedding (--embd-gguf) gathered and dequantized bit for bit
add_test(NAME native_expert_parity_bf16_embd COMMAND native_expert_parity --bf16-embd)
endif()
@@ -1917,6 +2089,22 @@ if(STRATA_BUILD_TESTS)
set_tests_properties(q2_bitplane_parity PROPERTIES ENVIRONMENT "STRATA_Q2_BITPLANE=1")
endif()
if(STRATA_NATIVE_EXPERTS)
# APR port: a Hadamard-folded trellis expert (TQ2_T / TQK6 / TQK7) on the CPU path vs llama.cpp's graph for the
# same weights (synthetic; needs STRATA_GGML_DIR = a ggml with the trellis types, else it reports and fails)
add_executable(native_hadamard_test src/kernels/cpu/native_hadamard_test.cpp)
target_link_libraries(native_hadamard_test PRIVATE strata_kernels_cpu strata_artifact)
add_test(NAME native_hadamard_test COMMAND native_hadamard_test)
set_tests_properties(native_hadamard_test PROPERTIES TIMEOUT 300)
# the CPU trellis kernels (tq_cpu.hpp): AVX2 / AVX-512 vs the scalar kernel bit for bit, vs ggml-cpu's vec_dot
# and to_float, plain and with the Hadamard rotation; `tq_cpu_parity --bench` is the micro-benchmark
add_executable(tq_cpu_parity src/kernels/cpu/tq_cpu_parity.cpp)
target_link_libraries(tq_cpu_parity PRIVATE strata_kernels_cpu)
add_test(NAME tq_cpu_parity COMMAND tq_cpu_parity)
add_test(NAME tq_cpu_parity_avx2 COMMAND ${CMAKE_COMMAND} -E env STRATA_FORCE_AVX2=1 $<TARGET_FILE:tq_cpu_parity>)
set_tests_properties(tq_cpu_parity tq_cpu_parity_avx2 PROPERTIES TIMEOUT 300)
endif()
add_executable(expert_parity src/kernels/cpu/expert_parity.cpp)
target_link_libraries(expert_parity PRIVATE strata_kernels_cpu)
# The pack is a REQUIRED input, not an optional one: this test compares against a real expert blob, and a
@@ -1927,6 +2115,14 @@ if(STRATA_BUILD_TESTS)
add_executable(pool_test src/kernels/cpu/pool_test.cpp)
target_link_libraries(pool_test PRIVATE strata_kernels_cpu)
add_test(NAME pool_test COMMAND pool_test --selftest WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
# the default CPU expert worker count on mock CPUs (8c/16t, 16c/32t, 64c/128t, 2-socket 128c/256t, ...)
add_executable(pool_plan_test src/kernels/cpu/pool_plan_test.cpp)
target_include_directories(pool_plan_test PRIVATE include)
add_test(NAME pool_plan_test COMMAND pool_plan_test)
# #3204: when the HIP start-up warm-up of hipBLAS may run (pure policy, no GPU)
add_executable(warm_policy_test src/prefill/warm_policy_test.cpp)
target_include_directories(warm_policy_test PRIVATE include)
add_test(NAME warm_policy_test COMMAND warm_policy_test)
# issue #29: tiny batches with sleeping workers, as on a big-VRAM card; a hang fails it
add_executable(pool_stress src/kernels/cpu/pool_stress.cpp)
target_link_libraries(pool_stress PRIVATE strata_kernels_cpu)
@@ -1996,6 +2192,23 @@ if(STRATA_HIP_GFX906)
file(GLOB _strata_mmq_cu ${_strata_mmq}/*.cu ${_strata_mmq}/template-instances/*.cu)
list(APPEND _strata_cu ${_strata_mmq_cu})
target_compile_definitions(strata_mmq PRIVATE GGML_USE_HIP)
# #3172: strata_mmq_w4a4 is built from llama.cpp's headers too; without GGML_USE_HIP its common.cuh took the CUDA vendor header
# (cuda.h, cuda_bf16.h: not found). Here it only reports that W4A4 (RTX 50's FP4 MMA) is not available.
if(TARGET strata_mmq_w4a4)
target_compile_definitions(strata_mmq_w4a4 PRIVATE GGML_USE_HIP)
endif()
# #3271: three more link errors of a 0.1.44+ gfx906 build, all sources that strata_mmq lists but the glob above never saw:
# the trellis overlay's quantize.cu and instances (they are generated into the build tree, so the glob missed them: CMake drops a
# .cu source whose language is not enabled) and moe_f16_kq.cu, which moe_fused_iq.cu calls (a stub here: no gfx11 device code, so
# f16kq::covers() answers false and the int8 kernels run, as on every other card)
if(_strata_mmq_tq_overlay)
set(_strata_tq_hip ${_strata_tq_overlay_dir}/quantize.cu)
foreach(_t tq2_t tqk6 tqk7)
list(APPEND _strata_tq_hip ${_strata_tq_overlay_dir}/template-instances/mmq-instance-${_t}.cu)
endforeach()
list(APPEND _strata_cu ${_strata_tq_hip})
endif()
target_sources(strata_mmq PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/prefill/moe_f16_kq.cu)
endif()
set_source_files_properties(${_strata_cu} PROPERTIES LANGUAGE HIP)
endif()
+122
View File
@@ -0,0 +1,122 @@
# Contributing to Strata
**AI assistance is welcome. The contributor is responsible for understanding the change and supplying real evidence.**
These rules apply to issues, pull requests, benchmark reports, and the agents helping prepare them.
You should be able to explain every changed line and answer review questions about it.
## Before starting
1. Search [issues](https://github.com/Niko1221/Strata/issues) and
[pull requests](https://github.com/Niko1221/Strata/pulls), including closed discussions. Add new evidence to an
existing thread when it covers your problem.
2. Read [AGENTS.md](AGENTS.md) and the documentation for the component you want to change.
3. Get maintainer agreement on scope before substantial features, refactors, architecture changes, or changes to
defaults. Describe the user problem, proposed behavior, and tradeoffs in an issue first. Agreement on scope
does not guarantee a merge. Focused fixes and benchmark reports can go straight to a PR.
4. Keep one problem per PR. Update that PR after feedback; do not open competing attempts for the same change.
Do not resubmit a declined proposal under a different title or through another agent. If new evidence changes
the situation, bring it to the original discussion and let a maintainer decide whether to reopen it.
## Report a problem people can investigate
Use the matching [issue form](https://github.com/Niko1221/Strata/issues/new/choose). A bug report should include:
- The smallest steps or command that reproduce the problem, and the expected and actual behavior.
- Strata version or commit, OS, GPU and VRAM, and relevant model, quantization, and configuration.
- The exact error and relevant logs, with credentials and private content removed.
- Whether you reproduced it yourself. Label a finding based only on reading code or a scanner as
**unconfirmed static analysis**, and explain the suspected path without presenting it as an observed failure.
Do not invent reproduction steps, errors, affected hardware, or severity. If you cannot reproduce something,
say what you inspected and what would verify the hypothesis. Feature requests should explain a concrete user
need and the current workaround. Questions should say what you tried and where you got stuck.
Example structure, with placeholders to replace with actual observations:
```text
Problem: <one observed behavior>
Environment: <commit, OS, GPU/VRAM, model/quant, relevant settings>
Reproduce: <exact command or short sequence>
Expected: <documented or intended behavior>
Actual: <observed behavior and exact error>
Evidence: <sanitized log attachment>
Reproduction status: <reproduced locally OR unconfirmed static analysis>
```
## Titles
Every issue and pull request title starts with its area in square brackets, then a short description: `[Engine]`, `[GUI]`, `[TUI]`,
`[Benchmark]`, `[Docs]` or `[Other]`. For example `[Engine] fix decode stop on long prompts` or `[GUI] keep the model list after a tab
switch`. The issue forms ask for the area and a check on the title fails when the prefix is missing.
## Prepare a focused pull request
Lead with the problem and resulting behavior. Explain why the approach is needed, the important limits, and
any compatibility effects. Link the issue and scope agreement when applicable. A file-by-file narration or
an AI-generated summary of the diff is not a substitute for this explanation.
- Keep unrelated formatting, cleanup, dependency changes, and speculative abstractions out of the PR.
- Add documentation where it changes what a user or developer needs to know. Avoid repeated summaries,
speculative roadmaps, and generated documentation with no actionable information.
- Preserve existing behavior outside the intended change. New features are opt-in; follow the default-path
and performance requirements in [AGENTS.md](AGENTS.md).
- Read the complete diff, including generated files, before submitting. Remove debugging output, secrets,
private paths, and accidental files.
- Prefer a small relevant regression check over tests that only restate the implementation. Documentation-only
changes need link, example, and consistency checks; they do not require GPU builds.
## Show the validation you actually performed
List exact commands, the revision and environment tested, and observed outcomes. Keep **passed**, **failed**,
and **not run** separate. Include relevant output or a log attachment. A command you suggest running is not
evidence that it passed; a CI run on a different revision is not validation of the submitted code.
Build every backend a code change touches, including CUDA, HIP, and SYCL where applicable, before requesting
review. For changes that need hardware execution, report that execution separately from compilation. A mock,
static check, or successful build does not establish hardware correctness. If you lack a required environment,
keep the PR as a draft, identify the missing checks, and ask for targeted help in the same thread. Do not claim
compatibility you have not tested.
Performance claims need before/after runs on the same hardware and workload, with every changed setting
identified. Preserve the individual runs, failures, and variability. Follow the
[community measurement guide](docs/COMMUNITY_BENCHMARKS.md); standard submissions will use the
[benchmark protocol](docs/BENCHMARK_PROTOCOL.md) once it is activated. One fast run does not justify changing a
default or claiming a speedup for other machines.
Example PR body; placeholders are not test results:
```text
Problem and result: <trigger; behavior before and after the change>
Related discussion: <issue URL and scope agreement, when required>
Approach: <why this fix; relevant compatibility effects>
Validation:
Passed: <exact command, revision/environment, observed result, log>
Failed: <exact command and failure, or none observed>
Not run: <check, reason, and what remains unverified>
Limits: <known limitations or remaining work>
```
## Rules for AI-assisted work
Use agents to help investigate, implement, and explain work that you can review. Before submitting, verify
their claims against the source, actual command output, or a cited primary reference.
Never submit fabricated measurements, invented test results, fake logs, or copied claims you have not checked.
Do not turn a repository scan into a batch of speculative issues or PRs. Consolidate related findings, establish
their effect, and submit the useful evidence. Do not present an agent's confidence or another model's approval
as validation. Keep review replies concise and address the requested change or evidence directly.
Maintainers may request a smaller change, return a PR to draft, or close a submission that lacks evidence,
duplicates existing work, or ignores agreed scope. A smaller, understandable contribution with honest limits
is easier to review and maintain.
## Share a benchmark
A results-only PR should contain the human report, raw measurements, sanitized configuration, and supporting
logs. Keep it separate from engine changes and include poor results and failed requests.
The [current community benchmark guide](docs/COMMUNITY_BENCHMARKS.md) remains available for measurements made
with today's tools. The [new protocol](docs/BENCHMARK_PROTOCOL.md) defines the draft standard and future approval
workflow. Its draft status must remain visible: existing reports are not automatically standard or approved.
+89 -105
View File
@@ -1,119 +1,103 @@
# syntax=docker/dockerfile:1
# Strata, built FROM SOURCE (NVIDIA CUDA 13). The engine is compiled here, packaged as an engine component package and then
# installed into the same runtime image as the ready-made ones (docker/Dockerfile): same Terminal, same entrypoint, same /data volume.
#
# Strata: Qwen3.8-Flash-Next on NVIDIA GPUs (RTX 30/40/50, 12+ GB VRAM; two or
# three cards can share one model, 8 GB each - docs/MULTI_GPU.md).
# Most people want the ready-made images instead (nothing compiled): docs/DOCKER.md.
#
# The engine is compiled during docker build, so the first container start only
# downloads the model (~70 GB) and starts the server. docker build has no GPU,
# so the CUDA architectures are fixed here instead of read from nvidia-smi: the
# engine is a fat binary with a cubin per listed arch, and the runtime picks the
# one matching your card. Narrow CUDA_ARCHITECTURES to your card for a faster
# build; a card outside the set needs a rebuild with its own arch.
# docker build -t strata:src . # RTX 20/30/40/50 (compiles for each, ~25 min)
# docker build -t strata:src --build-arg CUDA_ARCHITECTURES=86 . # one card only (faster); 86 = RTX 30, 89 = RTX 40, 120 = RTX 50
# docker run --rm --gpus all -p 127.0.0.1:8080:8080 --ulimit memlock=-1 --shm-size 2g --stop-timeout 120 \
# -v strata-data:/data -e MODEL=IQ3_XXS -e API_KEY=change-me-please-123 strata:src
#
# Build:
# docker build -t strata .
# docker build -t strata --build-arg CUDA_ARCHITECTURES=89 . # RTX 40 only
# docker build has no GPU, so the CUDA architectures are given here instead of read from nvidia-smi. The engine is a fat binary with
# a machine-code part per listed architecture (plus PTX for the newest, which later cards can JIT), and the runtime picks the one
# matching your card. Narrow CUDA_ARCHITECTURES to your card for a faster build. Native code for the CPU that builds the image is NOT
# used (STRATA_PORTABLE=ON), so the image runs on any x86-64 CPU with AVX2.
#
# Run (host needs an NVIDIA driver >= 580 and nvidia-container-toolkit):
# docker run --rm --gpus all \
# -p 8080:8080 \
# --ulimit memlock=-1 \
# -v strata-data:/data \
# -e MODEL=IQ2_XS \
# strata
#
# Setup choices are env vars, read by docker-entrypoint.sh: FAMILY, MODEL, CONTEXT,
# VISION (no | yes | cpu), KV (int8 | q4_0 | k8v4), GPU (one card) or GPUS ("0,2"
# or "all", with LAYER_SPLIT), LOW_RAM (auto | on | off), HOST, PORT, API_KEY,
# GGUF_DIR (GGUF files you already have), RESIDENT_BUDGET_GIB, KV_STREAMING.
#
# Only the model files, the prepared pack, the MTP layer and the install config
# live in the /data volume; the engine is part of the image. Strata loads 32-62 GB
# into RAM, so a capped container needs -e LOW_RAM=on: setup.py reads the RAM from
# /proc/meminfo, which here is the host's total, not the container's limit. Add an
# API key before exposing the port to a network: -e API_KEY=<secret>. Pass
# -e REINSTALL=1 to change the model settings later.
#
# --gpus all on a host with two usable cards: setup takes both (the layer split is
# its recommended default). Pin one card with -e GPU=0, or name them with
# -e GPUS=0,2. A volume set up for one card switches to the pair on its first start
# on a two-card host unless GPU or GPUS pins it. LOW_RAM=on runs on one card.
# Plain legacy-builder features only (no heredocs, no --mount). AMD (HIP) and Intel (SYCL) source builds: docs/AMD_HIP.md, docs/INTEL.md.
FROM nvidia/cuda:13.0.0-devel-ubuntu24.04
# STRATA_EXECV=1: setup.py replaces itself with the server, so the server is PID 1
# and docker stop's SIGTERM reaches it (see setup.start). Normal Linux starts, which
# don't set it, keep spawning the server as a child.
ENV DEBIAN_FRONTEND=noninteractive PYTHONUNBUFFERED=1 LANG=C.UTF-8 STRATA_EXECV=1
ARG CUDA_IMAGE=nvidia/cuda:13.0.0-devel-ubuntu24.04
ARG RUNTIME_BASE=ubuntu:24.04
# ---- 1. compile the engine and make its component package
FROM ${CUDA_IMAGE} AS engine-build
ENV DEBIAN_FRONTEND=noninteractive PYTHONUNBUFFERED=1 PATH=/venv/bin:$PATH
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential ca-certificates curl git libatomic1 libgomp1 \
python3 python3-pip python3-venv unzip \
build-essential ca-certificates git libgomp1 libatomic1 python3 python3-pip python3-venv unzip \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /opt/strata
WORKDIR /src
COPY . .
# RTX 20 (75), RTX 30 (86), RTX 40 (89), RTX 50 (120), plus 80 for A-series. CMakeLists
# refuses anything below 75. BUILD_VISION=0 skips the image encoder build.
ARG CUDA_ARCHITECTURES=75;80;86;89;120
ARG CUDA_ARCHITECTURES="75;80;86;89;120"
ARG BUILD_VISION=1
ARG ENGINE_VERSION=
RUN python3 -m venv /venv && /venv/bin/pip install --no-cache-dir -r requirements.txt -r requirements-build.txt
# llama.cpp at the pinned commit (ggml for the engine, mtmd for pictures), exactly as setup.py fetches it
RUN /venv/bin/python -c "import setup; print(setup.get_llama_cpp())"
RUN set -e; \
real=""; for a in $(echo "$CUDA_ARCHITECTURES" | tr ';,' ' '); do real="$real;${a}-real"; done; real=${real#;}; \
last=$(echo "$CUDA_ARCHITECTURES" | tr ';,' ' ' | awk '{print $NF}'); \
/venv/bin/cmake -G Ninja -S . -B build -DCMAKE_BUILD_TYPE=Release -DSTRATA_ENABLE_CUDA=ON -DSTRATA_BUILD_TESTS=OFF \
-DSTRATA_NATIVE_EXPERTS=ON -DSTRATA_PORTABLE=ON -DSTRATA_Q6K_EXPERTS=ON -DSTRATA_MMQ_KQUANTS=ON \
"-DCMAKE_CUDA_ARCHITECTURES=${real};${last}-virtual" -DSTRATA_GGML_DIR=/src/third_party/llama.cpp; \
/venv/bin/cmake --build build --target strata strata_kq -j "$(nproc)"; \
if [ "$BUILD_VISION" = "1" ]; then \
/venv/bin/cmake -G Ninja -S tools/vision -B build-vision -DCMAKE_BUILD_TYPE=Release -DLLAMA_DIR=/src/third_party/llama.cpp \
-DSTRATA_VISION_CUDA=ON -DSTRATA_PORTABLE=ON -DGGML_CUDA_NCCL=OFF "-DCMAKE_CUDA_ARCHITECTURES=${real};${last}-virtual"; \
/venv/bin/cmake --build build-vision --target strata-vision -j "$(nproc)"; \
fi
RUN set -e; \
mkdir -p /stage /engine; \
cp build/strata /stage/; \
[ -f build/libstrata_kq.so ] && cp build/libstrata_kq.so /stage/ || true; \
[ -f build-vision/bin/strata-vision ] && cp build-vision/bin/strata-vision /stage/ || true; \
ver="${ENGINE_VERSION:-$(sed -n 's/^[[:space:]]*project([^)]*VERSION[[:space:]]*\([0-9][0-9.]*\).*/\1/p' CMakeLists.txt | head -1)}"; \
echo "{\"version\": \"$ver\", \"source\": \"docker build\", \"archs\": \"$CUDA_ARCHITECTURES\", \"cuda\": \"13.0\", \"portable\": true}" > /stage/BUILD.json; \
python3 packaging/collect_libs.py --binary build/strata --search /usr/local/cuda/lib64 --out /stage --also 'libcublasLt.so*'; \
archs=""; for a in $(echo "$CUDA_ARCHITECTURES" | tr ';,' ' '); do archs="$archs --arch sm_$a"; done; \
last=$(echo "$CUDA_ARCHITECTURES" | tr ';,' ' ' | awk '{print $NF}'); \
python3 packaging/package_engine.py --src /stage --backend cuda --version "$ver" --out /engine $archs --arch "compute_$last" \
--min-driver nvidia=580.65 --variant cuda13 --platform linux-x86_64 --build-id "docker-source-${ver}-cuda13.0" --include SYSTEM-LIBS.txt --include 'libstrata_kq*' \
--include strata-vision; \
for d in /engine/*/; do python3 packaging/package_engine.py --verify "$d"; python3 packaging/binary_deps.py "$d"; done
RUN python3 -m venv .venv \
&& .venv/bin/pip install --no-cache-dir --upgrade pip \
&& .venv/bin/pip install --no-cache-dir -r requirements.txt \
&& chmod +x setup.sh docker-entrypoint.sh
# llama.cpp at the pinned commit, then the engine and the image encoder, built
# exactly the way setup.py builds them: native code for the CPU that builds the
# image, so build it on the PC it runs on. BUILD.json is what setup.py reads to
# decide whether an engine is current: source=local with a matching src hash
# means the first start reuses it instead of recompiling.
RUN .venv/bin/python - <<'PYEOF'
import json, os, pathlib, shutil
import setup
llama = setup.get_llama_cpp()
nvcc, _ = setup.find_nvcc()
arch = os.environ.get("CUDA_ARCHITECTURES", "75;80;86;89;120").strip().strip('"').replace(",", ";")
vision = "gpu" if os.environ.get("BUILD_VISION", "1") == "1" else "none"
setup.cmake_build(setup.ROOT, setup.ROOT / "build", "strata",
["-DSTRATA_ENABLE_CUDA=ON", "-DSTRATA_Q6K_EXPERTS=ON", "-DSTRATA_MMQ_KQUANTS=ON", "-DSTRATA_BUILD_TESTS=OFF",
f"-DCMAKE_CUDA_ARCHITECTURES={arch}", f"-DCMAKE_CUDA_COMPILER={nvcc}",
f"-DSTRATA_GGML_DIR={llama}"], None, "build-strata.bat")
if vision != "none":
setup.cmake_build(setup.ROOT / "tools" / "vision", setup.ROOT / "build-vision", "strata-vision",
[f"-DLLAMA_DIR={llama}", "-DSTRATA_VISION_CUDA=ON", "-DSTRATA_PORTABLE=OFF",
f"-DCMAKE_CUDA_ARCHITECTURES={arch}", f"-DCMAKE_CUDA_COMPILER={nvcc}"], None, "build-vision.bat")
eng = setup.ROOT / "engine"
eng.mkdir(exist_ok=True)
shutil.copy2(setup.ROOT / "build" / setup.EXE, eng / setup.EXE)
if (setup.ROOT / "build" / setup.KQ_LIB).exists(): # the K-quant MMQ prompt library (UD-Q4/Q5/Q6_K_XL files)
shutil.copy2(setup.ROOT / "build" / setup.KQ_LIB, eng / setup.KQ_LIB)
if vision != "none":
shutil.copy2(setup.ROOT / "build-vision" / "bin" / setup.VEXE, eng / setup.VEXE)
bindir = pathlib.Path(nvcc).parent
meta = {"source": "local", "version": setup.source_version(),
"archs": [int(a.split("-")[0]) for a in arch.split(";") if a.split("-")[0].isdigit()], "vision": vision,
"cuda_dirs": [str(d) for d in (bindir, bindir / "x64", bindir.parent / "lib64") if d.is_dir()],
"src": setup.source_hash(setup.ENGINE_SOURCES),
"vision_src": setup.source_hash(setup.VISION_SOURCES) if vision != "none" else None}
(eng / "BUILD.json").write_text(json.dumps(meta, indent=1))
PYEOF
# the cmake trees are build-time only; the engine itself is what the container needs
RUN rm -rf build build-vision
# ---- 2. the Terminal (the release archive, built here)
FROM ${RUNTIME_BASE} AS terminal
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends python3 ca-certificates && rm -rf /var/lib/apt/lists/*
COPY . /src/
RUN python3 /src/packaging/package_terminal.py --out /out --cache /cache \
&& mkdir -p /opt/strata \
&& tar -xzf /out/strata-terminal-*-linux-x86_64.tar.gz -C /opt/strata \
&& mv /opt/strata/strata-terminal-* /opt/strata/terminal \
&& /opt/strata/terminal/bin/strata --version
# llama.cpp at the commit setup.py pins: fetched here, so the first start does not depend on GitHub. Only gguf-py (the model preparation
# tools) and the file setup.py looks for (ggml/CMakeLists.txt) are kept: 170 MB become a few
RUN cd /opt/strata/terminal && STRATA_CONTROL_INSTALL=1 STRATA_INSTALL_DIR=/opt/strata/seed PYTHONNOUSERSITE=1 \
/opt/strata/terminal/python/bin/python3 -c "import setup; print(setup.get_llama_cpp())" \
&& rm -rf /opt/strata/seed/third_party/*.zip* \
&& cd /opt/strata/seed/third_party/llama.cpp \
&& find . -mindepth 1 -maxdepth 1 ! -name gguf-py ! -name ggml ! -name 'LICENSE*' -exec rm -rf {} + \
&& find ggml -mindepth 1 -depth ! -type d ! -name CMakeLists.txt -delete && find ggml -mindepth 1 -type d -empty -delete \
&& du -sh /opt/strata/seed
# ---- 3. the runtime image (the same as docker/Dockerfile's, FLAVOR=cuda)
FROM ${RUNTIME_BASE}
ARG STRATA_VERSION=dev
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends tini ca-certificates libgomp1 libatomic1 libnuma1 zlib1g \
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/*
COPY --from=terminal /opt/strata/terminal /opt/strata/terminal
COPY --from=terminal /opt/strata/seed /opt/strata/seed
COPY --from=engine-build /engine/ /opt/strata/components/engine/
COPY docker/entrypoint.sh docker/healthcheck.sh /opt/strata/
COPY docker/strata-wrapper.sh /usr/local/bin/strata
RUN chmod 0755 /opt/strata/entrypoint.sh /opt/strata/healthcheck.sh /usr/local/bin/strata
ENV STRATA_COMPONENTS_DIR=/opt/strata/components STRATA_INSTALL_DIR=/data/install XDG_STATE_HOME=/data/state \
XDG_CONFIG_HOME=/data/config HOME=/data/home PORT=8080 LANG=C.UTF-8 PYTHONUNBUFFERED=1 PYTHONNOUSERSITE=1 STRATA_IN_DOCKER=1 \
NVIDIA_VISIBLE_DEVICES=all NVIDIA_DRIVER_CAPABILITIES=compute,utility
VOLUME ["/data"]
EXPOSE 8080
# /health is answered before the API key gate, so it works with or without one.
# The port only opens after the model loads (1-3 minutes, longer on a first run),
# so the start period is generous: a too short one marks a still-loading container
# unhealthy and a restart policy would kill it mid-download.
HEALTHCHECK --interval=30s --timeout=5s --start-period=600s --retries=3 \
CMD curl -fs "http://127.0.0.1:${PORT:-8080}/health" || exit 1
ENTRYPOINT ["./docker-entrypoint.sh"]
LABEL org.opencontainers.image.title="Strata" com.strata.flavor="cuda" com.strata.build="from-source" org.opencontainers.image.version="${STRATA_VERSION}"
HEALTHCHECK --interval=30s --timeout=8s --start-period=2h --retries=3 CMD ["/opt/strata/healthcheck.sh"]
STOPSIGNAL SIGTERM
ENTRYPOINT ["/usr/bin/tini", "--", "/opt/strata/entrypoint.sh"]
CMD ["run"]
+336 -171
View File
@@ -1,239 +1,404 @@
<h1 align="center">Strata</h1>
<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">
</picture>
</p>
[English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja.md) · **Deutsch** · [Français](README.fr.md) · [Español](README.es.md) · [Português](README.pt-BR.md)
<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>
<p align="center"><b>Ein KI-Modell mit 125 Milliarden Parametern auf deinem eigenen Gaming-PC</b><br>
NVIDIA- oder AMD-Grafikkarte (ab 12 GB) · Windows oder Linux · kostenlos und Open Source</p>
<h3 align="center">Ein Spitzenmodell auf deinem eigenen Gaming-PC</h3>
<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="Ein Voxel-Pagodengarten, den das Modell in Strata geschrieben hat, läuft im Browser"></a><br>
<p align="center">Unterstützt NVIDIA, AMD, Intel · Windows, Linux, Mac · kostenlos und Open Source</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>
</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>
Strata lässt **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** auf einem normalen PC laufen.
Das ist ein großes, kluges KI-Modell, das sonst meist einen Server braucht. Es chattet, schreibt Code, versteht
Bilder und arbeitet mit deinen Apps und Coding-Agents zusammen. Nichts verlässt deinen PC.
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.
## Wie schnell ist es?
**Neu hier?** Sieh dir an, [was du brauchst](#was-du-brauchst), und [installiere dann die App](#option-a-die-strata-app).
Wir haben es auf zwei ganz normalen Gaming-PCs gemessen. Ein Token ist etwa ¾ eines Wortes.
- **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).
- **Schreibt Antworten:** wie schnell die Antwort in einem kurzen Chat erscheint. 60 Tokens pro Sekunde sind
schneller, als du lesen kannst.
- **Liest deinen Prompt:** wie schnell es aufnimmt, was du schickst (hier ein Dokument, Code oder Chatverlauf mit
32K Tokens).
<table>
<tr><th>NVIDIA: RTX 5070 (12 GB), Ryzen 5 7600, 64 GB RAM</th><th>AMD: RX 9070 XT (16 GB), Ryzen 9 3900X, 47 GB RAM</th></tr>
<tr><td>
| Größe | Schreibt Antworten | Liest deinen 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 |
</td><td>
| Größe | Schreibt Antworten | Liest deinen 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 |
</td></tr>
</table>
NVIDIA: Q2_0 mit Engine 0.1.36, die anderen Zeilen mit 0.1.26 (4K Antworten, 32K Prompts). Die vollständigen
Tabellen stehen in [DETAILS.md](docs/DETAILS.md#speed-measured). Eine Karte mit mehr VRAM ist schneller: Eine
RTX 3090 (24 GB) sollte etwa 100-140 Tokens pro Sekunde schreiben. Lange Chats und andere Karten:
[Tempo jedes Modells](docs/MODELS.md#how-fast-is-each-size), [Ergebnisse aus der Community](docs/COMMUNITY_BENCHMARKS.md).
<p align="center"><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><br>
<sub>Strata ist kostenlos. Wenn es auf deinem PC gut läuft, hält ein Kaffee die Arbeit daran am Laufen.</sub></p>
Version 0.2.1 · Engine 0.1.46
## Was du brauchst
| | |
**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 |
| --- | --- |
| **Grafikkarte** | **NVIDIA** GeForce RTX 20, 30, 40 oder 50 Serie, oder **AMD** Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700 oder RX 6800 / 6900 Serie. Sie braucht **12 GB VRAM oder mehr**. |
| **RAM** | 32 GB oder mehr. Dein RAM entscheidet, [welches Modell](#welches-modell-soll-ich-nehmen) passt. Mit 64 GB laufen alle Größen. |
| **Festplatte** | Etwa 80 GB frei. Nimm wenn möglich eine SSD: Der erste Start geht dann viel schneller. |
| **System** | Windows 10 / 11 oder Linux und ein aktueller Grafiktreiber von NVIDIA oder AMD. |
| **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) |
Alles andere richtet der Installer ein. Zwei oder drei Karten können sich das Modell teilen ([Multi-GPU](docs/MULTI_GPU.md)).
Außerdem nötig:
Experimentell, von Community-Mitgliedern auf ihren eigenen Rechnern geschrieben und getestet:
- etwa 80 GB freier Speicherplatz (mit einer SSD startet es beim ersten Mal viel schneller)
- Windows 10 / 11, Linux oder macOS
- ein aktueller Grafiktreiber
- **Ältere Grafikkarten** (Tesla P40 / V100, GTX 10, Radeon VII / MI50, RX 6700 XT, RX 5500 XT): [Ältere GPUs](docs/OLDER_GPUS.md).
- **Intel Arc**, unter Linux aus dem Quellcode gebaut: [Intel Arc](docs/INTEL_ARC.md).
- **AMD Ryzen AI Max (Strix Halo)**, unter Linux aus dem Quellcode gebaut: [Strix Halo](docs/STRIX_HALO.md).
- **Ältere Prozessoren ohne AVX2**: Sie funktionieren, aber langsam. [Ältere CPUs](docs/INSTALL.md#older-cpus-experimental).
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).
Die vollständige Liste: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
## Installation
## Installieren
Such dir eine Option aus. Alle starten dieselbe Engine und nutzen dieselben Modelle.
### Lass deine KI es einrichten
### Option A: die Strata-App
Nutzt du einen KI-Coding-Assistenten (Claude Code, Cursor, Codex, GitHub Copilot, ...)? Füge dort diesen Text ein:
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.
| System | Deine Grafikkarte | Datei zum Herunterladen |
| --- | --- | --- |
| **Windows 10 / 11** | NVIDIA RTX, Treiber 580 oder neuer | [`Strata-0.2.1-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-cuda13.exe) |
| | NVIDIA, ältere Karte oder Treiber älter als 580 | [`Strata-0.2.1-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-cuda12.exe) |
| | AMD Radeon RX 9000 Serie oder Radeon AI PRO R9700 | [`Strata-0.2.1-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-hip.exe) |
| | AMD Radeon RX 7000 Serie oder Ryzen AI Max (Strix Halo) | [`Strata-0.2.1-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-hip-rdna3.exe) |
| **Linux** | NVIDIA RTX, Treiber 580 oder neuer | [`Strata-0.2.1-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-nvidia-cuda13.AppImage) |
| | NVIDIA, ältere Karte oder Treiber älter als 580 | [`Strata-0.2.1-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-nvidia-cuda12.AppImage) |
| | AMD Radeon RX 9000 Serie oder Radeon AI PRO R9700 | [`Strata-0.2.1-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-rx9070.AppImage) |
| | AMD Radeon RX 7000 Serie | [`Strata-0.2.1-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-rx7900.AppImage) |
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.1-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-strixhalo.AppImage) |
| **Mac** (Apple Silicon, macOS 15 oder neuer) | eingebaut | [`Strata-0.2.1-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-mac.dmg) |
**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).
Danach:
- **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".
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.
### Option B: das Terminal (eine Zeile)
Das Terminal ist der Befehl `strata`, gedacht für Skripte, Server und KI-Agents.
Linux und Mac:
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
```
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:
```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.
```
Er prüft deine Grafikkarte, deinen RAM und deine Festplatte und wählt das passende Modell. Dann installiert und
startet er es und sagt dir, wie du deine Apps verbindest. KI-Tools können Strata auch über seinen
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.
### Oder mach es selbst
Du willst es lieber selbst bauen? Siehe [Aus dem Quellcode bauen](#aus-dem-quellcode-bauen).
[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 im Strata-Ordner **`./setup.sh`** aus.
## Modelle
**Mac (Apple Silicon, M1 bis M5, macOS 15 oder neuer):** Installiere einmal Apples Command Line Tools (`xcode-select --install` im Terminal; Xcode selbst brauchst du nicht). Öffne dann das Terminal im Strata-Ordner und führe **`./setup.sh`** aus. Es baut die Mac-Engine auf deinem Mac (etwa 7 Minuten), stellt dieselben Fragen, lädt das Modell und startet es. Beim nächsten Mal startet `./setup.sh` sofort. Welche Größe zu deinem Mac passt und wie schnell es ist: [docs/MAC.md](docs/MAC.md).
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.
Die Schritte sind für NVIDIA und AMD gleich. Der Installer erkennt deine Karte und richtet die passende Engine
dafür ein. Er stellt dir ein paar Fragen:
### Wenig Speicher? Probier Gyro
- welches Modell und welche Größe,
- wie viel Kontext (wie viel Text sich das Modell merkt),
- ob es Bilder lesen soll.
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) (von agentionai) ist die kleinste Möglichkeit, dieses Modell auszuführen.
Drück jedes Mal Enter, um die empfohlene Antwort zu nehmen. Dann lädt er das Modell herunter (etwa 70 GB) und
startet es. Bricht der Download ab, starte ihn einfach neu: Er macht dort weiter, wo er aufgehört hat. Dein Browser
öffnet die Strata-App unter `http://127.0.0.1:8080`.
| 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 auf einer 12-GB-Karte und auf einer R9700 17-21 % langsamer als Q2_0. |
> **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 reserviert einen Teil davon für die Grafikkarte. Das ist normal.
> Warte und schließ das Fenster nicht. Im Fenster siehst du, was Strata gerade tut.
Gyro läuft auf NVIDIA- und AMD-Karten (bei AMD experimentell), nicht auf Intel Arc und nicht auf dem Mac. Details und Messwerte:
[docs/GYRO.md](docs/GYRO.md). Starte es mit `strata launch --model gyro-s`.
**Beim nächsten Mal** startest du wieder `START-HERE.bat` (oder `./setup.sh`). Es startet sofort und lädt nichts
doppelt herunter. Schließ das Fenster, um das Modell zu stoppen. `UPDATE.bat` (`./update.sh`) aktualisiert Strata,
ohne es zu starten. Updates, Docker, mehrere Karten, wo die Dateien landen und alle Optionen:
[docs/INSTALL.md](docs/INSTALL.md).
### Die anderen Größen
## Welches Modell soll ich nehmen?
Der Installer empfiehlt eins passend zu deinem RAM. Dasselbe Modell gibt es in mehreren Größen, mehr oder weniger
stark komprimiert. Kleinere Größen sind schneller. Größere sind etwas klüger.
| Dein RAM | Nimm | Warum |
| Dein Speicher (RAM + VRAM) | Nimm | Hinweise |
| --- | --- | --- |
| **32 GB** | **Coder** | passt in 32 GB und ist für Code gemacht (mit einer 24-GB-Karte laufen auch Q2_0 und IQ2_XS) |
| **48 GB** | **IQ2_XS** (oder Q2_0, das schnellste) | die größeren Größen passen nicht |
| **64 GB** | **IQ2_XS** (empfohlen) oder IQ3_XXS / IQ3_S | alle Größen passen; IQ3_S ist das beste und das langsamste |
| **96 GB oder mehr** | **IQ3_S** oder Unsloths UD-IQ4_XS (~4 Bit) | Platz für die größten Größen, auch wenn alles andere offen ist |
| **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 17-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 |
- **[Coder](docs/MODELS.md#coder):** eine Version für Code, bei der die Hälfte der Experten entfernt wurde. Sie
erreicht 91 % des SWE-bench-Verified-Werts des vollen Modells (von ihren Autoren gemessen) und passt in 32 GB RAM.
Außerhalb von Code ist sie schwächer, auch bei chinesischem und anderem CJK-Text (#438). Nimm dafür Q2_0, IQ2_XS
oder IQ3_S, die alle Experten behalten.
- **[Swift 1.5](docs/MODELS.md#swift-15):** ein Fine-Tune, das viel kürzer nachdenkt, bevor es antwortet. Du
bekommst die Antwort früher, bei etwa gleicher Qualität.
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** die ~4-Bit-Version von Unsloth, in der Qualität
zwischen IQ3_S und UD-Q4_K_XL. 94 GB Download. Mit weniger als ~80 GB RAM liest Strata einen Teil davon während
der Antwort von der SSD, dort ist es also langsamer (eine NVMe-SSD hilft).
- **[Unsloth UD-Q4_K_XL, UD-Q5_K_XL, UD-Q6_K_XL und Q8_0](docs/MODELS.md#unsloths-files-ud-iq4_xs-to-q8_0):** 4 bis 8 Bit, näher am vollen Modell
(93-95 % das gleiche nächste Token wie Q8_0). Schnell, wenn alle Experten in den RAM passen (~88 GB für UD-Q4_K_XL
bis ~136 GB für Q8_0); mit weniger RAM liest Strata einen Teil davon während der Antwort von der SSD, was viel
langsamer ist (7-8.5 Tokens/s für UD-Q4_K_XL auf einem PC mit 64 GB).
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** Das richtest du von Hand
ein. Es steht nicht im Menü des Installers.
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.
Größen, Downloads und was wohin passt: [docs/MODELS.md](docs/MODELS.md). Um später ein weiteres Modell
hinzuzufügen, starte `SETUP.bat` (Linux: `./setup.sh --setup`).
Weitere Auswahl:
## So benutzt du es
- **[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.
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="Der Monitor-Tab der Strata-App neben einem Coding-Agent"><br>
<sub>Der <b>Monitor</b> der Strata-App (links), während ein Coding-Agent den Pagodengarten aus dem Video schreibt (rechts)</sub></p>
Alle Größen, Downloads und was wo läuft: [docs/MODELS.md](docs/MODELS.md).
- **Im Browser:** Öffne `http://127.0.0.1:8080`. Dort gibt es **Chat**, einen Live-**Monitor** für das Modell und
deine GPU/CPU/RAM und **About** mit den Einstellungen und Adressen.
- **Deine Apps und Coding-Agents:** Füge einen „OpenAI-kompatiblen“ Anbieter mit der Basis-URL
**`http://127.0.0.1:8080/v1`** hinzu. Jeder API-Key und jeder Modellname funktioniert.
- Apps, die die API von Anthropic nutzen: `http://127.0.0.1:8080/v1/messages` (Claude Code:
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`).
- Codex CLI und andere Apps, die die OpenAI Responses API nutzen: `/v1/responses`
([Einrichtung](docs/DETAILS.md#the-responses-api-and-codex-cli)).
- **Nachdenken:** Wähle **aus, niedrig, mittel oder hoch** im Chat-Menü oder unter „Reasoning Effort“ in deiner
App. Aus ist am schnellsten. Hoch ist am besten für schwierige Fragen.
- **Bilder:** Antworte im Setup mit Ja auf „Images?“. Klick dann im Chat auf **Picture** oder hänge Bilder in deiner
App an. AMD-Karten lesen Bilder unter Linux über den Prozessor; unter Windows geht das noch nicht.
- **Vom Handy oder einem anderen PC:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`. Setz immer einen
Key.
- **Eine Anfrage nach der anderen:** Standardmäßig beantwortet Strata eine Anfrage, die anderen warten. Um mehrere
gleichzeitig zu beantworten, setz `"parallel": 2` ([BATCHING.md](docs/BATCHING.md)). Auf einer 12-GB-Karte wird
dadurch jede Antwort langsamer.
- **Lange Prompts:** Strata liest die erste Nachricht eines Chats komplett, etwa 1 Minute pro 30,000 Tokens.
Folgenachrichten starten in Sekunden.
## Wie schnell ist es?
Mehr: [wo deine Chats gespeichert werden](docs/INSTALL.md#where-things-are-stored), [die API](docs/DETAILS.md#using-it).
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).
### Optional: mehr Tempo mit einer Grafikkarte, wenn das ganze Modell in den RAM passt
| 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 |
Zwei Opt-in-Einstellungen (von RubenTorney, #1904 und #1905). Sie helfen, wenn du eine Grafikkarte nutzt und dein RAM alle Experten hält (Stratas Arena-Modus, zum Beispiel Q2_0 oder IQ3_XXS auf einem PC mit 64 GB). Öffne deine Konfiguration `strata-<model>.json` im Strata-Datenordner ([wo](docs/INSTALL.md#where-things-are-stored)) und füge hinzu:
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.
- **Schnellere Ausgabe:** `"--adapt-async", "1"` in die Liste `"args"`. Der Experten-Cache wird dann in einem Hilfs-Thread aktualisiert, statt die Antwort anzuhalten. Gemessen auf einer RTX 5070 (Windows, Q2_0): +4-5 % Ausgabe, +7-10 % bei Code und langen Dokumenten. Antworten können leicht vom Standard abweichen (sie bleiben von Lauf zu Lauf gleich).
- **Schnelleres Prompt-Lesen unter Windows:** `"STRATA_PIN_BY_NEED": "1"` im Block `"env"`. Windows kann nur etwa den halben RAM für die Grafikkarte sperren; damit werden zuerst die Experten gesperrt, die die Karte wirklich braucht. Gemessen auf der RTX 5070: +4,4 % Prompt-Lesen.
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 danach neu (`START-HERE.bat` oder `./setup.sh`). Zum Rückgängigmachen entferne die beiden Einträge.
Starte Strata nach der Änderung neu. Zum Rückgängigmachen entfernst du die beiden Einträge.
## Etwas ist schiefgelaufen?
</details>
- **Mein PC ist beim ersten Start von Strata eingefroren.** Das ist normal, während das Modell lädt. Warte und
schließ das Fenster nicht. Nach 10 Minuten immer noch eingefroren? Starte den PC neu, schließ andere Programme und
versuch es noch einmal, oder nimm eine kleinere Größe.
- **Es hat beim Herunterladen oder Installieren aufgehört.** Starte `START-HERE.bat` (oder `./setup.sh`) noch
einmal. Es macht dort weiter, wo es aufgehört hat.
- **Es ist sehr langsam und die Festplattenlampe blinkt ständig, oder es meldet „the engine stopped
unexpectedly“.** Dein PC hat nicht genug freien RAM. Schließ andere Programme (Browser brauchen viel) oder nimm
eine kleinere Größe (Q2_0 oder IQ2_XS).
- **Es meldet, dass Port 8080 schon belegt ist.** Strata läuft bereits. Such nach seinem Fenster.
## Wie funktioniert das?
Weitere Probleme und ihre Lösungen: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Kommst du nicht weiter?
Öffne ein [Issue](https://github.com/Niko1221/Strata/issues) und hänge `strata-<model>.log` aus dem Strata-Ordner
an. Du hast ein Sicherheitsproblem gefunden? Melde es vertraulich: [SECURITY.md](SECURITY.md).
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.
## Wie funktioniert es?
- 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.
Modelle wie dieses laufen normalerweise auf Servern mit Hunderten Gigabyte Grafikspeicher. Deine Grafikkarte hat
12-24 GB. Strata bringt das Modell trotzdem unter, indem es **die Arbeit auf deinen ganzen PC verteilt**. Stell dir
eine Küche vor: Was du ständig brauchst, bleibt auf der Arbeitsfläche, der Rest wartet in der Vorratskammer.
<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>
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="Die 24,576 Experten des Modells: die meistgenutzten auf der Grafikkarte, alle im RAM, eine Nachschlagetabelle auf der SSD"></p>
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.
- **Das Modell ist ein Team aus 24,576 kleinen Spezialisten („Experten“).** Jedes Wort braucht nur 10 davon.
- **Deine Grafikkarte** hält die paar tausend Experten, die am häufigsten gebraucht werden. **Dein RAM** hält alle,
und **dein Prozessor** rechnet gleichzeitig am Rest. **Deine SSD** hält eine große Nachschlagetabelle.
<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>
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="Ein kleiner Helfer rät die nächsten Wörter; das große Modell prüft alle auf einmal und behält die richtigen"></p>
- **Raten, dann prüfen:** Ein kleiner Helfer rät die nächsten paar Wörter. Das große Modell prüft sie alle auf
einmal. Du bekommst dieselbe Antwort, nur 1.6-1.8x schneller.
- **Lange Texte werden in großen Stücken gelesen** (bis zu 8,192 Tokens auf einmal), mit über 1,000 Tokens pro
Sekunde.
Die längere Erklärung: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Jeder Teil und seine Zahlen:
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).
## Etwas ist schiefgegangen?
- **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.
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).
## Danksagung und Lizenz
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)
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). Ein
paar Teile und alle Modelle haben eigene Lizenzen ([welche](docs/HOW_IT_WORKS.md#license)).
[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)).
## 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.
## Strata unterstützen
Strata ist kostenlos und Open Source. Wenn es dir nützt, kannst du die Entwicklung 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.
<p align="center"><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></p>
<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>
Du möchtest Strata sponsern? Schreib mir über [GitHub](https://github.com/Niko1221).
+334 -164
View File
@@ -1,233 +1,403 @@
<h1 align="center">Strata</h1>
<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">
</picture>
</p>
[English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja.md) · [Deutsch](README.de.md) · [Français](README.fr.md) · **Español** · [Português](README.pt-BR.md)
<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>
<p align="center"><b>Ejecuta un modelo de IA de 125 mil millones de parámetros en tu propio PC gaming</b><br>
Tarjeta gráfica NVIDIA o AMD (12 GB o más) · Windows o Linux · libre y de código abierto</p>
<h3 align="center">Ejecuta un modelo de última generación en tu propio PC gaming</h3>
<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="Un jardín con una pagoda de vóxeles que escribió el modelo de Strata, funcionando en el navegador"></a><br>
<p align="center">Compatible con NVIDIA, AMD, Intel · Windows, Linux, Mac · libre y de código abierto</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>
</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>
Strata ejecuta **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** en un PC normal. Es un
modelo de IA grande e inteligente que normalmente necesita un servidor. Conversa, escribe código, entiende imágenes
y funciona con tus aplicaciones y agentes de programación. Nada sale de tu PC.
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.
## ¿Qué velocidad tiene?
**¿Eres nuevo?** Mira [lo que necesitas](#lo-que-necesitas) y luego [instala la aplicación](#opción-a-la-aplicación-strata).
Lo medimos en dos PC gaming normales. Un token equivale más o menos a ¾ de una palabra.
- **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).
- **Escribe respuestas:** lo rápido que aparece la respuesta en un chat corto. 60 tokens por segundo es más rápido de lo que puedes leer.
- **Lee tu prompt:** lo rápido que procesa lo que le envías (aquí, un documento, código o historial de chat de 32K tokens).
Versión 0.2.1 · motor 0.1.46
<table>
<tr><th>NVIDIA: RTX 5070 (12 GB), Ryzen 5 7600, 64 GB RAM</th><th>AMD: RX 9070 XT (16 GB), Ryzen 9 3900X, 47 GB RAM</th></tr>
<tr><td>
## Lo que necesitas
| Tamaño | Escribe respuestas | Lee tu 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 |
**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.
</td><td>
- 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.
| Tamaño | Escribe respuestas | Lee tu 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 |
</td></tr>
</table>
NVIDIA: Q2_0 con el motor 0.1.36, las demás filas con 0.1.26 (respuestas de 4K, prompts de 32K). Las tablas
completas están en [DETAILS.md](docs/DETAILS.md#speed-measured). 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. Chats largos y otras tarjetas:
[velocidad de cada modelo](docs/MODELS.md#how-fast-is-each-size), [resultados de la comunidad](docs/COMMUNITY_BENCHMARKS.md).
<p align="center"><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><br>
<sub>Strata es gratis. Si funciona bien en tu PC, un café ayuda a seguir trabajando en él.</sub></p>
## Qué necesitas
| | |
| Tu ordenador | Detalles |
| --- | --- |
| **Tarjeta gráfica** | **NVIDIA** GeForce serie RTX 20, 30, 40 o 50, o **AMD** Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700 o serie RX 6800 / 6900. Necesita **12 GB de VRAM o más**. |
| **RAM** | 32 GB o más. Tu RAM decide [qué modelo](#qué-modelo-elijo) cabe. Con 64 GB funcionan todos los tamaños. |
| **Disco** | Unos 80 GB libres. Usa un SSD si puedes: el primer arranque es mucho más rápido. |
| **Sistema** | Windows 10 / 11 o Linux, y un controlador gráfico actual de NVIDIA o AMD. |
| **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) |
El instalador prepara todo lo demás. Dos o tres tarjetas pueden repartirse el modelo ([multi-GPU](docs/MULTI_GPU.md)).
También hace falta:
Experimental, escrito y probado por miembros de la comunidad en sus propios equipos:
- 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
- **Tarjetas gráficas antiguas** (Tesla P40 / V100, GTX 10, Radeon VII / MI50, RX 6700 XT, RX 5500 XT): [Older GPUs](docs/OLDER_GPUS.md).
- **Intel Arc**, compilado desde el código fuente en Linux: [Intel Arc](docs/INTEL_ARC.md).
- **AMD Ryzen AI Max (Strix Halo)**, compilado desde el código fuente en Linux: [Strix Halo](docs/STRIX_HALO.md).
- **Procesadores antiguos sin AVX2**: funcionan, pero despacio. [Older CPUs](docs/INSTALL.md#older-cpus-experimental).
La lista completa: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
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).
## Instalación
### Deja que tu IA lo instale
Elige una opción. Todas arrancan el mismo motor y usan los mismos modelos.
¿Usas un asistente de programación con IA (Claude Code, Cursor, Codex, GitHub Copilot, ...)? Pega esto en él:
### Opción A: la aplicación Strata
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.
| Sistema | Tu tarjeta gráfica | Archivo que descargar |
| --- | --- | --- |
| **Windows 10 / 11** | NVIDIA RTX, controlador 580 o posterior | [`Strata-0.2.1-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-cuda13.exe) |
| | NVIDIA, tarjeta más antigua o controlador anterior a 580 | [`Strata-0.2.1-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-cuda12.exe) |
| | AMD Radeon serie RX 9000 o Radeon AI PRO R9700 | [`Strata-0.2.1-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-hip.exe) |
| | AMD Radeon serie RX 7000 o Ryzen AI Max (Strix Halo) | [`Strata-0.2.1-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-hip-rdna3.exe) |
| **Linux** | NVIDIA RTX, controlador 580 o posterior | [`Strata-0.2.1-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-nvidia-cuda13.AppImage) |
| | NVIDIA, tarjeta más antigua o controlador anterior a 580 | [`Strata-0.2.1-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-nvidia-cuda12.AppImage) |
| | AMD Radeon serie RX 9000 o Radeon AI PRO R9700 | [`Strata-0.2.1-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-rx9070.AppImage) |
| | AMD Radeon serie RX 7000 | [`Strata-0.2.1-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-rx7900.AppImage) |
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.1-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-strixhalo.AppImage) |
| **Mac** (Apple Silicon, macOS 15 o posterior) | integrada | [`Strata-0.2.1-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-mac.dmg) |
**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).
Después:
- **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.
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.
### Opción B: la Terminal (una línea)
La Terminal es el comando `strata`, pensado para scripts, servidores y agentes de IA.
Linux y Mac:
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
```
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:
```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.
```
Revisa tu tarjeta gráfica, tu RAM y tu disco, y elige el modelo que cabe. Luego lo instala, lo arranca y te explica
cómo conectar tus aplicaciones. Las herramientas de IA también pueden instalar, arrancar y detener Strata con su
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).
### O hazlo tú mismo
¿Prefieres compilarlo tú? Mira [Compilar desde el código fuente](#compilar-desde-el-código-fuente).
[Descarga Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) y descomprímelo (o usa `git clone`).
**Windows:** haz doble clic en **`START-HERE.bat`**. **Linux:** ejecuta **`./setup.sh`** en la carpeta de Strata.
## Modelos
**Mac (Apple Silicon, M1 a M5, macOS 15 o posterior):** instala una vez las Command Line Tools de Apple (`xcode-select --install` en Terminal; no hace falta Xcode). Luego abre Terminal en la carpeta de Strata y ejecuta **`./setup.sh`**. Compila el motor de Mac en tu Mac (unos 7 minutos), hace las mismas preguntas, descarga el modelo y lo arranca. La próxima vez, `./setup.sh` arranca enseguida. Qué tamaño cabe en tu Mac y qué velocidad tiene: [docs/MAC.md](docs/MAC.md).
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.
Los pasos son los mismos para NVIDIA y AMD. El instalador detecta tu tarjeta y prepara el motor adecuado para ella.
Te hace unas pocas preguntas:
### ¿Poca memoria? Prueba Gyro
- qué modelo y qué tamaño,
- cuánto contexto (cuánto texto tiene presente el modelo),
- si debe entender imágenes.
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) (de agentionai) es la forma más pequeña de ejecutar este modelo.
Pulsa Enter cada vez para usar la respuesta recomendada. Después descarga el modelo (unos 70 GB) y lo arranca. Si la
descarga se detiene, vuelve a ejecutarlo: sigue donde lo dejó. Tu navegador abre la aplicación de Strata en
`http://127.0.0.1:8080`.
| 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 17-21 % más despacio que Q2_0 en una tarjeta de 12 GB y en una R9700. |
> **Mientras el modelo arranca, tu PC puede ir lento o dejar de responder durante 1-3 minutos** (más la primera vez).
> Strata carga 35-55 GB en tu RAM y reserva una parte para la tarjeta gráfica. Es normal. Espera y no cierres la
> ventana. La ventana muestra lo que está haciendo Strata.
Gyro funciona en tarjetas NVIDIA y AMD (en AMD es experimental), no en Intel Arc ni en el Mac. Detalles y mediciones:
[docs/GYRO.md](docs/GYRO.md). Arráncalo con `strata launch --model gyro-s`.
**La próxima vez**, vuelve a ejecutar `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.
Actualizaciones, Docker, varias tarjetas, dónde se guardan los archivos y todas las opciones: [docs/INSTALL.md](docs/INSTALL.md).
### Los demás tamaños
## ¿Qué modelo elijo?
El instalador te recomienda uno según tu RAM. El mismo modelo viene en varios tamaños, más o menos comprimidos. Los
tamaños pequeños son más rápidos. Los grandes son algo más inteligentes.
| Tu RAM | Elige | Por qué |
| Tu memoria (RAM + VRAM) | Elige | Notas |
| --- | --- | --- |
| **32 GB** | **Coder** | cabe en 32 GB y está hecho para programar (con una tarjeta de 24 GB también funcionan Q2_0 e IQ2_XS) |
| **48 GB** | **IQ2_XS** (o Q2_0, el más rápido) | los tamaños más grandes no caben |
| **64 GB** | **IQ2_XS** (recomendado), o IQ3_XXS / IQ3_S | caben todos los tamaños; IQ3_S es el mejor y el más lento |
| **96 GB o más** | **IQ3_S**, o el UD-IQ4_XS de Unsloth (~4 bits) | espacio para los tamaños más grandes con todo lo demás abierto |
| **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 17-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 |
- **[Coder](docs/MODELS.md#coder):** una versión para programar a la que se le quitó la mitad de los expertos. Alcanza
el 91% de la puntuación SWE-bench Verified del modelo completo (medido por sus autores) y cabe en 32 GB de RAM. Es
más flojo fuera del código, también con chino y otros textos CJK (#438). Para eso, elige Q2_0, IQ2_XS o IQ3_S, que
conservan todos los expertos.
- **[Swift 1.5](docs/MODELS.md#swift-15):** un ajuste fino que piensa mucho menos tiempo antes de responder. Recibes
la respuesta antes, con más o menos la misma calidad.
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** la versión de ~4 bits de Unsloth, entre IQ3_S y
UD-Q4_K_XL en calidad. Una descarga de 94 GB. Con menos de ~80 GB de RAM, Strata lee una parte desde el SSD
mientras responde, así que ahí es más lento (un SSD NVMe ayuda).
- **[Unsloth UD-Q4_K_XL, UD-Q5_K_XL, UD-Q6_K_XL y Q8_0](docs/MODELS.md#unsloths-files-ud-iq4_xs-to-q8_0):** de 4 a 8 bits, más cerca del modelo completo
(93-95 % el mismo token siguiente que Q8_0). Rápidos cuando todos los expertos caben en la RAM (~88 GB para
UD-Q4_K_XL hasta ~136 GB para Q8_0); con menos RAM, Strata lee una parte desde el SSD mientras responde, lo que es
mucho más lento (7-8.5 tokens/s para UD-Q4_K_XL en un PC con 64 GB).
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** se instala a mano. No está en
el menú del instalador.
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.
Tamaños, descargas y qué cabe dónde: [docs/MODELS.md](docs/MODELS.md). Para añadir otro modelo más adelante, ejecuta
`SETUP.bat` (Linux: `./setup.sh --setup`).
Más opciones:
## Cómo usarlo
- **[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.
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="La pestaña Monitor de la aplicación de Strata junto a un agente de programación"><br>
<sub>El <b>Monitor</b> de la aplicación de Strata (izquierda) mientras un agente de programación escribe el jardín de la pagoda del vídeo (derecha)</sub></p>
Todos los tamaños, descargas y lo que cabe en cada equipo: [docs/MODELS.md](docs/MODELS.md).
- **En el navegador:** abre `http://127.0.0.1:8080`. Tiene **Chat**, un **Monitor** en vivo del modelo y de tu
GPU/CPU/RAM, y **About** con los ajustes y las direcciones.
- **Tus aplicaciones y agentes de programación:** añade un proveedor "compatible con OpenAI" con la URL base
**`http://127.0.0.1:8080/v1`**. Sirve cualquier clave de API y cualquier nombre de modelo.
- Aplicaciones que usan la API de Anthropic: `http://127.0.0.1:8080/v1/messages` (Claude Code:
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`).
- Codex CLI y otras aplicaciones que usan la API Responses de OpenAI: `/v1/responses`
([configuración](docs/DETAILS.md#the-responses-api-and-codex-cli)).
- **Razonamiento:** elige **off, low, medium o high** en el menú del chat o en el "reasoning effort" de tu aplicación.
Off es lo más rápido. High es lo mejor para preguntas difíciles.
- **Imágenes:** responde que sí a "Images?" en la instalación. Luego haz clic en **Picture** en el chat, o adjunta
imágenes en tu aplicación. Las tarjetas AMD leen imágenes en Linux a través del procesador; en Windows todavía no.
- **Desde tu móvil u otro PC:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`. Pon siempre una clave.
- **Una petición cada vez:** por defecto Strata responde una petición y las demás esperan. Para responder varias a la
vez, pon `"parallel": 2` ([BATCHING.md](docs/BATCHING.md)). En una tarjeta de 12 GB, cada respuesta va más lenta.
- **Prompts largos:** Strata lee entero el primer mensaje de un chat, más o menos 1 minuto por cada 30,000 tokens. Los
mensajes siguientes empiezan en segundos.
## ¿Qué velocidad tiene?
Más: [dónde se guardan tus chats](docs/INSTALL.md#where-things-are-stored), [la API](docs/DETAILS.md#using-it).
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).
### Opcional: más velocidad con una sola tarjeta cuando todo el modelo cabe en la RAM
| 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 |
Dos ajustes opcionales (de RubenTorney, #1904 y #1905). Ayudan si usas una tarjeta gráfica y tu RAM guarda todos los expertos (el modo arena de Strata, por ejemplo Q2_0 o IQ3_XXS en un PC con 64 GB). Abre tu configuración `strata-<model>.json` en la carpeta de datos de Strata ([dónde](docs/INSTALL.md#where-things-are-stored)) y añade:
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.
- **Generación más rápida:** `"--adapt-async", "1"` en la lista `"args"`. La caché de expertos se actualiza entonces en un hilo auxiliar en vez de pausar la respuesta. Medido en una RTX 5070 (Windows, Q2_0): +4-5 % de generación, +7-10 % en código y documentos largos. Las respuestas pueden diferir un poco de las por defecto (son iguales de una ejecución a otra).
- **Lectura del prompt más rápida en Windows:** `"STRATA_PIN_BY_NEED": "1"` en el bloque `"env"`. Windows solo puede bloquear cerca de la mitad de la RAM para la tarjeta; esto bloquea primero los expertos que la tarjeta necesita de verdad. Medido en la RTX 5070: +4,4 % en la lectura del prompt.
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 (`START-HERE.bat` o `./setup.sh`) después del cambio. Para deshacerlo, quita las dos entradas.
Reinicia Strata después del cambio. Para deshacerlo, quita las dos entradas.
## ¿Algo salió mal?
- **Mi PC se congeló la primera vez que arrancó Strata.** Es normal mientras carga el modelo. Espera y no cierres la
ventana. ¿Sigue congelado después de 10 minutos? Reinicia el PC, cierra otros programas y vuelve a intentarlo, o
elige un tamaño más pequeño.
- **Se detuvo durante la descarga o la instalación.** Vuelve a ejecutar `START-HERE.bat` (o `./setup.sh`). Sigue
donde se detuvo.
- **Va muy lento y la luz del disco no deja de parpadear, o dice "the engine stopped unexpectedly".** Tu PC no tiene
suficiente RAM libre. Cierra otros programas (los navegadores usan mucha), o elige un tamaño más pequeño (Q2_0 o
IQ2_XS).
- **Dice que el puerto 8080 ya está en uso.** Strata ya está en marcha. Busca su ventana.
Más problemas y sus soluciones: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). ¿Sigues atascado? Abre un
[issue](https://github.com/Niko1221/Strata/issues) y adjunta `strata-<model>.log` de la carpeta de Strata. ¿Has
encontrado un problema de seguridad? Infórmalo en privado: [SECURITY.md](SECURITY.md).
</details>
## ¿Cómo funciona?
Los modelos como este suelen funcionar 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**. Piensa en una cocina: lo
que usas todo el rato se queda en la encimera, y el resto espera en la despensa.
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.
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="Los 24,576 expertos del modelo: los más usados en la tarjeta gráfica, todos en la RAM y una tabla de consulta en el SSD"></p>
- 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.
- **El modelo es un equipo de 24,576 pequeños especialistas ("expertos").** Cada palabra solo necesita 10 de ellos.
- **Tu tarjeta gráfica** guarda los pocos miles de expertos que más se usan. **Tu RAM** los tiene todos, y **tu
procesador** trabaja con el resto al mismo tiempo. **Tu SSD** guarda una gran tabla de consulta.
<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>
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="Un pequeño ayudante adivina las siguientes palabras; el modelo grande las comprueba todas a la vez y se queda con las correctas"></p>
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.
- **Adivinar y comprobar:** un pequeño ayudante adivina las siguientes palabras. El modelo grande las comprueba todas
a la vez. Recibes la misma respuesta, 1.6-1.8x antes.
- **Los textos largos se leen en trozos grandes** (hasta 8,192 tokens cada vez), a más de 1,000 tokens por segundo.
<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>
La explicación larga: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Cada parte y sus números:
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).
## ¿Algo ha ido mal?
- **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.
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).
## Créditos y licencia
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)
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 cada modelo tienen sus propias licencias ([cuáles](docs/HOW_IT_WORKS.md#license)).
[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)).
## Apoya a Strata
## Contribuir
Strata es libre y de código abierto. Si te resulta útil, puedes apoyar su desarrollo:
¿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.
<p align="center"><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></p>
## 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.
<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>
¿Te interesa patrocinar Strata? Escríbeme a través de [GitHub](https://github.com/Niko1221).
+337 -170
View File
@@ -1,238 +1,405 @@
<h1 align="center">Strata</h1>
<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">
</picture>
</p>
[English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja.md) · [Deutsch](README.de.md) · **Français** · [Español](README.es.md) · [Português](README.pt-BR.md)
<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>
<p align="center"><b>Faites tourner un modèle d'IA de 125 milliards de paramètres sur votre propre PC de jeu</b><br>
Carte graphique NVIDIA ou AMD (12 Go ou plus) · Windows ou Linux · gratuit et open source</p>
<h3 align="center">Faites tourner un modèle de pointe sur votre propre PC de jeu</h3>
<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="Un jardin de pagode en voxels écrit par le modèle de Strata, qui tourne dans le navigateur"></a><br>
<p align="center">Compatible avec NVIDIA, AMD, Intel · Windows, Linux, Mac · gratuit et open source</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>
</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>
Strata fait tourner **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** sur un PC normal. C'est
un grand modèle d'IA, très capable, qui demande d'habitude un serveur. Il discute, écrit du code, lit des images et
fonctionne avec vos applications et vos agents de code. Rien ne quitte votre PC.
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.
## Quelle est sa vitesse ?
**Nouveau ici ?** Regardez [ce qu'il vous faut](#ce-quil-vous-faut), puis [installez l'application](#option-a--lapplication-strata).
Nous l'avons mesurée sur deux PC de jeu ordinaires. Un token correspond à environ ¾ d'un mot.
- **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).
- **Écrit les réponses :** la vitesse à laquelle la réponse s'affiche dans une courte discussion. 60 tokens par seconde, c'est plus rapide que votre lecture.
- **Lit votre prompt :** la vitesse à laquelle il absorbe ce que vous envoyez (ici un document, du code ou un historique de discussion de 32K tokens).
<table>
<tr><th>NVIDIA : RTX 5070 (12 Go), Ryzen 5 7600, 64 Go de RAM</th><th>AMD : RX 9070 XT (16 Go), Ryzen 9 3900X, 47 Go de RAM</th></tr>
<tr><td>
| Taille | Écrit les réponses | 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 |
</td><td>
| Taille | Écrit les réponses | 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 |
</td></tr>
</table>
NVIDIA : Q2_0 avec le moteur 0.1.36, les autres lignes avec 0.1.26 (réponses de 4K, prompts de 32K). Les tableaux
complets sont dans [DETAILS.md](docs/DETAILS.md#speed-measured). Une carte avec plus de VRAM va plus vite : une
RTX 3090 (24 Go) devrait écrire environ 100-140 tokens par seconde. Longues discussions et autres cartes :
[vitesse de chaque modèle](docs/MODELS.md#how-fast-is-each-size), [résultats de la communauté](docs/COMMUNITY_BENCHMARKS.md).
<p align="center"><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><br>
<sub>Strata est gratuit. S'il tourne bien sur votre PC, un café aide à poursuivre le travail.</sub></p>
Version 0.2.1 · moteur 0.1.46
## Ce qu'il vous faut
| | |
**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 |
| --- | --- |
| **Carte graphique** | **NVIDIA** GeForce RTX série 20, 30, 40 ou 50, ou **AMD** Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700 ou série RX 6800 / 6900. Il faut **12 Go de VRAM ou plus**. |
| **RAM** | 32 Go ou plus. Votre RAM décide [quel modèle](#quel-modèle-choisir-) tient. Avec 64 Go, toutes les tailles tournent. |
| **Disque** | Environ 80 Go libres. Prenez un SSD si possible : le premier démarrage est bien plus rapide. |
| **Système** | Windows 10 / 11 ou Linux, avec un pilote graphique NVIDIA ou AMD à jour. |
| **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) |
L'installateur s'occupe de tout le reste. Deux ou trois cartes peuvent se partager le modèle ([multi-GPU](docs/MULTI_GPU.md)).
Il faut aussi :
Expérimental, écrit et testé par des membres de la communauté sur leurs propres machines :
- 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
- **Cartes graphiques plus anciennes** (Tesla P40 / V100, GTX 10, Radeon VII / MI50, RX 6700 XT, RX 5500 XT) : [Older GPUs](docs/OLDER_GPUS.md).
- **Intel Arc**, compilé depuis les sources sous Linux : [Intel Arc](docs/INTEL_ARC.md).
- **AMD Ryzen AI Max (Strix Halo)**, compilé depuis les sources sous Linux : [Strix Halo](docs/STRIX_HALO.md).
- **Processeurs plus anciens sans AVX2** : ça marche, mais lentement. [Older CPUs](docs/INSTALL.md#older-cpus-experimental).
La liste complète : [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
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).
## Installation
### Laissez votre IA l'installer
Choisissez une option. Elles lancent toutes le même moteur et utilisent les mêmes modèles.
Vous utilisez un assistant de code IA (Claude Code, Cursor, Codex, GitHub Copilot, ...) ? Collez-lui ceci :
### Option A : l'application Strata
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.
| Système | Votre carte graphique | Fichier à télécharger |
| --- | --- | --- |
| **Windows 10 / 11** | NVIDIA RTX, pilote 580 ou plus récent | [`Strata-0.2.1-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-cuda13.exe) |
| | NVIDIA, carte plus ancienne ou pilote antérieur à 580 | [`Strata-0.2.1-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-cuda12.exe) |
| | AMD Radeon série RX 9000 ou Radeon AI PRO R9700 | [`Strata-0.2.1-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-hip.exe) |
| | AMD Radeon série RX 7000 ou Ryzen AI Max (Strix Halo) | [`Strata-0.2.1-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-hip-rdna3.exe) |
| **Linux** | NVIDIA RTX, pilote 580 ou plus récent | [`Strata-0.2.1-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-nvidia-cuda13.AppImage) |
| | NVIDIA, carte plus ancienne ou pilote antérieur à 580 | [`Strata-0.2.1-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-nvidia-cuda12.AppImage) |
| | AMD Radeon série RX 9000 ou Radeon AI PRO R9700 | [`Strata-0.2.1-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-rx9070.AppImage) |
| | AMD Radeon série RX 7000 | [`Strata-0.2.1-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-rx7900.AppImage) |
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.1-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-strixhalo.AppImage) |
| **Mac** (Apple Silicon, macOS 15 ou plus récent) | intégrée | [`Strata-0.2.1-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-mac.dmg) |
**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).
Ensuite :
- **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.
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.
### Option B : le Terminal (une ligne)
Le Terminal est la commande `strata`, pensée pour les scripts, les serveurs et les agents d'IA.
Linux et Mac :
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
```
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 :
```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.
```
Il vérifie votre carte graphique, votre RAM et votre disque, et choisit le modèle qui convient. Ensuite il 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 grâce à son [serveur MCP](docs/MCP_SERVER.md).
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).
### Ou faites-le vous-même
Vous préférez le compiler vous-même ? Voir [Compiler depuis les sources](#compiler-depuis-les-sources).
[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.
## Modèles
**Mac (Apple Silicon, M1 à M5, macOS 15 ou plus récent) :** installez une fois les Command Line Tools d'Apple (`xcode-select --install` dans le Terminal ; Xcode lui-même n'est pas nécessaire). Ouvrez ensuite le Terminal dans le dossier Strata et lancez **`./setup.sh`**. Il 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` démarre tout de suite. Quelle taille convient à votre Mac et sa vitesse : [docs/MAC.md](docs/MAC.md).
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.
Les étapes sont les mêmes pour NVIDIA et AMD. L'installateur détecte votre carte et installe le bon moteur pour elle.
Il vous pose quelques questions :
### Peu de mémoire ? Essayez Gyro
- quel modèle et quelle taille,
- combien de contexte (la quantité de texte que le modèle garde en tête),
- s'il doit lire les images.
[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.
Appuyez sur Entrée à chaque fois pour garder la réponse recommandée. Ensuite il télécharge le modèle (environ 70 Go)
et le démarre. Si le téléchargement s'arrête, relancez-le : il reprend là où il s'était arrêté. Votre navigateur
ouvre l'application Strata à l'adresse `http://127.0.0.1:8080`.
| 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 17 à 21 % plus lentement que Q2_0 sur une carte de 12 Go et sur une R9700. |
> **Pendant le démarrage du modèle, votre PC peut ralentir ou ne plus répondre pendant 1-3 minutes** (plus longtemps la première fois).
> Strata charge 35-55 Go dans votre RAM et en réserve une partie pour la carte graphique. C'est normal. Attendez, et
> ne fermez pas la fenêtre. La fenêtre montre ce que fait Strata.
Gyro fonctionne sur les cartes NVIDIA et AMD (expérimental sur AMD), pas sur Intel Arc ni sur Mac. Détails et mesures :
[docs/GYRO.md](docs/GYRO.md). Lancez-le avec `strata launch --model gyro-s`.
**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.
Mises à jour, Docker, plusieurs cartes, emplacement des fichiers et toutes les options : [docs/INSTALL.md](docs/INSTALL.md).
### Les autres tailles
## Quel modèle choisir ?
L'installateur en recommande un selon votre RAM. Le même modèle existe en plusieurs tailles, plus ou moins
compressées. Les petites tailles sont plus rapides. Les grandes sont un peu plus intelligentes.
| Votre RAM | Prenez | Pourquoi |
| Votre mémoire (RAM + VRAM) | Prenez | Remarques |
| --- | --- | --- |
| **32 Go** | **Coder** | il tient dans 32 Go et il est fait pour le code (avec une carte de 24 Go, Q2_0 et IQ2_XS tournent aussi) |
| **48 Go** | **IQ2_XS** (ou Q2_0, le plus rapide) | les plus grandes tailles ne tiennent pas |
| **64 Go** | **IQ2_XS** (recommandé), ou IQ3_XXS / IQ3_S | toutes les tailles tiennent ; IQ3_S est le meilleur et le plus lent |
| **96 Go ou plus** | **IQ3_S**, ou l'UD-IQ4_XS d'Unsloth (~4 bits) | de la place pour les plus grandes tailles, avec tout le reste ouvert |
| **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 17 à 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 |
- **[Coder](docs/MODELS.md#coder) :** une version pour le code, avec la moitié des experts en moins. Elle atteint 91 %
du score SWE-bench Verified du modèle complet (mesuré par ses auteurs) et tient dans 32 Go de RAM. Elle est moins
bonne en dehors du code, y compris pour le chinois et les autres textes CJK (#438). Pour ces cas, prenez Q2_0,
IQ2_XS ou IQ3_S, qui gardent tous les experts.
- **[Swift 1.5](docs/MODELS.md#swift-15) :** un fine-tune qui réfléchit bien moins longtemps avant de répondre. Vous
avez la réponse plus tôt, avec à peu près la même qualité.
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs)** : la version ~4 bits d'Unsloth, entre IQ3_S et
UD-Q4_K_XL en qualité. Un téléchargement de 94 Go. Avec moins de ~80 Go de RAM, Strata en lit une partie depuis
le SSD pendant qu'il répond, il y est donc plus lent (un SSD NVMe aide).
- **[Unsloth UD-Q4_K_XL, UD-Q5_K_XL, UD-Q6_K_XL et Q8_0](docs/MODELS.md#unsloths-files-ud-iq4_xs-to-q8_0) :** de 4 à 8 bits, plus proches du modèle
complet (93-95 % le même token suivant que Q8_0). Rapides quand tous les experts tiennent dans la RAM (~88 Go pour
UD-Q4_K_XL jusqu'à ~136 Go pour Q8_0) ; avec moins de RAM, Strata en lit une partie depuis le SSD pendant qu'il
répond, ce qui est bien plus lent (7-8.5 tokens/s pour UD-Q4_K_XL sur un PC avec 64 Go).
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs) :** vous l'installez à la main.
Il n'est pas dans le menu de l'installateur.
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.
Tailles, téléchargements et ce qui tient où : [docs/MODELS.md](docs/MODELS.md). Pour ajouter un autre modèle plus
tard, lancez `SETUP.bat` (Linux : `./setup.sh --setup`).
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).
## 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
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="L'onglet Monitor de l'application Strata à côté d'un agent de code"><br>
<sub>Le <b>Monitor</b> de l'application Strata (à gauche) pendant qu'un agent de code écrit le jardin de pagode de la vidéo (à droite)</sub></p>
### Dans l'application
- **Dans le navigateur :** ouvrez `http://127.0.0.1:8080`. Vous y trouvez **Chat**, un **Monitor** en direct du
modèle et de votre GPU/CPU/RAM, et **About** avec les réglages et les adresses.
- **Vos applications et agents de code :** ajoutez un fournisseur « OpenAI-compatible » avec l'URL de base
**`http://127.0.0.1:8080/v1`**. N'importe quelle clé API et n'importe quel nom de modèle fonctionnent.
- Applications qui utilisent l'API d'Anthropic : `http://127.0.0.1:8080/v1/messages` (Claude Code :
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`).
- Codex CLI et les autres applications qui utilisent l'API OpenAI Responses : `/v1/responses`
([configuration](docs/DETAILS.md#the-responses-api-and-codex-cli)).
- **Réflexion :** choisissez **off, low, medium ou high** dans le menu du chat ou dans le « reasoning effort » de
votre application. Off est le plus rapide. High est le meilleur pour les questions difficiles.
- **Images :** répondez oui à « Images? » pendant l'installation. Ensuite cliquez sur **Picture** dans le chat, ou
joignez des images dans votre application. Les cartes AMD lisent les images sous Linux via le processeur ; sous
Windows, pas encore.
- **Depuis votre téléphone ou un autre PC :** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`. Définissez
toujours une clé.
- **Une requête à la fois :** par défaut, Strata répond à une requête et les autres attendent. Pour répondre à
plusieurs en même temps, mettez `"parallel": 2` ([BATCHING.md](docs/BATCHING.md)). Sur une carte de 12 Go, chaque
réponse devient alors plus lente.
- **Longs prompts :** Strata lit en entier le premier message d'une discussion, environ 1 minute pour 30,000 tokens.
Les messages suivants démarrent en quelques secondes.
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.
Plus d'infos : [où sont stockées vos discussions](docs/INSTALL.md#where-things-are-stored), [l'API](docs/DETAILS.md#using-it).
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).
### Optionnel : plus de vitesse sur une seule carte quand tout le modèle tient en RAM
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.
Deux réglages optionnels (de RubenTorney, #1904 et #1905). Ils aident si vous utilisez une seule carte graphique et que votre RAM contient tous les experts (le mode arena de Strata, par exemple Q2_0 ou IQ3_XXS sur un PC de 64 Go). Ouvrez votre configuration `strata-<model>.json` dans le dossier de données de Strata ([où](docs/INSTALL.md#where-things-are-stored)) et ajoutez :
<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>
- **Génération plus rapide :** `"--adapt-async", "1"` dans la liste `"args"`. Le cache d'experts est alors mis à jour dans un thread auxiliaire au lieu de mettre la réponse en pause. Mesuré sur une RTX 5070 (Windows, Q2_0) : +4-5 % de génération, +7-10 % sur le code et les longs documents. Les réponses peuvent légèrement différer de celles par défaut (elles restent identiques d'une exécution à l'autre).
- **Lecture du prompt plus rapide sous Windows :** `"STRATA_PIN_BY_NEED": "1"` dans le bloc `"env"`. Windows ne peut verrouiller qu'environ la moitié de la RAM pour la carte ; ce réglage verrouille d'abord les experts dont la carte a vraiment besoin. Mesuré sur la RTX 5070 : +4,4 % en lecture du prompt.
### 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 (`START-HERE.bat` ou `./setup.sh`) après la modification. Pour annuler, retirez les deux entrées.
Redémarrez Strata après la modification. Pour annuler, supprimez les deux entrées.
## Un problème ?
- **Mon PC s'est figé au premier démarrage de Strata.** C'est normal pendant le chargement du modèle. Attendez, 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.
- **Il s'est arrêté pendant le téléchargement ou l'installation.** Relancez `START-HERE.bat` (ou `./setup.sh`). Il
reprend là où il s'était arrêté.
- **C'est très lent et 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 les autres programmes (les navigateurs en utilisent beaucoup), ou
choisissez une taille plus petite (Q2_0 ou IQ2_XS).
- **Il dit que le port 8080 est déjà utilisé.** Strata tourne déjà. Cherchez sa fenêtre.
Autres problèmes et leurs solutions : [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Toujours bloqué ? Ouvrez une
[issue](https://github.com/Niko1221/Strata/issues) et joignez `strata-<model>.log` depuis le dossier Strata. Vous
avez trouvé un problème de sécurité ? Signalez-le en privé : [SECURITY.md](SECURITY.md).
</details>
## Comment ça marche ?
Les modèles comme celui-ci tournent d'habitude sur des serveurs avec des centaines de gigaoctets de mémoire
graphique. Votre carte graphique a 12-24 Go. Strata fait tenir le modèle en **répartissant le travail sur tout votre
PC**. Pensez à une cuisine : ce que vous utilisez tout le temps reste sur le plan de travail, et le reste attend dans
le placard.
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.
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="Les 24,576 experts du modèle : les plus sollicités sur la carte graphique, tous en RAM, une table de correspondance sur le SSD"></p>
- 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.
- **Le modèle est une équipe de 24,576 petits spécialistes (les « experts »).** Chaque mot n'en demande que 10.
- **Votre carte graphique** garde les quelques milliers d'experts les plus utilisés. **Votre RAM** les contient tous,
et **votre processeur** travaille sur les autres en même temps. **Votre SSD** contient une grande table de
correspondance.
<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>
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="Un petit assistant devine les mots suivants ; le grand modèle les vérifie tous d'un coup et garde les bons"></p>
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.
- **Deviner, puis vérifier :** un petit assistant devine les quelques mots suivants. Le grand modèle les vérifie
tous d'un coup. Vous obtenez la même réponse, 1.6-1.8x plus tôt.
- **Les longs textes sont lus par gros morceaux** (jusqu'à 8,192 tokens à la fois), à plus de 1,000 tokens par seconde.
<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>
L'explication plus longue : [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Chaque partie et ses chiffres :
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).
## Crédits et licence
## Un problème ?
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)
- **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.
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).
## Remerciements et licence
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
crédits : [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata est open source sous [licence MIT](LICENSE).
Quelques parties et chaque modèle ont leur propre licence ([lesquelles](docs/HOW_IT_WORKS.md#license)).
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)).
## 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.
## Soutenir Strata
Strata est gratuit et open source. S'il vous est utile, vous pouvez soutenir son développement :
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.
<p align="center"><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></p>
<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>
Vous souhaitez sponsoriser Strata ? Contactez-moi via [GitHub](https://github.com/Niko1221).
+323 -167
View File
@@ -1,229 +1,385 @@
<h1 align="center">Strata</h1>
<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">
</picture>
</p>
[English](README.md) · [简体中文](README.zh-CN.md) · **日本語** · [Deutsch](README.de.md) · [Français](README.fr.md) · [Español](README.es.md) · [Português](README.pt-BR.md)
<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>
<p align="center"><b>1,250 億パラメータの AI モデルを、手元のゲーミング PC で動かす</b><br>
NVIDIA または AMD のグラフィックカード(12 GB 以上) · Windows または Linux · 無料・オープンソース</p>
<h3 align="center">最先端のモデルを、手元のゲーミングPCで動かす</h3>
<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="Strata で生成した、五重塔のある庭。ボクセルで表現され、ブラウザーで動作している"></a><br>
<sub>五重塔のある庭をボクセルで表現。RTX 5070 で動かした Strata(IQ3_S、コンテキスト長 128K)に、1 回のプロンプトで作成を依頼 ·
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">動画を見る(全編 49 秒)</a></sub></p>
<p align="center">NVIDIA、AMD、Intel に対応 · Windows、Linux、Mac · 無料のオープンソース</p>
Strata を使うと、**[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** を一般的な PC で動かせます。
通常はサーバーを必要とする大規模で高性能な AI モデルで、チャットやコードの生成、画像の読み取りができます。
アプリやコーディングエージェントとも連携でき、データは PC の外に送信されません。
<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>
</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>
一般的なゲーミング PC 2 台で速度を測定しました。英語の場合、1 トークンはおよそ ¾ 語に相当します。
Strata は **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** を動かします。1250億パラメータのモデルで、
ふつうはサーバーが必要ですが、Strata なら一般的なPCで使えます。チャット、コード作成、画像の読み取りができ、
お使いのアプリやコーディングエージェントとも連携します。チャットとファイルはPCの外に出ません。
- **生成速度:** 短いチャットで回答を生成する速度です。毎秒 60 トークンなら、人が読むよりも速く文章が表示されます。
- **プロンプトの読み込み:** 入力した内容を読み込む速度です。ここでは、文書やコード、チャット履歴など 32K トークン分を読み込ませています。
**はじめての方へ。** まず[必要な環境](#必要な環境)を確認して、[アプリをインストール](#方法a-strata-アプリ)してください。
<table>
<tr><th>NVIDIA: RTX 5070 (12 GB), Ryzen 5 7600, 64 GB RAM</th><th>AMD: RX 9070 XT (16 GB), Ryzen 9 3900X, 47 GB RAM</th></tr>
<tr><td>
- **ゲーミングPCで動きます。** RAMとグラフィックスメモリの合計が32 GBあれば始められます。
- **お使いのツールと連携します。** OpenAI と Anthropic の API に対応しているので、Claude Code、Codex CLI、OpenCode などのエージェントから接続できます。
- **画像を読めます。** 初期設定でオンです。
- **無料です。** MIT ライセンス。アカウントもクラウドも不要です。
- **プライバシーを守ります。** テレメトリはありません。Strata が何をダウンロードするか、いつインターネットを使うか、ダウンロードをどう検証するかは [docs/SAFETY.md](docs/SAFETY.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 |
バージョン 0.2.1 · エンジン 0.1.46
</td><td>
## 必要な環境
| モデルサイズ | 生成速度 | プロンプトの読み込み |
| --- | ---: | ---: |
| **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 |
**RAMとグラフィックスメモリ(VRAM)の合計が、少なくとも32 GB必要です。** MacやRyzen AI Maxでは、ユニファイドメモリがその量にあたります。
メモリが多いほど、大きなモデルを使えたり、速く動いたりします。
</td></tr>
</table>
- RAM 32 GBのPCに12〜16 GBのグラフィックスカードがあれば、Coder が動きます。
- RAM 64 GBなら、どのサイズも動きます。
NVIDIA の測定には、Q2_0 のみエンジン 0.1.36、それ以外は 0.1.26 を使用しています(回答 4K、プロンプト 32K)。測定結果の詳細は
[DETAILS.md](docs/DETAILS.md#speed-measured) をご覧ください。VRAM の容量が大きいカードほど高速で、RTX 3090(24 GB)なら
毎秒およそ 100-140 トークンの生成速度が見込めます。長いチャットや他のカードでの性能については、[モデルごとの速度](docs/MODELS.md#how-fast-is-each-size)や
[コミュニティの測定結果](docs/COMMUNITY_BENCHMARKS.md)を参照してください。
<p align="center"><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><br>
<sub>Strata は無料で使えます。お使いの PC で快適に動いたら、コーヒー 1 杯分のご支援が開発を続ける励みになります。</sub></p>
## 必要なもの
| | |
| お使いのPC | 詳細 |
| --- | --- |
| **グラフィックカード** | **NVIDIA** GeForce RTX 20、30、40、50 シリーズ、または **AMD** Radeon RX 7900 XT / XTX、RX 7800 XT / 7700 XT、RX 9060 XT、RX 9070 / 9070 XT、Radeon AI PRO R9700、RX 6800 / 6900 シリーズ。**VRAM 12 GB 以上**が必要です。 |
| **RAM** | 32 GB 以上。[利用できるモデル](#どのモデルを選べばいい)は RAM の容量によって異なります。64 GB あれば、どのサイズも利用できます。 |
| **ディスク** | 約 80 GB の空き容量。SSD を使うと初回の起動が大幅に速くなるため、おすすめです。 |
| **OS** | Windows 10 / 11 または Linux。NVIDIA または AMD の最新のグラフィックドライバーも必要です。 |
| **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) |
その他の必要なソフトウェアは、インストーラーが用意します。2 枚または 3 枚のカードにモデルを分散させることもできます([マルチ GPU](docs/MULTI_GPU.md))。
そのほかに必要なもの:
以下のハードウェアにも試験的に対応しています。いずれもコミュニティのメンバーが手元のマシンで実装・テストしたものです。
- 約80 GBの空きディスク容量(SSDなら初回の起動がずっと速くなります)
- Windows 10 / 11、Linux、または macOS
- 最新のグラフィックスドライバー
- **旧世代のグラフィックカード**(Tesla P40 / V100、GTX 10、Radeon VII / MI50、RX 6700 XT、RX 5500 XT):[旧世代 GPU の対応状況](docs/OLDER_GPUS.md)。
- **Intel Arc**(Linux でソースからビルド):[Intel Arc の対応状況](docs/INTEL_ARC.md)。
- **AMD Ryzen AI Max(Strix Halo)**(Linux でソースからビルド):[Strix Halo の対応状況](docs/STRIX_HALO.md)。
- **AVX2 非対応の旧世代 CPU**:動作しますが、速度は遅くなります。[旧世代 CPU の対応状況](docs/INSTALL.md#older-cpus-experimental)を参照してください。
必要な環境の一覧は、[docs/INSTALL.md](docs/INSTALL.md#what-you-need)をご覧ください。
同じメーカーのカードを2〜3枚使って、1つのモデルを動かせます([マルチGPU](docs/MULTI_GPU.md))。NVIDIA と AMD のカードにまたがってモデルを分けることはありません。
一覧は [docs/INSTALL.md](docs/INSTALL.md#what-you-need) にあります。
## インストール
### AI にセットアップしてもらう
3つの方法から1つ選んでください。どれも同じエンジンを起動し、同じモデルを使います。
Claude Code、Cursor、Codex、GitHub Copilot などの AI コーディングアシスタントを使っている場合は、次の文を貼り付けてください。
### 方法A: Strata アプリ
いちばん簡単な方法です。エンジンはアプリに入っているので、インストールしてモデルを選べばチャットを始められます。お使いのOSとグラフィックスカードに合うファイルを
[リリースページ](https://github.com/Niko1221/Strata/releases/latest)からダウンロードしてください。どれか分からない場合は、表の下の説明を見てください。
| OS | グラフィックスカード | ダウンロードするファイル |
| --- | --- | --- |
| **Windows 10 / 11** | NVIDIA RTX、ドライバー 580 以降 | [`Strata-0.2.1-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-cuda13.exe) |
| | NVIDIA、古いカード、またはドライバーが 580 より古い場合 | [`Strata-0.2.1-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-cuda12.exe) |
| | AMD Radeon RX 9000 シリーズまたは Radeon AI PRO R9700 | [`Strata-0.2.1-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-hip.exe) |
| | AMD Radeon RX 7000 シリーズまたは Ryzen AI Max (Strix Halo) | [`Strata-0.2.1-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-hip-rdna3.exe) |
| **Linux** | NVIDIA RTX、ドライバー 580 以降 | [`Strata-0.2.1-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-nvidia-cuda13.AppImage) |
| | NVIDIA、古いカード、またはドライバーが 580 より古い場合 | [`Strata-0.2.1-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-nvidia-cuda12.AppImage) |
| | AMD Radeon RX 9000 シリーズまたは Radeon AI PRO R9700 | [`Strata-0.2.1-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-rx9070.AppImage) |
| | AMD Radeon RX 7000 シリーズ | [`Strata-0.2.1-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-rx7900.AppImage) |
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.1-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-strixhalo.AppImage) |
| **Mac**(Apple Silicon、macOS 15 以降) | 内蔵 | [`Strata-0.2.1-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-mac.dmg) |
**迷ったときは:** NVIDIA の場合、ドライバーがバージョン 580 以降なら `cuda13`、それより古ければ `cuda12` を選びます。`cuda12` は古いカードと古いドライバー向けです。
`hip` と `amd` のファイルは AMD のカード用です。それ以外の AMD カード(RX 6000)には、まだ用意されたアプリがありません。
[AMD のカード](docs/AMD_HIP.md)を見てください。
そのあとの手順:
- **Windows:** `.exe` を実行します。ユーザー単位でインストールされ、管理者権限は不要です。「Windows によってPCが保護されました」と表示されたら、
**詳細情報**を選び、**実行**を選んでください(インストーラーにはまだコード署名がありません)。
- **Linux:** ファイルを実行可能にして(`chmod +x`)起動します。FUSE 2 が必要です。`sudo apt install libfuse2`(Ubuntu 22.04)
または `libfuse2t64`(Ubuntu 24.04 以降)を実行してください。
- **Mac:** `.dmg` を開き、Strata を「アプリケーション」にドラッグします。
最初の画面で、アプリがPCを調べて合うモデルをすすめます。インストールを押してください。モデルは大きく(約40〜70 GB)、中断しても続きからダウンロードできます。
ウィンドウを閉じると、Strata とエンジンが止まります。
### 方法B: ターミナル(1行)
ターミナル版は `strata` コマンドです。スクリプトやサーバー、AI エージェント向けです。
Linux と Mac:
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
```
Windows(PowerShell):
```powershell
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
```
スクリプトはPCを調べ、ターミナル版をダウンロードし、SHA-256 を検証して、ホームフォルダに展開します。管理者権限は不要です。Windows では、終わったら
新しいターミナルを開いてください。そのあと Strata を起動します。
```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、ディスクを確認して、PC に合ったモデルを選びます。インストールと起動を行い、
アプリとの接続方法も案内します。また、Strata の [MCP サーバー](docs/MCP_SERVER.md) を使えば、
AI ツールから Strata のインストール・起動・停止を行えます。
AI はグラフィックスカード、RAM、ディスクを調べ、合うモデルを選んで、インストールと起動をし、アプリの接続方法を教えてくれます。
AI ツールは [MCP サーバー](docs/MCP_SERVER.md)を通じて、Strata のインストール、起動、停止もできます。
### 自分でやる
自分でビルドしたい方は[ソースからビルド](#ソースからビルド)を見てください。
[Strata をダウンロード](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip)して展開します(`git clone` でも構いません)。
**Windows:** **`START-HERE.bat`** をダブルクリックします。**Linux:** Strata フォルダーで **`./setup.sh`** を実行します。
## モデル
**Mac(Apple シリコン、M1〜M5、macOS 15 以降):** まず一度だけ Apple の Command Line Tools をインストールします(ターミナルで `xcode-select --install`。Xcode 本体は不要です)。次に Strata フォルダーでターミナルを開き、**`./setup.sh`** を実行します。Mac 用エンジンをお使いの Mac 上でビルドし(約 7 分)、同じ質問をしてからモデルをダウンロードして起動します。次回からは `./setup.sh` ですぐに起動します。どのサイズが Mac に合うかと速度:[docs/MAC.md](docs/MAC.md)。
アプリと `strata launch` は、メモリ量に合うモデルをすすめます。モデルは1つで、圧縮の度合いが違う複数のサイズがあります。
小さいサイズは速く、大きいサイズは少し賢くなります。
NVIDIA でも AMD でも手順は同じです。インストーラーがカードを検出し、対応するエンジンを用意します。
続いて、次の項目を設定します。
### メモリが少ないなら Gyro
- 使用するモデルとサイズ
- コンテキストの長さ(モデルが覚えておけるテキストの量)
- 画像を読み取る機能を使うかどうか
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF)(agentionai 製)は、このモデルをいちばん小さく動かせる方法です。
各項目で Enter を押すと、推奨設定が選ばれます。設定が終わるとモデル(約 70 GB)をダウンロードし、起動します。
ダウンロードが途中で止まった場合は、もう一度実行すると続きから再開できます。起動が完了すると、ブラウザーで
`http://127.0.0.1:8080` が開き、Strata の画面が表示されます。
| サイズ | 必要なメモリ(RAM + VRAM) | ダウンロード | 内容 |
| --- | ---: | ---: | --- |
| **Gyro-S** | 27.6 GB | 58.5 GB | ほかのサイズが入らないPC向け。品質は IQ2_XS より下がります。 |
| **Gyro-M** | 35 GB | 92.0 GB | 品質は IQ3_XXS 程度で、GPUメモリの使用量は約20%少なくなります。12 GBのカードと R9700 では、回答の書き出しが Q2_0 より17〜21%遅くなります。 |
> **モデルの起動中は、1-3 分ほど PC の動作が遅くなったり、応答しなくなったりすることがあります**(初回は特に時間がかかります)。
> Strata が 35-55 GB のデータを RAM に読み込み、その一部をグラフィックカード用に固定(ピン留め)しているためです。
> 正常な動作なので、ウィンドウに表示される処理状況を確認しながら、閉じずにお待ちください。
Gyro は NVIDIA と AMD のカードで動きます(AMD は実験的)。Intel Arc と Mac では動きません。詳細と測定結果は
[docs/GYRO.md](docs/GYRO.md) にあります。`strata launch --model gyro-s` で起動します。
**次回からは** `START-HERE.bat`(または `./setup.sh`)を実行するだけですぐに起動できます。2 回目以降は、ダウンロード済みのファイルを再度取得する必要はありません。
モデルを停止するにはウィンドウを閉じてください。`UPDATE.bat`(`./update.sh`)を使うと、Strata を起動せずに更新できます。
更新方法や Docker、複数のカードの使い方、ファイルの保存先、各種オプションについては、[docs/INSTALL.md](docs/INSTALL.md)をご覧ください。
### ほかのサイズ
## どのモデルを選べばいい
インストーラーが RAM の容量に合ったモデルをおすすめします。同じモデルでも、圧縮率によって複数のサイズが用意されています。
サイズが小さいほど高速に動作し、大きいほど回答の品質が少し高くなります。
| RAM | おすすめのモデル | 選ぶ理由 |
| メモリ(RAM + VRAM) | 選ぶモデル | 備考 |
| --- | --- | --- |
| **32 GB** | **Coder** | 32 GB で動作するコーディング向けモデル(24 GB のカードがあれば Q2_0 と IQ2_XS も動作) |
| **48 GB** | **IQ2_XS**(速度優先なら Q2_0) | これより大きいサイズはメモリに収まらない |
| **64 GB** | **IQ2_XS**(おすすめ)、または IQ3_XXS / IQ3_S | どのサイズも利用可能。IQ3_S は回答の品質が最も高い一方、速度は最も遅い |
| **96 GB 以上** | **IQ3_S**、または Unsloth の UD-IQ4_XS(約 4-bit) | 他のアプリを開いたままでも、最大サイズのモデルを動かせるだけの余裕がある |
| **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 より17〜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 から読むため、ずっと遅くなります |
- **[Coder](docs/MODELS.md#coder):** エキスパートを半分に減らしたコーディング向けモデルです。SWE-bench Verified ではフルモデルのスコアの
91% を達成しており(開発者による測定)、32 GB の RAM で動作します。ただし、コード以外の用途では性能が劣り、中国語などの CJK 言語の処理も苦手です(#438)。
こうした用途には、すべてのエキスパートを残した Q2_0、IQ2_XS、IQ3_S を選んでください。
- **[Swift 1.5](docs/MODELS.md#swift-15):** 回答前の思考時間が大幅に短くなるようファインチューニングしたモデルです。
回答の品質をほぼ保ったまま、より短い時間で答えが返ってきます。
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** Unsloth による約 4-bit のモデルです。回答の品質は IQ3_S と UD-Q4_K_XL の中間です。
ダウンロード容量は 94 GB です。RAM が約 80 GB 未満の場合、回答の生成中にモデルの一部を SSD から読み込むため、速度が落ちます(NVMe SSD を使うと改善します)。
- **[Unsloth UD-Q4_K_XL / UD-Q5_K_XL / UD-Q6_K_XL / Q8_0](docs/MODELS.md#unsloths-files-ud-iq4_xs-to-q8_0):** 4〜8 ビットで、フルモデルにより近いモデルです
(次のトークンが Q8_0 と一致する割合は 93〜95%)。すべてのエキスパートが RAM に収まる場合(UD-Q4_K_XL で約 88 GB、
Q8_0 で約 136 GB)は高速です。RAM が足りない場合は回答の生成中に一部を SSD から読み込むため、大幅に遅くなります
(RAM が 64 GB の PC で UD-Q4_K_XL は毎秒 7-8.5 トークン程度)。
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** 手動でのセットアップが必要です。
インストーラーのメニューからは選べません。
表の行より RAM が少なくても動くことがあります。Strata は自動で[低RAMモード](docs/MODELS.md#a-big-graphics-card-and-little-ram)に切り替わります。
速度は落ち、グラフィックスカードが小さいとさらに大きく落ちます。
各サイズのダウンロード情報や必要なメモリ容量は、[docs/MODELS.md](docs/MODELS.md)をご覧ください。後から別のモデルを追加する場合は、
`SETUP.bat`(Linux:`./setup.sh --setup`)を実行してください。
そのほかの選択肢:
- **[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) にあります。
## どのくらい速い?
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) にあります。
## 使い方
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="Strata の Monitor タブ(左)と、コードを生成するコーディングエージェント(右)"><br>
<sub>動画に登場する五重塔の庭を作成中の画面。左が Strata の <b>Monitor</b>、右がコードを生成しているコーディングエージェント</sub></p>
### アプリで
- **ブラウザーで使う:** `http://127.0.0.1:8080` を開きます。チャット用の **Chat**、モデルや GPU/CPU/RAM の状態をリアルタイムで確認できる **Monitor**、
設定や接続先のアドレスを確認できる **About** の各タブがあります。
- **アプリやコーディングエージェントと連携する:** 「OpenAI-compatible」プロバイダーを追加し、ベース URL に
**`http://127.0.0.1:8080/v1`** を指定します。API キーとモデル名には任意の値を指定できます。
- Anthropic API を使うアプリ:`http://127.0.0.1:8080/v1/messages`(Claude Code では
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080` を設定)。
- Codex CLI など、OpenAI Responses API を使うアプリ:`/v1/responses`
([設定方法](docs/DETAILS.md#the-responses-api-and-codex-cli))。
- **思考(Thinking)の設定:** チャットのメニュー、または連携先アプリの「reasoning effort」で **off、low、medium、high** から選びます。
off が最も速く、難しい質問には high が向いています。
- **画像を読み取る:** セットアップ時に「Images?」で yes を選びます。その後、チャットの **Picture** ボタンをクリックするか、連携先のアプリで画像を添付してください。
AMD のカードを使う場合、Linux では CPU で画像を読み取ります。Windows ではまだ画像の読み取りに対応していません。
- **スマートフォンや別の PC から使う:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>` で起動します。API キーは必ず設定してください。
- **リクエストは 1 つずつ:** 標準設定では 1 つずつ処理し、それ以外のリクエストは順番を待ちます。同時に処理するには、
`"parallel": 2` を設定してください([BATCHING.md](docs/BATCHING.md))。12 GB のカードでは、リクエストごとの回答速度が落ちます。
- **長いプロンプトを送る:** チャットの最初のメッセージは全文を読み込むため、30,000 トークンあたり約 1 分かかります。
2 通目以降は数秒で応答が始まります。
アプリには、**チャット**、モデルとGPU・CPU・RAMの状態を見る**モニター**、**モデル**ページ、**設定**があります。チャットはPC上のフォルダで作業できます。
**手動**モードでは、ファイルを変更したりコマンドを実行したりする前に確認します。`/loop 10m <やること>` と入力すると、チャットを開いている間、メッセージを10分ごとに繰り返し送ります。
[チャットの保存場所](docs/INSTALL.md#where-things-are-stored)や[API の詳細](docs/DETAILS.md#using-it)も参照してください。
チャットは DuckDuckGo(または Bing)でウェブ検索ができ、アカウントは不要です。**設定、ツール**で検索キーを追加することもできます。
検索は、PCの外に何かを送る唯一の機能で、送られるのは検索語です。
### オプション:モデル全体が RAM に収まる場合、GPU 1 枚でさらに高速化
2つのモデルを動かすには、**複数のモデルを実行**をオンにして、それぞれのモデルに同じメーカーのカードを1枚ずつ割り当てます。
2 つのオプトイン設定です(RubenTorney 氏による #1904 と #1905)。GPU を 1 枚使い、RAM にすべてのエキスパートが収まる場合(Strata の arena モード。例:64 GB の PC で Q2_0 や IQ3_XXS)に効果があります。Strata のデータフォルダーにある設定ファイル `strata-<model>.json`([場所](docs/INSTALL.md#where-things-are-stored))を開き、次を追加します:
<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>
- **生成の高速化:** `"args"` リストに `"--adapt-async", "1"`。エキスパートキャッシュの更新が回答を止めずに補助スレッドで行われます。RTX 5070(Windows、Q2_0)での実測:生成 +4〜5%、コードや長文では +7〜10%。回答はデフォルトとわずかに異なる場合があります(実行ごとには同じです)。
- **Windows でのプロンプト読み込みの高速化:** `"env"` ブロックに `"STRATA_PIN_BY_NEED": "1"`。Windows はGPU 用に RAM の約半分しか固定できないため、この設定で GPU が本当に必要とするエキスパートから先に固定します。RTX 5070 での実測:プロンプト読み込み +4.4%。
### ターミナルで
<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 を再起動してください(`START-HERE.bat` または `./setup.sh`)。元に戻すには、2 つの項目を削除します。
変更したら Strata を再起動してください。元に戻すには、2つの項目を削除します。
</details>
## 仕組み
このようなモデルは、ふつう数百GBのグラフィックスメモリを持つサーバーで動きます。お使いのグラフィックスカードは12〜24 GBです。Strata は、PC全体で作業を分担して、モデルを収めます。
図書館にたとえてみます。
- モデルは、24,576人の小さな専門家(「エキスパート」)からなるチームです。24,576冊の参考書のようなものです。1つの単語に必要なのは、そのうち10冊だけです。
- **グラフィックスカードは机です。** 小さくて速く、最もよく使う約4,000冊が開いて置いてあります。
- **RAM は閲覧室の書棚です。** 24,576冊すべてがここにあります。単語に必要な本が机になければ、プロセッサがすぐに書棚から読み、その間もカードは作業を続けます。
- **SSD は書庫です。** 29 GBの参照テーブルがしまってあります。モデルは単語ごとにその数行だけを読むので、RAM に置いておく必要はありません。
<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>
小さな助手が、次の数単語も予想します。大きなモデルがそれをまとめて確認し、正しいものだけを残します。答えは同じで、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>
詳しい説明は [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md) にあります。各部分と数値は
[詳細](docs/DETAILS.md#how-it-works)と[論文](docs/paper/Strata-Paper.pdf)を見てください。
## うまくいかないとき
- **初回起動時に PC が応答しなくなる。** モデルの読み込み中は、一時的に応答しなくなることがあります。ウィンドウを閉じずにお待ちください。
10 分たっても応答しない場合は、PC を再起動して他のアプリを閉じてからやり直すか、モデルのサイズを小さくしてください。
- **ダウンロードやインストールが途中で止まる。** `START-HERE.bat`(または `./setup.sh`)をもう一度実行してください。
中断したところから再開できます。
- **動作が極端に遅く、ディスクのアクセスランプが点滅し続ける。または「the engine stopped unexpectedly」と表示される。** RAM の空き容量が不足しています。
ブラウザーなど、RAM を多く使う他のアプリを閉じるか、小さいモデル(Q2_0 または IQ2_XS)を選んでください。
- **ポート 8080 が使用中と表示される。** Strata がすでに起動しています。起動済みのウィンドウを探してください。
- **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 がすでに動いています。ウィンドウを探してください。
その他の問題と対処方法は、[docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)をご覧ください。解決しない場合は
[Issue](https://github.com/Niko1221/Strata/issues) を作成し、Strata フォルダー内の `strata-<model>.log` を添付してください。
セキュリティ上の問題は、[SECURITY.md](SECURITY.md)の手順に従って非公開で報告してください。
## しくみ
この規模のモデルは通常、数百ギガバイトの GPU メモリを搭載したサーバーで動かします。一方、手元のグラフィックカードのメモリは
12-24 GB 程度です。Strata は **PC 全体で処理を分担する** ことで、こうした環境でもモデルを動かせるようにしています。
台所で、よく使うものは調理台に置き、それ以外は食品庫にしまっておくような仕組みです。
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="24,576 個のエキスパートのうち、よく使うものをグラフィックカードに配置。全エキスパートは RAM に、ルックアップテーブルは SSD に保持"></p>
- **モデルは「エキスパート」と呼ばれる 24,576 個の小さな処理単位から構成されています。** 1 語の生成に必要なのは、このうち 10 個だけです。
- **グラフィックカード** に、使用頻度の高い数千個のエキスパートを置きます。**RAM** にはすべてのエキスパートを保持し、
**CPU** が残りの処理を並行して担当します。**SSD** には大きなルックアップテーブルを保存します。
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="小さな補助モデルが次の数語を予測し、大きなモデルがまとめて検証。正しい予測だけを採用する"></p>
- **予測してから検証:** 小さな補助モデルが次の数語を予測し、大きなモデルがまとめて検証します。
回答を変えずに、生成速度を 1.6-1.8 倍に高められます。
- **長いテキストはまとめて読み込みます**(一度に最大 8,192 トークン)。読み込み速度は毎秒 1,000 トークンを超えます。
仕組みについては、[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md)で詳しく説明しています。各処理の詳細や測定値は、
[技術資料](docs/DETAILS.md#how-it-works)と[論文](docs/paper/Strata-Paper.pdf)をご覧ください。
そのほかの問題と対処法は [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) にあります。それでも解決しない場合は、
[issue](https://github.com/Niko1221/Strata/issues) を作成してください。アプリでは、**設定、エンジン、診断、ログを見る、ログを保存**で作られるファイルを添付します。
ターミナルでは、`strata doctor` の出力を貼り付けます。ソースから動かしている場合は、Strata フォルダの `strata-<model>.log` を添付してください。
セキュリティ上の問題は、非公開で報告してください: [SECURITY.md](SECURITY.md)。
## クレジットとライセンス
使用しているモデルは、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)、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](LICENSE) で公開されていますが、一部の構成要素と
各モデルには、それぞれ別のライセンスが適用されます([ライセンスの詳細](docs/HOW_IT_WORKS.md#license))。
モデルは 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))。
## Strata を支援する
## コントリビューション
Strata は無料で使えるオープンソースソフトウェアです。役に立ったら、開発へのご支援をお願いします。
バグを見つけた、変更やベンチマークを送りたい、という方は、先に [CONTRIBUTING.md](CONTRIBUTING.md) を読んでください。
ほかの人があなたの成果を確認できるように、何を書くべきかが説明してあります。
<p align="center"><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></p>
## Strata を応援する
Strata は無料で、MIT ライセンスです。私が入手できるハードウェアで開発とテストをしています。お使いのPCで快適に動いたなら、コーヒー1杯分の支援は、
新しいカードのテストと、報告いただいた不具合の修正を続ける助けになります。無理のない範囲で大丈夫です。使って、気づいたことを報告してもらえるだけでも同じように助かります。
<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) から連絡してください。
+323 -170
View File
@@ -1,243 +1,396 @@
<h1 align="center">Strata</h1>
<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">
</picture>
</p>
**English** · [简体中文](README.zh-CN.md) · [日本語](README.ja.md) · [Deutsch](README.de.md) · [Français](README.fr.md) · [Español](README.es.md) · [Português](README.pt-BR.md)
<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>
<p align="center"><b>Run a 125-billion-parameter AI model on your own gaming PC</b><br>
NVIDIA or AMD graphics card (12 GB or more) · Windows or Linux · free and open source</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>
<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>
</p>
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="A voxel pagoda garden that Strata's model wrote, running in the browser"></a><br>
<sub>A voxel pagoda garden, 1 shot prompt running on an RTX 5070 with Strata (IQ3_S, 128K context) ·
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">full video (49 s)</a></sub></p>
Strata runs **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** on a normal PC. This is a
large, smart AI model that usually needs a server. It chats, writes code, reads pictures and works with your apps
and coding agents. Nothing leaves your PC.
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.
## How fast is it?
**New here?** Check [what you need](#what-you-need), then [install the app](#option-a-the-strata-app).
We measured it on two ordinary gaming PCs. A token is about ¾ of a word.
- **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).
- **Writes answers:** how fast the reply appears in a short chat. 60 tokens per second is faster than you can read.
- **Reads your prompt:** how fast it takes in what you send (here a 32K-token document, code or chat history).
<table>
<tr><th>NVIDIA: RTX 5070 (12 GB), Ryzen 5 7600, 64 GB RAM</th><th>AMD: RX 9070 XT (16 GB), Ryzen 9 3900X, 47 GB RAM</th></tr>
<tr><td>
| Size | Writes answers | Reads your 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 |
</td><td>
| Size | Writes answers | Reads your 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 |
</td></tr>
</table>
NVIDIA: Q2_0 with engine 0.1.36, the other rows with 0.1.26 (4K answers, 32K prompts). The full tables are in
[DETAILS.md](docs/DETAILS.md#speed-measured). A card with more VRAM is faster: an RTX 3090 (24 GB) should write
about 100-140 tokens per second. Long chats and other cards: [speed of each model](docs/MODELS.md#how-fast-is-each-size),
[community results](docs/COMMUNITY_BENCHMARKS.md).
<p align="center"><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><br>
<sub>Strata is free. If it runs well on your PC, a coffee keeps the work on it going.</sub></p>
Version 0.2.1 · engine 0.1.46
## What you need
| | |
**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 |
| --- | --- |
| **Graphics card** | **NVIDIA** GeForce RTX 20, 30, 40 or 50 series, or **AMD** Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700 or RX 6800 / 6900 series. It needs **12 GB of VRAM or more**. |
| **RAM** | 32 GB or more. Your RAM decides [which model](#which-model-should-i-pick) fits. 64 GB runs every size. |
| **Disk** | About 80 GB free. Use an SSD if you can: the first start is much faster. |
| **System** | Windows 10 / 11 or Linux, and a current graphics driver from NVIDIA or AMD. |
| **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) |
The installer sets up everything else. Two or three cards can share the model ([multi-GPU](docs/MULTI_GPU.md)).
Also needed:
Experimental, written and tested by community members on their own machines:
- about 80 GB of free disk (an SSD makes the first start much faster)
- Windows 10 / 11, Linux or macOS
- a current graphics driver
- **Older graphics cards** (Tesla P40 / V100, GTX 10, Radeon VII / MI50, RX 6700 XT, RX 5500 XT): [Older GPUs](docs/OLDER_GPUS.md).
- **Intel Arc**, built from source on Linux: [Intel Arc](docs/INTEL_ARC.md).
- **Apple Silicon Macs (M1 to M5)**, built from source by `./setup.sh` (the Metal engine): [Mac](docs/MAC.md).
- **AMD Ryzen AI Max (Strix Halo)**, built from source on Linux: [Strix Halo](docs/STRIX_HALO.md).
- **Older processors without AVX2**: they work, but slowly. [Older CPUs](docs/INSTALL.md#older-cpus-experimental).
The full list: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
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).
## Install
### Let your AI set it up
Pick one option. They all start the same engine and use the same models.
Do you use an AI coding assistant (Claude Code, Cursor, Codex, GitHub Copilot, ...)? Paste this into it:
### Option A: the Strata 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.
| System | Your graphics card | File to download |
| --- | --- | --- |
| **Windows 10 / 11** | NVIDIA RTX, driver 580 or newer | [`Strata-0.2.1-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-cuda13.exe) |
| | NVIDIA, older card or driver older than 580 | [`Strata-0.2.1-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-cuda12.exe) |
| | AMD Radeon RX 9000 series or Radeon AI PRO R9700 | [`Strata-0.2.1-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-hip.exe) |
| | AMD Radeon RX 7000 series or Ryzen AI Max (Strix Halo) | [`Strata-0.2.1-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-hip-rdna3.exe) |
| **Linux** | NVIDIA RTX, driver 580 or newer | [`Strata-0.2.1-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-nvidia-cuda13.AppImage) |
| | NVIDIA, older card or driver older than 580 | [`Strata-0.2.1-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-nvidia-cuda12.AppImage) |
| | AMD Radeon RX 9000 series or Radeon AI PRO R9700 | [`Strata-0.2.1-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-rx9070.AppImage) |
| | AMD Radeon RX 7000 series | [`Strata-0.2.1-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-rx7900.AppImage) |
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.1-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-strixhalo.AppImage) |
| **Mac** (Apple Silicon, macOS 15 or newer) | built in | [`Strata-0.2.1-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-mac.dmg) |
**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. Other AMD cards (RX 6000) have no
ready-made app yet: see [AMD cards](docs/AMD_HIP.md).
Then:
- **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.
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.
### Option B: the Terminal (one line)
The Terminal is the `strata` command, for scripts, servers and AI agents.
Linux and Mac:
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
```
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:
```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 and picks the model that fits. Then it 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).
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).
### Or do it yourself
Prefer to build it yourself? See [Build from source](#build-from-source).
[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.
## Models
**Mac (Apple Silicon, M1 to M5, macOS 15 or newer):** install Apple's Command Line Tools once
(`xcode-select --install` in Terminal; Xcode itself is not needed). Then 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 and how fast it is:
[docs/MAC.md](docs/MAC.md).
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.
The steps are the same for NVIDIA and AMD. The installer finds your card and sets up the right engine for it. It
asks you a few questions:
### Low on memory? Try Gyro
- which model and which size,
- how much context (how much text the model keeps in mind),
- whether it should read pictures.
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) (by agentionai) is the smallest way to run this model.
Press Enter each time for the recommended answer. Then it downloads the model (about 70 GB) and starts it. If the
download stops, run it again: it continues where it left off. Your browser opens the Strata app at
`http://127.0.0.1:8080`.
| 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 17-21% slower than Q2_0 on a 12 GB card and on an R9700. |
> **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. This is normal. Wait, and don't
> close the window. The window shows what Strata is doing.
Gyro runs on NVIDIA and AMD cards (AMD is experimental), not on Intel Arc or the Mac. Details and measurements:
[docs/GYRO.md](docs/GYRO.md). Start it with `strata launch --model gyro-s`.
**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).
### The other sizes
**Is it fast on your PC?** Run `BENCH.bat` (`./bench.sh`): about 5-10 minutes, it measures chat, 4K/16K prompts and an agent-style session with your own settings and writes a report to share ([how](docs/COMMUNITY_BENCHMARKS.md#run-the-benchmark)).
## Which model should I pick?
The installer recommends one for your RAM. The same model comes in several sizes, compressed more or less. Smaller
sizes are faster. Larger sizes are a bit smarter.
| Your RAM | Take | Why |
| Your memory (RAM + VRAM) | Take | Notes |
| --- | --- | --- |
| **32 GB** | **Coder** | it fits 32 GB, and it is made for code (with a 24 GB card, Q2_0 and IQ2_XS run too) |
| **48 GB** | **IQ2_XS** (or Q2_0, the fastest) | the larger sizes do not fit |
| **64 GB** | **IQ2_XS** (recommended), or IQ3_XXS / IQ3_S | every size fits; IQ3_S is the best and the slowest |
| **96 GB or more** | **IQ3_S**, or Unsloth's UD-IQ4_XS (~4-bit) | room for the largest sizes with everything else open |
| **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 17-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 |
- **[Coder](docs/MODELS.md#coder):** a coding version with half of the experts removed. It reaches 91% of the full
model's SWE-bench Verified score (measured by its authors) and fits 32 GB of RAM. It is weaker outside code,
including Chinese and other CJK text (#438). For those, take Q2_0, IQ2_XS or IQ3_S, which keep every expert.
- **[Swift 1.5](docs/MODELS.md#swift-15):** a fine-tune that thinks for a much shorter time before it answers. You
get the answer sooner, at about the same quality.
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** Unsloth's ~4-bit version, between IQ3_S and
UD-Q4_K_XL in quality. A 94 GB download. With less than ~80 GB of RAM, Strata reads part of it from the SSD
while it answers, so it is slower there (an NVMe SSD helps).
- **[Unsloth UD-Q4_K_XL, UD-Q5_K_XL, UD-Q6_K_XL and Q8_0](docs/MODELS.md#unsloths-files-ud-iq4_xs-to-q8_0):**
4- to 8-bit, closer to the full model (93-95% the same next token as Q8_0). Fast where every expert fits in RAM
(~88 GB for UD-Q4_K_XL up to ~136 GB for Q8_0); with less RAM Strata reads part of them from the SSD while it
answers, which is much slower (7-8.5 tokens/s for UD-Q4_K_XL on a 64 GB PC).
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** you set it up by hand. It is
not in the installer's menu.
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.
Sizes, downloads and what fits where: [docs/MODELS.md](docs/MODELS.md). To add another model later, run
`SETUP.bat` (Linux: `./setup.sh --setup`).
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).
## 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
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="The Strata app's Monitor tab next to a coding agent"><br>
<sub>The Strata app's <b>Monitor</b> (left) while a coding agent writes the pagoda garden from the video (right)</sub></p>
### In the app
- **In the browser:** open `http://127.0.0.1:8080`. It has **Chat**, a live **Monitor** of the model and your
GPU/CPU/RAM, and **About** with the settings and addresses.
- **Your apps and coding agents:** add an "OpenAI-compatible" provider with the base URL
**`http://127.0.0.1:8080/v1`**. Any API key and any model name work.
- Apps that use Anthropic's API: `http://127.0.0.1:8080/v1/messages` (Claude Code:
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`).
- Codex CLI and other apps that use the OpenAI Responses API: `/v1/responses`
([setup](docs/DETAILS.md#the-responses-api-and-codex-cli)).
- **Thinking:** choose **off, low, medium or high** in the chat menu or in your app's "reasoning effort". Off is the
fastest. High is best for hard questions.
- **Pictures:** say yes to "Images?" in setup. Then click **Picture** in the chat, or attach pictures in your app.
AMD cards read pictures on Linux through the processor; on Windows they can't yet.
- **From your phone or another PC:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`. Always set a key.
- **One request at a time:** by default Strata answers one request, and the others wait. To answer several at once,
set `"parallel": 2` ([BATCHING.md](docs/BATCHING.md)). On a 12 GB card this makes each answer slower.
- **Long prompts:** Strata reads the first message of a chat in full, about 1 minute per 30,000 tokens. Follow-up
messages start in seconds.
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.
More: [where your chats are stored](docs/INSTALL.md#where-things-are-stored), [the API](docs/DETAILS.md#using-it).
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.
### Optional: more speed on one card when the whole model fits in RAM
To run two models, turn on **Run multiple models** and give each model its own card of the same make.
Two opt-in settings (by RubenTorney, #1904 and #1905). They help when you run one graphics card and your RAM holds
every expert (Strata's arena mode, for example Q2_0 or IQ3_XXS on a 64 GB PC). Open your config,
`strata-<model>.json` in the Strata data folder ([where](docs/INSTALL.md#where-things-are-stored)), and add:
<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>
- **Faster decode:** `"--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 (they stay the same from run to run).
- **Faster prompt reading on Windows:** `"STRATA_PIN_BY_NEED": "1"` in the `"env"` block. Windows can lock only about
half the RAM for the graphics card; this locks the experts the card actually needs first. Measured on the RTX 5070:
+4.4% prompt reading.
### 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 (`START-HERE.bat` or `./setup.sh`) after the change. To undo, remove the two entries.
Restart Strata after the change. To undo, remove the two entries.
## Something went wrong?
- **My PC froze the first time Strata started.** This is normal while it loads the model. 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.** Run `START-HERE.bat` (or `./setup.sh`) again. It continues where
it stopped.
- **It's very slow and 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 (Q2_0 or IQ2_XS).
- **It says port 8080 is already in use.** Strata is already running. Look for its window.
More problems and their fixes: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Still stuck? Open an
[issue](https://github.com/Niko1221/Strata/issues) and attach `strata-<model>.log` from the Strata folder. Found a
security problem? Report it privately: [SECURITY.md](SECURITY.md).
</details>
## How does it work?
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**. Think of a kitchen: the things
you use all the time stay on the counter, and the rest waits in the pantry.
12-24 GB. Strata makes the model fit by sharing the work across your whole PC. Picture a library.
- 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.
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="The model's 24,576 experts: the busiest on the graphics card, all of them in RAM, a lookup table on the SSD"></p>
- **The model is a team of 24,576 small specialists ("experts").** Each word needs only 10 of them.
- **Your graphics card** keeps the few thousand experts that are used most often. **Your RAM** holds all of them,
and **your processor** works on the rest at the same time. **Your SSD** holds a big lookup table.
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.
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="A small helper guesses the next words; the big model checks them all at once and keeps the right ones"></p>
- **Guess, then check:** a small helper guesses the next few words. The big model checks them all at once. 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.
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).
## Something went wrong?
- **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.
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).
## Credits and license
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)
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)).
[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)).
## 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.
## Support Strata
Strata is free and open source. If it is useful to you, you can support its development:
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.
<p align="center"><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></p>
<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>
Interested in sponsoring Strata? Contact me via [GitHub](https://github.com/Niko1221).
+328 -168
View File
@@ -1,234 +1,394 @@
<h1 align="center">Strata</h1>
<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">
</picture>
</p>
[English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja.md) · [Deutsch](README.de.md) · [Français](README.fr.md) · [Español](README.es.md) · **Português**
<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>
<p align="center"><b>Rode um modelo de IA de 125 bilhões de parâmetros no seu próprio PC gamer</b><br>
Placa de vídeo NVIDIA ou AMD (12 GB ou mais) · Windows ou Linux · gratuito e de código aberto</p>
<h3 align="center">Rode um modelo de ponta no seu próprio PC gamer</h3>
<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="Um jardim com pagode em voxels que o modelo do Strata escreveu, rodando no navegador"></a><br>
<sub>Um jardim com pagode em voxels, feito com um único prompt numa RTX 5070 com o Strata (IQ3_S, contexto de 128K) ·
<p align="center">Suporte a NVIDIA, AMD, Intel · Windows, Linux, Mac · gratuito e de código aberto</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>
</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>
O Strata roda o **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** num PC comum. É um modelo
de IA grande e inteligente que normalmente precisa de um servidor. Ele conversa, escreve código, entende imagens e
funciona com seus apps e agentes de programação. Nada sai do seu PC.
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.
## Qual é a velocidade?
**Primeira vez aqui?** Veja [o que você precisa](#o-que-você-precisa) e depois [instale o app](#opção-a-o-app-strata).
Medimos em dois PCs gamer comuns. Um token equivale a cerca de ¾ de uma palavra.
- **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).
- **Escreve respostas:** a velocidade com que a resposta aparece numa conversa curta. 60 tokens por segundo é mais rápido do que você consegue ler.
- **Lê seu prompt:** a velocidade com que ele absorve o que você envia (aqui, um documento, código ou histórico de conversa de 32K tokens).
Versão 0.2.1 · mecanismo 0.1.46
<table>
<tr><th>NVIDIA: RTX 5070 (12 GB), Ryzen 5 7600, 64 GB de RAM</th><th>AMD: RX 9070 XT (16 GB), Ryzen 9 3900X, 47 GB de RAM</th></tr>
<tr><td>
## O que você precisa
| Tamanho | Escreve respostas | Lê seu 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 |
**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.
</td><td>
- Um PC de 32 GB com uma placa de 12 a 16 GB roda o Coder.
- 64 GB de RAM rodam todos os tamanhos.
| Tamanho | Escreve respostas | Lê seu 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 |
</td></tr>
</table>
NVIDIA: Q2_0 com o engine 0.1.36, as outras linhas com 0.1.26 (respostas de 4K, prompts de 32K). As tabelas
completas estão em [DETAILS.md](docs/DETAILS.md#speed-measured). Uma placa com mais VRAM é mais rápida: uma
RTX 3090 (24 GB) deve escrever cerca de 100-140 tokens por segundo. Conversas longas e outras placas:
[velocidade de cada modelo](docs/MODELS.md#how-fast-is-each-size), [resultados da comunidade](docs/COMMUNITY_BENCHMARKS.md).
<p align="center"><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><br>
<sub>O Strata é gratuito. Se ele roda bem no seu PC, um café ajuda a manter o trabalho nele.</sub></p>
## Do que você precisa
| | |
| Seu computador | Detalhes |
| --- | --- |
| **Placa de vídeo** | **NVIDIA** GeForce RTX séries 20, 30, 40 ou 50, ou **AMD** Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700 ou séries RX 6800 / 6900. Ela precisa de **12 GB de VRAM ou mais**. |
| **RAM** | 32 GB ou mais. Sua RAM decide [qual modelo](#qual-modelo-devo-escolher) cabe. Com 64 GB, roda qualquer tamanho. |
| **Disco** | Cerca de 80 GB livres. Use um SSD se puder: a primeira inicialização fica bem mais rápida. |
| **Sistema** | Windows 10 / 11 ou Linux, e um driver de vídeo atualizado da NVIDIA ou da AMD. |
| **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) |
O instalador cuida de todo o resto. Duas ou três placas podem dividir o modelo ([multi-GPU](docs/MULTI_GPU.md)).
Você também precisa de:
Experimental, escrito e testado por membros da comunidade nas próprias máquinas:
- 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
- **Placas de vídeo mais antigas** (Tesla P40 / V100, GTX 10, Radeon VII / MI50, RX 6700 XT, RX 5500 XT): [Older GPUs](docs/OLDER_GPUS.md).
- **Intel Arc**, compilado a partir do código-fonte no Linux: [Intel Arc](docs/INTEL_ARC.md).
- **AMD Ryzen AI Max (Strix Halo)**, compilado a partir do código-fonte no Linux: [Strix Halo](docs/STRIX_HALO.md).
- **Processadores mais antigos sem AVX2**: funcionam, mas devagar. [Older CPUs](docs/INSTALL.md#older-cpus-experimental).
A lista completa: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
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).
## Instalação
### Deixe sua IA instalar
Escolha uma opção. Todas iniciam o mesmo mecanismo e usam os mesmos modelos.
Você usa um assistente de programação com IA (Claude Code, Cursor, Codex, GitHub Copilot, ...)? Cole isto nele:
### Opção A: o app Strata
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.
| Sistema | Sua placa de vídeo | Arquivo para baixar |
| --- | --- | --- |
| **Windows 10 / 11** | NVIDIA RTX, driver 580 ou mais novo | [`Strata-0.2.1-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-cuda13.exe) |
| | NVIDIA, placa mais antiga ou driver anterior ao 580 | [`Strata-0.2.1-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-cuda12.exe) |
| | AMD Radeon série RX 9000 ou Radeon AI PRO R9700 | [`Strata-0.2.1-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-hip.exe) |
| | AMD Radeon série RX 7000 ou Ryzen AI Max (Strix Halo) | [`Strata-0.2.1-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-hip-rdna3.exe) |
| **Linux** | NVIDIA RTX, driver 580 ou mais novo | [`Strata-0.2.1-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-nvidia-cuda13.AppImage) |
| | NVIDIA, placa mais antiga ou driver anterior ao 580 | [`Strata-0.2.1-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-nvidia-cuda12.AppImage) |
| | AMD Radeon série RX 9000 ou Radeon AI PRO R9700 | [`Strata-0.2.1-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-rx9070.AppImage) |
| | AMD Radeon série RX 7000 | [`Strata-0.2.1-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-rx7900.AppImage) |
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.1-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-strixhalo.AppImage) |
| **Mac** (Apple Silicon, macOS 15 ou mais novo) | integrada | [`Strata-0.2.1-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-mac.dmg) |
**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).
Depois:
- **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.
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.
### Opção B: o Terminal (uma linha)
O Terminal é o comando `strata`, para scripts, servidores e agentes de IA.
Linux e Mac:
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
```
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:
```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 verifica sua placa de vídeo, RAM e disco e escolhe o modelo que cabe. Depois instala, inicia e explica como
conectar seus apps. Ferramentas de IA também podem instalar, iniciar e parar o Strata pelo
[servidor MCP](docs/MCP_SERVER.md) dele.
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.
### Ou faça você mesmo
Prefere compilar por conta própria? Veja [Compilar a partir do código-fonte](#compilar-a-partir-do-código-fonte).
[Baixe o Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) e descompacte (ou use `git clone`).
**Windows:** clique duas vezes em **`START-HERE.bat`**. **Linux:** rode **`./setup.sh`** na pasta do Strata.
## Modelos
**Mac (Apple Silicon, M1 a M5, macOS 15 ou mais recente):** instale uma vez as Command Line Tools da Apple (`xcode-select --install` no Terminal; o Xcode em si não é necessário). Depois abra o Terminal na pasta do Strata e rode **`./setup.sh`**. Ele compila o motor do Mac no seu Mac (uns 7 minutos), faz as mesmas perguntas, baixa o modelo e o inicia. Da próxima vez, `./setup.sh` inicia na hora. Qual tamanho cabe no seu Mac e qual a velocidade: [docs/MAC.md](docs/MAC.md).
Os passos são os mesmos para NVIDIA e AMD. O instalador encontra sua placa e configura o engine certo para ela.
Ele faz algumas perguntas:
- qual modelo e qual tamanho,
- quanto contexto (quanto texto o modelo guarda na memória),
- se ele deve entender imagens.
Aperte Enter em cada uma para usar a resposta recomendada. Depois ele baixa o modelo (cerca de 70 GB) e o inicia.
Se o download parar, rode de novo: ele continua de onde parou. Seu navegador abre o app do Strata em
`http://127.0.0.1:8080`.
> **Enquanto o modelo inicia, seu PC pode ficar lento ou parar de responder por 1-3 minutos** (mais na primeira vez).
> O Strata carrega 35-55 GB na sua RAM e reserva parte dela para a placa de vídeo. Isso é normal. Espere e não
> feche a janela. A janela mostra o que o Strata está fazendo.
**Da próxima vez**, rode `START-HERE.bat` (ou `./setup.sh`) de novo. Ele inicia na hora e não baixa nada duas
vezes. Feche a janela 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).
## Qual modelo devo escolher?
O instalador recomenda um de acordo com sua RAM. O mesmo modelo vem em vários tamanhos, mais ou menos comprimidos.
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.
| Sua RAM | Escolha | Por quê |
### 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 17 a 21% mais devagar que o Q2_0 numa placa de 12 GB e numa R9700. |
O Gyro roda em placas NVIDIA e AMD (AMD é experimental), e não roda em Intel Arc nem no Mac. 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** | **Coder** | cabe em 32 GB e é feito para código (com uma placa de 24 GB, Q2_0 e IQ2_XS também rodam) |
| **48 GB** | **IQ2_XS** (ou Q2_0, o mais rápido) | os tamanhos maiores não cabem |
| **64 GB** | **IQ2_XS** (recomendado), ou IQ3_XXS / IQ3_S | todos os tamanhos cabem; IQ3_S é o melhor e o mais lento |
| **96 GB ou mais** | **IQ3_S**, ou o UD-IQ4_XS da Unsloth (~4-bit) | espaço para os maiores tamanhos com todo o resto aberto |
| **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 17 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 |
- **[Coder](docs/MODELS.md#coder):** uma versão para programação com metade dos experts removida. Ela alcança 91%
da pontuação do modelo completo no SWE-bench Verified (medido pelos autores) e cabe em 32 GB de RAM. É mais fraca
fora do código, inclusive em chinês e outros textos CJK (#438). Para isso, escolha Q2_0, IQ2_XS ou IQ3_S, que
mantêm todos os experts.
- **[Swift 1.5](docs/MODELS.md#swift-15):** um fine-tune que pensa por bem menos tempo antes de responder. Você
recebe a resposta mais cedo, com quase a mesma qualidade.
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** a versão de ~4 bits da Unsloth, entre IQ3_S e
UD-Q4_K_XL em qualidade. Um download de 94 GB. Com menos de ~80 GB de RAM, o Strata lê uma parte dele do SSD
enquanto responde, então ali ele fica mais lento (um SSD NVMe ajuda).
- **[Unsloth UD-Q4_K_XL, UD-Q5_K_XL, UD-Q6_K_XL e Q8_0](docs/MODELS.md#unsloths-files-ud-iq4_xs-to-q8_0):** de 4 a 8 bits, mais próximos do modelo
completo (93-95% o mesmo próximo token que o Q8_0). Rápidos quando todos os especialistas cabem na RAM (~88 GB para
o UD-Q4_K_XL até ~136 GB para o Q8_0); com menos RAM, o Strata lê uma parte do SSD enquanto responde, o que é bem
mais lento (7-8.5 tokens/s para o UD-Q4_K_XL num PC com 64 GB).
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** você configura à mão. Ele
não está no menu do instalador.
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.
Tamanhos, downloads e o que cabe onde: [docs/MODELS.md](docs/MODELS.md). Para adicionar outro modelo depois, rode
`SETUP.bat` (Linux: `./setup.sh --setup`).
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).
## 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
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="A aba Monitor do app Strata ao lado de um agente de programação"><br>
<sub>O <b>Monitor</b> do app Strata (à esquerda) enquanto um agente de programação escreve o jardim com pagode do vídeo (à direita)</sub></p>
### No app
- **No navegador:** abra `http://127.0.0.1:8080`. Lá tem o **Chat**, um **Monitor** ao vivo do modelo e da sua
GPU/CPU/RAM, e o **About** com as configurações e os endereços.
- **Seus apps e agentes de programação:** adicione um provedor "OpenAI-compatible" com a URL base
**`http://127.0.0.1:8080/v1`**. Qualquer chave de API e qualquer nome de modelo funcionam.
- Apps que usam a API da Anthropic: `http://127.0.0.1:8080/v1/messages` (Claude Code:
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`).
- Codex CLI e outros apps que usam a OpenAI Responses API: `/v1/responses`
([configuração](docs/DETAILS.md#the-responses-api-and-codex-cli)).
- **Raciocínio:** escolha **off, low, medium ou high** no menu do chat ou no "reasoning effort" do seu app. Off é o
mais rápido. High é o melhor para perguntas difíceis.
- **Imagens:** responda sim para "Images?" na instalação. Depois clique em **Picture** no chat, ou anexe imagens no
seu app. Placas AMD entendem imagens no Linux usando o processador; no Windows, ainda não.
- **Do seu celular ou de outro PC:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`. Sempre defina uma chave.
- **Uma requisição por vez:** por padrão, o Strata responde uma requisição e as outras esperam. Para responder
várias ao mesmo tempo, defina `"parallel": 2` ([BATCHING.md](docs/BATCHING.md)). Numa placa de 12 GB, isso deixa
cada resposta mais lenta.
- **Prompts longos:** o Strata lê a primeira mensagem de uma conversa inteira, cerca de 1 minuto a cada 30,000
tokens. As mensagens seguintes começam em segundos.
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.
Mais: [onde suas conversas ficam salvas](docs/INSTALL.md#where-things-are-stored), [a API](docs/DETAILS.md#using-it).
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.
### Opcional: mais velocidade com uma placa quando o modelo inteiro cabe na RAM
Para rodar dois modelos, ative **Executar vários modelos** e dê a cada modelo a sua própria placa da mesma marca.
Duas configurações opcionais (de RubenTorney, #1904 e #1905). Elas ajudam quando você usa uma placa de vídeo e sua RAM guarda todos os experts (o modo arena do Strata, por exemplo Q2_0 ou IQ3_XXS num PC com 64 GB). Abra sua configuração `strata-<model>.json` na pasta de dados do Strata ([onde](docs/INSTALL.md#where-things-are-stored)) e adicione:
<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>
- **Geração mais rápida:** `"--adapt-async", "1"` na 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-5% de geração, +7-10% em código e documentos longos. As respostas podem diferir um pouco do padrão (ficam iguais de uma execução para outra).
- **Leitura do prompt mais rápida no Windows:** `"STRATA_PIN_BY_NEED": "1"` no bloco `"env"`. O Windows só consegue travar cerca de metade da RAM para a placa; isto trava primeiro os experts de que a placa realmente precisa. Medido na RTX 5070: +4,4% na leitura do prompt.
### 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 (`START-HERE.bat` ou `./setup.sh`) depois da mudança. Para desfazer, remova as duas entradas.
Reinicie o Strata depois da alteração. Para desfazer, remova as duas entradas.
## Algo deu errado?
- **Meu PC travou na primeira vez que o Strata iniciou.** Isso é normal enquanto ele carrega o modelo. 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.** Rode `START-HERE.bat` (ou `./setup.sh`) de novo. Ele continua de
onde parou.
- **Está muito lento e a luz do disco não para de piscar, ou aparece "the engine stopped unexpectedly".** Seu PC
não tem RAM livre suficiente. Feche outros programas (navegadores usam muita), ou escolha um tamanho menor (Q2_0
ou IQ2_XS).
- **Diz que a porta 8080 já está em uso.** O Strata já está rodando. Procure a janela dele.
Mais problemas e como resolver: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Ainda sem solução? Abra uma
[issue](https://github.com/Niko1221/Strata/issues) e anexe o `strata-<model>.log` da pasta do Strata. Encontrou um
problema de segurança? Informe em particular: [SECURITY.md](SECURITY.md).
</details>
## Como funciona?
Modelos como este normalmente rodam em servidores com centenas de gigabytes de memória de vídeo. Sua placa de vídeo
tem 12-24 GB. O Strata faz o modelo caber **dividindo o trabalho pelo PC inteiro**. Pense numa cozinha: o que você
usa o tempo todo fica na bancada, e o resto espera na despensa.
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.
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="Os 24,576 experts do modelo: os mais usados na placa de vídeo, todos na RAM, uma tabela de consulta no SSD"></p>
- 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.
- **O modelo é uma equipe de 24,576 pequenos especialistas ("experts").** Cada palavra precisa de só 10 deles.
- **Sua placa de vídeo** guarda os poucos milhares de experts usados com mais frequência. **Sua RAM** guarda todos
eles, e **seu processador** trabalha no resto ao mesmo tempo. **Seu SSD** guarda uma grande tabela de consulta.
<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>
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="Um pequeno ajudante adivinha as próximas palavras; o modelo grande confere todas de uma vez e fica com as certas"></p>
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.
- **Adivinhar e conferir:** um pequeno ajudante adivinha as próximas palavras. O modelo grande confere todas de
uma vez. Você recebe a mesma resposta, 1.6-1.8x mais cedo.
- **Textos longos são lidos em grandes pedaços** (até 8,192 tokens por vez), a mais de 1,000 tokens por segundo.
<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>
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).
## Algo deu errado?
- **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.
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).
## Créditos e licenç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)
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 [MIT License](LICENSE).
Algumas partes e todos os modelos têm licenças próprias ([quais](docs/HOW_IT_WORKS.md#license)).
[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)).
## 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.
## Apoie o Strata
O Strata é gratuito e de código aberto. Se ele é útil para você, você pode apoiar o desenvolvimento:
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.
<p align="center"><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></p>
<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>
Quer patrocinar o Strata? Fale comigo pelo [GitHub](https://github.com/Niko1221).
+318 -154
View File
@@ -1,217 +1,381 @@
<h1 align="center">Strata</h1>
<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">
</picture>
</p>
[English](README.md) · **简体中文** · [日本語](README.ja.md) · [Deutsch](README.de.md) · [Français](README.fr.md) · [Español](README.es.md) · [Português](README.pt-BR.md)
<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>
<p align="center"><b>在你自己的游戏电脑上运行 1250 亿参数的 AI 模型</b><br>
NVIDIA 或 AMD 显卡(12 GB 及以上)· Windows 或 Linux · 免费开源</p>
<h3 align="center">在你自己的游戏电脑上运行最先进的模型</h3>
<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="Strata 的模型写出的体素宝塔花园,在浏览器中运行"></a><br>
<sub>体素宝塔花园,一次提示生成,在 RTX 5070 上用 Strata 运行(IQ3_S,128K 上下文)·
<p align="center">支持 NVIDIA、AMD、Intel · Windows、Linux、Mac · 免费开源</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>
</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>
Strata 能在普通电脑上运行 **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**。这是一个又大又聪明的
AI 模型,通常要用服务器才能跑。它能聊天、写代码、看图片,还能配合你的应用和编程智能体一起工作。所有数据都留在你的电脑上。
Strata 运行 **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**。这是一个 1250 亿参数的模型,
通常需要服务器才能跑,现在用普通电脑就行。它能聊天、写代码、看图片,也能配合你的应用和编程智能体使用。
你的聊天记录和文件都留在你自己的电脑上。
## 速度有多快?
**第一次用?** 先看[硬件要求](#硬件要求),再[安装应用](#方式-astrata-应用)。
我们在两台普通的游戏电脑上做了测试。一个 token 大约相当于 ¾ 个英文单词。
- **游戏电脑就能跑。** 内存和显存加起来有 32 GB 就可以开始。
- **能接入你的工具。** 支持 OpenAI 和 Anthropic 的 API,Claude Code、Codex CLI、OpenCode 等智能体都能连上。
- **能看图片。** 默认开启。
- **免费。** MIT 许可证,不用注册账号,不走云端。
- **隐私。** 没有遥测。Strata 下载什么、什么时候联网、如何校验下载内容,都写在 [docs/SAFETY.md](docs/SAFETY.md)。
- **写回答:** 短对话中回复出现的速度。每秒 60 个 token 已经比你的阅读速度还快。
- **读提示:** 读入你发送内容的速度(这里是一份 32K token 的文档、代码或聊天记录)。
版本 0.2.1 · 引擎 0.1.46
<table>
<tr><th>NVIDIA:RTX 5070(12 GB)、Ryzen 5 7600、64 GB 内存</th><th>AMD:RX 9070 XT(16 GB)、Ryzen 9 3900X、47 GB 内存</th></tr>
<tr><td>
## 硬件要求
| 规格 | 写回答 | 读提示 |
| --- | ---: | ---: |
| **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 |
**内存和显存(VRAM)加起来至少要有 32 GB。** Mac 和 Ryzen AI Max 上指的是统一内存。内存越大,能跑的模型越大,
或者速度越快。
</td><td>
- 32 GB 内存加一张 12-16 GB 显存的显卡,可以跑 Coder。
- 64 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 |
</td></tr>
</table>
NVIDIA:Q2_0 用的是引擎 0.1.36,其他各行用的是 0.1.26(4K 回答,32K 提示)。完整表格见
[DETAILS.md](docs/DETAILS.md#speed-measured)。显存越大的显卡越快:RTX 3090(24 GB)写回答应该能达到每秒约
100-140 个 token。长对话和其他显卡的数据:[各模型的速度](docs/MODELS.md#how-fast-is-each-size)、
[社区测试结果](docs/COMMUNITY_BENCHMARKS.md)。
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="请我喝杯咖啡" height="50"></a><br>
<sub>Strata 是免费的。如果它在你的电脑上跑得不错,请我喝杯咖啡,让这个项目继续下去。</sub></p>
## 你需要什么
| | |
| 你的电脑 | 说明 |
| --- | --- |
| **显卡** | **NVIDIA** GeForce RTX 20、30、40 或 50 系列,或 **AMD** Radeon RX 7900 XT / XTX、RX 7800 XT / 7700 XT、RX 9060 XT、RX 9070 / 9070 XT、Radeon AI PRO R9700 或 RX 6800 / 6900 系列。需要 **12 GB 或以上显存**。 |
| **内存** | 32 GB 或以上。内存大小决定能装下[哪个模型](#该选哪个模型)。64 GB 可以运行所有规格。 |
| **硬盘** | 约 80 GB 可用空间。尽量用 SSD:第一次启动会快很多。 |
| **系统** | Windows 10 / 11 或 Linux,以及 NVIDIA 或 AMD 的最新显卡驱动。 |
| **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) |
其他的都由安装程序搞定。两到三张显卡可以分担同一个模型([多 GPU](docs/MULTI_GPU.md))。
另外还需要:
实验性支持,由社区成员在自己的机器上编写和测试:
- 约 80 GB 的可用磁盘空间(用固态硬盘,首次启动会快很多)
- Windows 10 / 11、Linux 或 macOS
- 较新的显卡驱动
- **较老的显卡**(Tesla P40 / V100、GTX 10、Radeon VII / MI50、RX 6700 XT、RX 5500 XT):[较老的 GPU](docs/OLDER_GPUS.md)。
- **Intel Arc**,在 Linux 上从源码构建:[Intel Arc](docs/INTEL_ARC.md)。
- **AMD Ryzen AI Max(Strix Halo)**,在 Linux 上从源码构建:[Strix Halo](docs/STRIX_HALO.md)。
- **不支持 AVX2 的老处理器**:能用,但很慢。[较老的 CPU](docs/INSTALL.md#older-cpus-experimental)。
完整列表:[docs/INSTALL.md](docs/INSTALL.md#what-you-need)。
同一品牌的两三张显卡可以共同运行一个模型([多显卡](docs/MULTI_GPU.md))。Strata 不会把一个模型拆到 NVIDIA 和 AMD
显卡上。完整列表见 [docs/INSTALL.md](docs/INSTALL.md#what-you-need)。
## 安装
### 让你的 AI 帮你装
三种方式任选一种。它们启动的是同一个引擎,用的是同样的模型。
你在用 AI 编程助手吗(Claude Code、Cursor、Codex、GitHub Copilot 等)?把下面这段粘贴给它:
### 方式 A:Strata 应用
最简单的方式。应用内置引擎:安装、选模型、开始聊天。请根据你的系统和显卡,从
[发布页](https://github.com/Niko1221/Strata/releases/latest)下载对应的文件。不确定选哪个?看表格下面那一段。
| 系统 | 你的显卡 | 要下载的文件 |
| --- | --- | --- |
| **Windows 10 / 11** | NVIDIA RTX,驱动 580 或更新 | [`Strata-0.2.1-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-cuda13.exe) |
| | NVIDIA,较旧的显卡,或驱动低于 580 | [`Strata-0.2.1-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-cuda12.exe) |
| | AMD Radeon RX 9000 系列或 Radeon AI PRO R9700 | [`Strata-0.2.1-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-hip.exe) |
| | AMD Radeon RX 7000 系列或 Ryzen AI Max (Strix Halo) | [`Strata-0.2.1-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-hip-rdna3.exe) |
| **Linux** | NVIDIA RTX,驱动 580 或更新 | [`Strata-0.2.1-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-nvidia-cuda13.AppImage) |
| | NVIDIA,较旧的显卡,或驱动低于 580 | [`Strata-0.2.1-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-nvidia-cuda12.AppImage) |
| | AMD Radeon RX 9000 系列或 Radeon AI PRO R9700 | [`Strata-0.2.1-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-rx9070.AppImage) |
| | AMD Radeon RX 7000 系列 | [`Strata-0.2.1-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-rx7900.AppImage) |
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.1-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-strixhalo.AppImage) |
| **Mac**(Apple 芯片,macOS 15 及以上) | 内置 | [`Strata-0.2.1-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-mac.dmg) |
**不确定时:** NVIDIA 显卡的驱动版本在 580 及以上就选 `cuda13`,否则选 `cuda12`。`cuda12` 适用于较旧的显卡和驱动。
`hip` 和 `amd` 的文件是给 AMD 显卡用的。其他 AMD 显卡(RX 6000)目前还没有现成的应用,
请看 [AMD 显卡](docs/AMD_HIP.md)。
然后:
- **Windows:** 运行 `.exe`。它只为当前用户安装,不需要管理员权限。如果 Windows 提示“Windows 已保护你的电脑”,
点击**更多信息**,再点**仍要运行**(安装程序还没有代码签名)。
- **Linux:** 给文件加上可执行权限(`chmod +x`),然后启动。需要 FUSE 2:`sudo apt install libfuse2`(Ubuntu 22.04),
或 `libfuse2t64`(Ubuntu 24.04 及更新)。
- **Mac:** 打开 `.dmg`,把 Strata 拖到“应用程序”。
第一个界面里,应用会检测你的电脑,并推荐合适的模型。点击安装即可。模型很大,要下载约 40-70 GB,中断后会从断点继续。
关闭窗口会同时停止 Strata 和引擎。
### 方式 B:终端(一行命令)
终端版就是 `strata` 命令,适合脚本、服务器和 AI 智能体。
Linux 和 Mac:
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
```
Windows(PowerShell):
```powershell
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
```
脚本会检查你的电脑,下载终端版,校验 SHA-256,然后解压到你的用户目录。不需要管理员权限。在 Windows 上,装完后请
打开一个新的终端窗口。然后启动 Strata:
```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。
它会检查你的显卡、内存和磁盘,选出合适的模型,完成安装和启动,并告诉你怎么连接你的应用。AI 工具也可以通过 Strata 的
[MCP 服务器](docs/MCP_SERVER.md)安装、启动和停止 Strata。
### 或者自己动手
想自己动手编译?请看[从源码构建](#从源码构建)。
[下载 Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) 并解压(或者用 `git clone`)。
**Windows:** 双击 **`START-HERE.bat`**。**Linux:** 在 Strata 文件夹里运行 **`./setup.sh`**。
## 模型
**Mac(Apple 芯片,M1 到 M5,macOS 15 或更新版本):** 先安装一次 Apple 的 Command Line Tools(在终端里运行 `xcode-select --install`;不需要 Xcode 本身)。然后在 Strata 文件夹里打开终端,运行 **`./setup.sh`**。它会在你的 Mac 上编译 Mac 版引擎(大约 7 分钟),问同样的问题,下载模型并启动。下次运行 `./setup.sh` 会马上启动。哪个大小适合你的 Mac、速度如何:[docs/MAC.md](docs/MAC.md)。
应用和 `strata launch` 会按你的内存推荐模型。它是同一个模型的几种尺寸,压缩程度不同。尺寸小的更快,尺寸大的稍微更聪明一点。
NVIDIA 和 AMD 的步骤完全一样。安装程序会识别你的显卡,并装好对应的引擎。它会问你几个问题:
### 内存不够?试试 Gyro
- 用哪个模型、哪个规格,
- 上下文多大(模型能记住多少文字),
- 是否要识别图片。
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF)(作者 agentionai)是运行这个模型占用最小的方式。
每次直接按回车就是推荐选项。之后它会下载模型(约 70 GB)并启动。如果下载中断了,再运行一次即可:它会从中断的地方继续。
浏览器会打开 Strata 应用,地址是 `http://127.0.0.1:8080`。
| 尺寸 | 所需内存(RAM + VRAM) | 下载大小 | 说明 |
| --- | ---: | ---: | --- |
| **Gyro-S** | 27.6 GB | 58.5 GB | 给其他尺寸都放不下的电脑。质量低于 IQ2_XS。 |
| **Gyro-M** | 35 GB | 92.0 GB | 质量约相当于 IQ3_XXS,显存占用少约 20%。在 12 GB 显卡和 R9700 上,输出回答比 Q2_0 慢 17-21%。 |
> **模型启动时,你的电脑可能会变慢或卡住 1-3 分钟**(第一次最久)。
> Strata 会把 35-55 GB 加载到内存,并为显卡锁定其中一部分。这是正常的。请耐心等待,不要关闭窗口。
> 窗口里会显示 Strata 正在做什么。
Gyro 可在 NVIDIA 和 AMD 显卡上运行(AMD 为实验性支持),不支持 Intel Arc 和 Mac。详情和测试数据:
[docs/GYRO.md](docs/GYRO.md)。用 `strata launch --model gyro-s` 启动。
**下次使用时**,再运行一次 `START-HERE.bat`(或 `./setup.sh`)。它会马上启动,已下载的东西不会重复下载。关闭它的窗口就能停止模型。
`UPDATE.bat`(`./update.sh`)只更新 Strata,不启动。更新、Docker、多张显卡、文件存放位置以及所有选项:
[docs/INSTALL.md](docs/INSTALL.md)。
### 其他尺寸
## 该选哪个模型
安装程序会根据你的内存推荐一个。同一个模型有几种规格,压缩程度不同。规格越小越快,越大越聪明一些。
| 你的内存 | 选择 | 原因 |
| 你的内存(RAM + VRAM) | 选择 | 备注 |
| --- | --- | --- |
| **32 GB** | **Coder** | 32 GB 装得下,而且专为代码打造(如果显卡是 24 GB,Q2_0 和 IQ2_XS 也能跑) |
| **48 GB** | **IQ2_XS**(或 Q2_0,最快) | 更大的规格装不下 |
| **64 GB** | **IQ2_XS**(推荐),或 IQ3_XXS / IQ3_S | 所有规格都装得下;IQ3_S 最好,也最慢 |
| **96 GB 或以上** | **IQ3_S**,或 Unsloth 的 UD-IQ4_XS(约 4-bit) | 开着其他程序也能放下最大的规格 |
| **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 慢 17-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 会从固态硬盘读取一部分,速度会慢很多 |
- **[Coder](docs/MODELS.md#coder):** 编程版本,去掉了一半专家。它达到完整模型 SWE-bench Verified 分数的 91%(由其作者测得),
32 GB 内存就能装下。代码以外的能力较弱,包括中文和其他中日韩文本(#438)。这类用途请选 Q2_0、IQ2_XS 或 IQ3_S,
它们保留了所有专家。
- **[Swift 1.5](docs/MODELS.md#swift-15):** 一个微调版本,回答前思考的时间短得多。你能更早拿到答案,质量基本不变。
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** Unsloth 的约 4-bit 版本,质量介于 IQ3_S 和 UD-Q4_K_XL 之间。
下载 94 GB。内存少于约 80 GB 时,Strata 回答时要从 SSD 读取其中一部分,所以会更慢(NVMe SSD 有帮助)。
- **[Unsloth UD-Q4_K_XL、UD-Q5_K_XL、UD-Q6_K_XL 和 Q8_0](docs/MODELS.md#unsloths-files-ud-iq4_xs-to-q8_0):** 4 到 8 位,更接近完整模型(下一个 token
与 Q8_0 相同的比例为 93-95%)。所有专家都能放进内存时速度很快(UD-Q4_K_XL 约需 88 GB,Q8_0 约需 136 GB);内存不够时,
Strata 回答时要从 SSD 读取一部分,会慢很多(64 GB 的电脑上 UD-Q4_K_XL 每秒 7-8.5 个 token)。
- **[OrcaRouter 的 Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** 需要手动设置,不在安装程序的菜单里。
内存比这一行写的少,也可能能用:Strata 会自动切换到[低内存模式](docs/MODELS.md#a-big-graphics-card-and-little-ram)。
速度会变慢,显卡较小时会慢得多。
规格、下载以及各配置能装下什么:[docs/MODELS.md](docs/MODELS.md)。以后想再添加模型,运行
`SETUP.bat`(Linux:`./setup.sh --setup`)。
更多选择:
- **[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)。
## 速度如何?
以下数据基于 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)。
## 使用方法
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="Strata 应用的 Monitor 标签页,旁边是一个编程智能体"><br>
<sub>Strata 应用的 <b>Monitor</b>(左),编程智能体正在写视频里的宝塔花园(右)</sub></p>
### 在应用里
- **在浏览器里:** 打开 `http://127.0.0.1:8080`。里面有 **Chat**(聊天)、实时 **Monitor**(监控模型和你的
GPU/CPU/内存),以及 **About**(设置和地址)。
- **你的应用和编程智能体:** 添加一个“OpenAI 兼容”的提供商,base URL 填 **`http://127.0.0.1:8080/v1`**。
API key 和模型名随便填都行。
- 使用 Anthropic API 的应用:`http://127.0.0.1:8080/v1/messages`(Claude Code:
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`)。
- Codex CLI 和其他使用 OpenAI Responses API 的应用:`/v1/responses`
([设置方法](docs/DETAILS.md#the-responses-api-and-codex-cli))。
- **思考:** 在聊天菜单或应用的“reasoning effort”里选择 **off、low、medium 或 high**。off 最快,
high 最适合难题。
- **图片:** 在安装时对“Images?”选是。然后在聊天里点 **Picture**,或在你的应用里附上图片。
AMD 显卡在 Linux 上通过处理器识别图片;在 Windows 上暂时还不行。
- **从手机或另一台电脑访问:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`。一定要设置 key。
- **一次处理一个请求:** 默认情况下 Strata 一次只回答一个请求,其他请求排队等待。想同时回答多个,
设置 `"parallel": 2`([BATCHING.md](docs/BATCHING.md))。在 12 GB 显卡上,这会让每个回答变慢。
- **长提示:** Strata 会完整读入对话的第一条消息,大约每 30,000 个 token 需要 1 分钟。之后的消息几秒内就开始回答。
应用包含**聊天**、显示模型以及 GPU、CPU 和内存状态的**监控**、**模型**页面和**设置**。聊天可以在你电脑上的某个文件夹里工作。
在**手动**模式下,修改文件或运行命令之前它会先询问你。输入 `/loop 10m <要做的事>`,在聊天保持打开期间每 10 分钟重复发送一次消息。
更多:[聊天记录存在哪里](docs/INSTALL.md#where-things-are-stored)、[API](docs/DETAILS.md#using-it)。
聊天可以用 DuckDuckGo(或 Bing)搜索网页,不需要账号。你也可以在**设置,工具**里添加搜索密钥。搜索是唯一会把内容发出电脑的功能,
发出的就是搜索词。
### 可选:整个模型都能放进内存时,单卡再提速
要同时运行两个模型,请打开**运行多个模型**,并为每个模型分配同一品牌的单独一张显卡。
两个可选设置(来自 RubenTorney 的 #1904 和 #1905)。适用于只用一张显卡、并且内存能放下所有专家的情况(Strata 的 arena 模式,例如 64 GB 电脑上的 Q2_0 或 IQ3_XXS)。打开 Strata 数据文件夹里的配置文件 `strata-<model>.json`([在哪里](docs/INSTALL.md#where-things-are-stored)),加入:
<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>
- **生成更快:** 在 `"args"` 列表里加 `"--adapt-async", "1"`。专家缓存改在辅助线程里更新,不再暂停回答。在 RTX 5070(Windows,Q2_0)上实测:生成 +4-5%,代码和长文档 +7-10%。回答可能与默认设置略有不同(但每次运行都一样)。
- **Windows 上读取提示更快:** 在 `"env"` 块里加 `"STRATA_PIN_BY_NEED": "1"`。Windows 只能为显卡锁定大约一半的内存;这个设置会先锁定显卡真正需要的专家。在 RTX 5070 上实测:读取提示 +4.4%。
### 在终端里
<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(`START-HERE.bat` 或 `./setup.sh`)。要撤销,删掉这两项即可。
修改后请重启 Strata。要撤销,删除这两项即可。
</details>
## 工作原理
这类模型通常运行在有几百 GB 显存的服务器上。你的显卡只有 12-24 GB。Strata 把工作分给整台电脑,让模型装得下。
可以把它想成一座图书馆。
- 模型是一个由 24,576 位小专家(“experts”)组成的团队,好比 24,576 本参考书。每个词只需要其中 10 本。
- **你的显卡是书桌。** 它小而快。最常用的大约 4,000 本书摊开放在上面。
- **你的内存是阅览室的书架。** 24,576 本书都在这里。某个词需要一本不在书桌上的书时,处理器会立刻从书架上取来读,显卡则继续工作。
- **你的固态硬盘是档案室。** 里面存着一张 29 GB 的查找表。模型每个词只读其中几行,所以不用常驻内存。
<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>
另外有一个小助手会猜接下来的几个词。大模型一次检查所有猜测,保留正确的部分。得到的答案不变,但快了 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>
更详细的解释:[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md)。每个部分和它的数据:
[详细说明](docs/DETAILS.md#how-it-works)和[论文](docs/paper/Strata-Paper.pdf)。
## 出问题了?
- **Strata 第一次启动时电脑卡死了。** 加载模型时这是正常的。请耐心等待,不要关闭窗口。
10 分钟后还卡着?重启电脑,关掉其他程序再试一次,或者换一个更小的规格。
- **下载或安装时中断了。** 再运行一次 `START-HERE.bat`(或 `./setup.sh`)。它会从中断的地方继续。
- **非常慢,硬盘灯一直闪,或者提示“the engine stopped unexpectedly”。** 你的电脑可用内存不够。
关掉其他程序(浏览器很占内存),或者换一个更小的规格(Q2_0 或 IQ2_XS)。
- **第一次启动 Strata 时我的电脑卡住了。** 它在把模型加载到内存。请等待,不要关闭窗口。10 分钟后还是卡住?重启电脑,
关闭其他程序后再试,或者选一个更小的尺寸。
- **下载或安装时中断了。** 重新启动应用或 `strata launch`,或者再运行一次 `START-HERE.bat`(或 `./setup.sh`)。
它会从中断的地方继续。
- **非常慢、硬盘灯一直闪,或者提示“引擎意外停止”。** 你的电脑可用内存不够。关闭其他程序(浏览器很占内存),
或者选一个更小的尺寸,比如 Q2_0 或 IQ2_XS。
- **提示这台电脑没有可用的引擎。** 在应用里打开**设置,引擎**,点击**更新引擎**。在终端里运行 `strata update`。
`strata doctor` 会显示已安装哪些引擎文件。
- **提示 8080 端口已被占用。** Strata 已经在运行了。找一下它的窗口。
更多问题和解决办法:[docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)。还是解决不了?提交一个
[issue](https://github.com/Niko1221/Strata/issues),并附上 Strata 文件夹里的 `strata-<model>.log`。发现了安全问题?
请私下报告:[SECURITY.md](SECURITY.md)。
## 它是怎么工作的?
这类模型通常运行在拥有数百 GB 显存的服务器上。而你的显卡只有 12-24 GB。Strata 让模型装得下的办法是
**把工作分摊到整台电脑上**。可以想象一个厨房:常用的东西放在台面上,其余的放在储藏室里。
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="模型的 24,576 个专家:最常用的在显卡上,全部在内存里,一张查找表在 SSD 上"></p>
- **这个模型是由 24,576 个小专家(“experts”)组成的团队。** 每个词只需要其中 10 个。
- **你的显卡** 存放最常用的几千个专家。**你的内存** 存放全部专家,
**你的处理器** 同时处理其余的部分。**你的 SSD** 存放一张大查找表。
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="一个小助手猜测接下来的几个词;大模型一次性全部检查,保留正确的"></p>
- **先猜,再检查:** 一个小助手先猜接下来的几个词,大模型再一次性检查它们。答案完全一样,但快 1.6-1.8 倍。
- **长文本分大块读入**(每次最多 8,192 个 token),速度超过每秒 1,000 个 token。
更详细的解释:[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md)。每个部分及其数据:
[详细说明](docs/DETAILS.md#how-it-works)和[论文](docs/paper/Strata-Paper.pdf)。
更多问题和解决办法:[docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)。还是解决不了?请提交
[issue](https://github.com/Niko1221/Strata/issues)。在应用里,附上**设置,引擎,诊断,查看日志,保存日志**得到的文件。
在终端里,粘贴 `strata doctor` 的输出。从源码运行时,附上 Strata 文件夹里的 `strata-<model>.log`。
如果是安全问题,请私下报告:[SECURITY.md](SECURITY.md)。
## 致谢与许可证
模型是 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)
和 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))。
模型是 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)。其中说明了需要附上哪些内容,
方便其他人核实你的工作。
## 支持 Strata
Strata 免费且开源。如果它对你有用,欢迎支持它的开发:
Strata 免费,采用 MIT 许可证。我在能拿到的各种硬件上开发和测试它。如果它在你的电脑上跑得不错,请我喝杯咖啡,能帮我继续测试新显卡、
修复你们反馈的问题。不用有压力。使用它并反馈你的发现,同样很有帮助。
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="请我喝杯咖啡" height="50"></a></p>
<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) 联系我。
+49
View File
@@ -43,4 +43,53 @@ this PC only. The details and every setting are in [docs/DETAILS.md](docs/DETAIL
last few prompts, whatever `"api_monitor"` says, to encode the next turn faster; no endpoint shows them, and they
are gone when the server stops.
## The desktop app and the Terminal
Strata comes in three parts that share one engine: the **desktop app**, the **Terminal** (`strata`) and the **engine** that runs
the model. The section above is about the engine's HTTP server. This is what the other two add.
- **The app's host listens on this PC only.** The app serves its page from a local host process on `127.0.0.1` (port 8791 unless
changed). A request to its `/api/` endpoints that carries a browser `Origin` from any other site gets 403. The app shell holds a
random secret (32 characters or more, compared in constant time) that is needed to quit the host or tell it which window is open,
and the host gives its own helper processes a second random secret. By default the models the app starts are served on this PC only.
- **Sharing is a separate, key-protected listener.** "Share with other devices" (and the Cloudflare tunnel) will not start until an
API key is set. It opens a different listener that answers only `/v1/models`, `/v1/chat/completions` and `/v1/messages`, never the
app's page or its host endpoints.
- **App and Terminal talk through an owner channel.** One owner process per installation starts and ends the engine, and a second
client (the Terminal) attaches to it. On macOS and Linux that is a private Unix socket in a folder only you can open. On Windows it
is a TCP listener on `127.0.0.1` that answers only requests carrying a 256-bit token kept in an owner-only folder. That is a weaker
guarantee than a socket other users cannot open: on Windows the token is what protects it, as with any loopback service that has a
token (`strata_control/owner_transport.py`). The bridge between the app shell and the host (used for the app update) is also loopback and protected by a token.
- **Starting a model and serving never download anything.** Downloads and updates are one explicit step: `strata update`, or "Update
engine" and "Update Strata" in the app's Settings. Models and saved tuning are never replaced by an update.
- **Updates come from a signed feed.** The release publishes `strata-feed.json` and `strata-feed.json.sig` (Ed25519). An installed
Strata checks the signature against the public key it carries (`strata_control/update_keys.json`), refuses a feed older than the last
one it accepted (a replayed old feed) or expired, and checks every engine package against its SHA-256, its signed manifest and the
card in your PC before it test-starts it and switches to it. Any failure keeps the engine you had. The app installer itself is signed
with a separate key (Tauri's minisign key) that the app and the Terminal both check. The private keys are kept offline by the owner.
See [packaging/UPDATES.md](packaging/UPDATES.md).
### How far to trust `curl | sh` and `irm | iex`
The Terminal installs with one line:
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
```
Piping a script into a shell runs whatever the server sends, so it deserves the same care as running any downloaded program.
What the script does is short and written at its top: it fetches the Terminal archive named in the release's `install-manifest.txt`,
checks its SHA-256, unpacks it into your home folder (no administrator rights, nothing outside your home), checks the release's signed feed
with the Terminal it just unpacked and removes it again if that fails, and puts `strata` on your PATH.
Those checks have a limit. The checksum comes from the same release as the archive, and the public key that checks the feed arrives inside
that archive, so for the first install all trust rests on HTTPS and on the `Niko1221/Strata` GitHub release itself. The checks catch a
damaged or half-finished download and a feed that was not signed by the release key; they do not protect you if the release has been replaced
by someone who controls it. Later updates are checked against the key already on your PC, which is a stronger position.
If that is not enough: save the script (`curl -fsSLO https://github.com/Niko1221/Strata/releases/latest/download/install.sh`), read it, and
run it from the file; or install the desktop app or build from source instead and check the code you build.
Strata has not had an outside security audit yet.
What Strata downloads, when it uses the internet, what stays on your PC and how downloads are checked is written up in [docs/SAFETY.md](docs/SAFETY.md).
+13
View File
@@ -2,8 +2,13 @@
rem Strata for Windows: the first run installs everything and starts the model; later runs just start it.
rem Needs only an NVIDIA or AMD graphics driver. Python is installed for your user account if it is missing (no admin needed).
setlocal
rem Python reads and writes text as UTF-8, not in the Windows code page (cp1251, cp866, ...): a folder or user name with Cyrillic or other letters stays intact (#3202).
set "PYTHONUTF8=1"
title Strata
cd /d "%~dp0"
rem A bundled Python runtime (the python\ folder of the terminal archive) is used when it is there: nothing to find,
rem install or download. SETUP.bat and UPDATE.bat run this file, so they use it too.
if exist "python\python.exe" goto bundled
if exist ".venv\Scripts\python.exe" goto run
call :findpy
@@ -36,6 +41,14 @@ echo Could not create the Python environment in .venv
pause
exit /b 1
:bundled
set "PYTHONNOUSERSITE=1"
set "PYTHONHOME="
set "PYTHONPATH="
"python\python.exe" setup.py %*
if errorlevel 1 pause
exit /b
:run
".venv\Scripts\python.exe" setup.py %*
if errorlevel 1 pause
+2 -1
View File
@@ -2,7 +2,8 @@
rem Update Strata without starting the model (#475): the newest code (git pull, when this folder is a git clone), then
rem what START-HERE.bat does before a start - the engine (a new one when this version needs it), the Python packages,
rem each installed model's settings and draft subset. The model files are not touched. Start the model later with
rem START-HERE.bat. Options are passed on to setup.py.
rem START-HERE.bat. Options are passed on to setup.py. The bundled Python (python\ beside this file) is used when
rem there is one: START-HERE.bat, which this file runs, picks it.
setlocal
title Strata - update
cd /d "%~dp0"
+232
View File
@@ -0,0 +1,232 @@
{
"name": "strata-desktop",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "strata-desktop",
"version": "0.1.0",
"devDependencies": {
"@tauri-apps/cli": "2.12.3"
}
},
"node_modules/@tauri-apps/cli": {
"version": "2.12.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.12.3.tgz",
"integrity": "sha512-f7/LsY74SVb1ChmOPw1ajOpxii0GSzDAgmWHRci8JUYuuai04rXsSbFobpmB0sP2DdlvMD/B5ZotEn8GOGMRUg==",
"dev": true,
"license": "Apache-2.0 OR MIT",
"bin": {
"tauri": "tauri.js"
},
"engines": {
"node": ">= 10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/tauri"
},
"optionalDependencies": {
"@tauri-apps/cli-darwin-arm64": "2.12.3",
"@tauri-apps/cli-darwin-x64": "2.12.3",
"@tauri-apps/cli-linux-arm-gnueabihf": "2.12.3",
"@tauri-apps/cli-linux-arm64-gnu": "2.12.3",
"@tauri-apps/cli-linux-arm64-musl": "2.12.3",
"@tauri-apps/cli-linux-riscv64-gnu": "2.12.3",
"@tauri-apps/cli-linux-x64-gnu": "2.12.3",
"@tauri-apps/cli-linux-x64-musl": "2.12.3",
"@tauri-apps/cli-win32-arm64-msvc": "2.12.3",
"@tauri-apps/cli-win32-ia32-msvc": "2.12.3",
"@tauri-apps/cli-win32-x64-msvc": "2.12.3"
}
},
"node_modules/@tauri-apps/cli-darwin-arm64": {
"version": "2.12.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.12.3.tgz",
"integrity": "sha512-xmVNoJOOKzKLj7GLHXmXLimRby9M42aBIJa0sB80N7o24eKV5JiVmToKTRLVH9yhL05GZ/B6rSSOmbzNFJwBAA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-darwin-x64": {
"version": "2.12.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.12.3.tgz",
"integrity": "sha512-URd+0usfmUQQyb5kNPiKt2OT8gp7oGU7QR+qOppB6e35mpVBFFVNhWc9oWb3p0u71fDJ09DpF4T6m9xYxoIB8g==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
"version": "2.12.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.12.3.tgz",
"integrity": "sha512-0kwUVx2LbmOoTRlwF1oDww7th4/NSluv4ZBQ6KRZsFKg4g0TlTcIiJ0VehqkQYUeC9xIT1Mk6XtF52mI155VSQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
"version": "2.12.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.12.3.tgz",
"integrity": "sha512-gKSGDx6fx+HBLQmG6guChFdisbrCSdx9Yli+j5teOa64gm1CeM+C6NLru5Pj3ZkkEep05BmCFjxvX+cjqAycZQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
"version": "2.12.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.12.3.tgz",
"integrity": "sha512-Yr59qMy/gcvAfBuFdCGlAmGiLsYWMFDbsP4bAI6iNBzcJTGrYWZGeiQhGDo29MQwfF/NUBqyM2qljXu5tZ6wjQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
"version": "2.12.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.12.3.tgz",
"integrity": "sha512-jKr0/REBMnLKC0OwPQZkyge0mKDaNZ0b/AbfGwVyCgFfwS730EVLfVDo3nt7MUu6jJaytRq08+xSj5qfy+U1+Q==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
"version": "2.12.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.12.3.tgz",
"integrity": "sha512-gTkEqJAGCMROGanxHnzaJzQ+/rXl39HWVg0/hDODS1wDWBGqoceqGC6Qm1+mQbyT11xnsB8mKu/gyVID/YpFIw==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-x64-musl": {
"version": "2.12.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.12.3.tgz",
"integrity": "sha512-T2dTxL1svRym71fZtaW4pZYXbthbfFsEJr1NeOUg/p7laWXZbUt/8DMSZf8recu9f7+1F/Lv00HtzsEu6vDO9w==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
"version": "2.12.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.12.3.tgz",
"integrity": "sha512-eoiQwpEZWbpsVHcM9BAzqqe4TCHszvr3rsM7pUQ4ofLK0lF1i1d/jX0Qo5DYMqcrUtwQuDdDuOCbcT0Izf1VwQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
"version": "2.12.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.12.3.tgz",
"integrity": "sha512-kE+KUg6Dbbkgvcw0m99wJVIToFQzJqpMBAXah74WqxaLzssNwSTUccrHp6+QVzMhjQP330tvekFmnQF+0NbE6Q==",
"cpu": [
"ia32"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
"version": "2.12.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.12.3.tgz",
"integrity": "sha512-L7wndWAgX2mLW7alswpL3d1R0P9FN2WYBgHaQkAYEBxHHGkY4vFX9CjcRQ1LAxDAMIJxrY7/aDziFJeChS84NQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
}
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"name": "strata-desktop",
"productName": "Strata",
"version": "0.2.1",
"private": true,
"description": "Strata Desktop - local AI, tuned to one model.",
"scripts": {
"build:ui": "npm --prefix ../host run build",
"typecheck:ui": "npm --prefix ../host run typecheck",
"test:ui": "npm --prefix ../host test",
"tauri": "tauri",
"dev": "npm run build:ui && tauri dev",
"build": "npm run build:ui && tauri build",
"build:app": "npm run build:ui && tauri build --bundles app",
"dmg": "npm run build:ui && tauri build --bundles dmg"
},
"devDependencies": {
"@tauri-apps/cli": "2.12.3"
}
}
+361
View File
@@ -0,0 +1,361 @@
#!/usr/bin/env python3
"""The installed Windows app, closed every way, with a stub engine: nothing may be left running.
python apps/desktop/scripts/app-close-matrix.py --install <folder the installer wrote> [--scenarios all] [--report out.json]
For each case it starts the REAL installed `strata-desktop.exe` (its window opens), loads a stub engine through the host's API
(profile + `runtime start` under Control's owner runtime, exactly the app's path), records every process of the tree (the app,
the host, Control, the owner host, the server, the engine), ends the app one way, and asserts:
* no process of the tree is left, the engine's port is free, the runtime lease can be taken again;
* the engine got the clean stop where the way of closing allows one (normal close, window X);
* the next load of the same slot works (a lost owner's state is cleaned, not a blocker).
It also proves a second launch focuses the open window and starts nothing, and that one app runs two engines and closes both.
Everything per-user lives under a scratch folder; the stub engine is a script. No real engine ever runs.
"""
from __future__ import annotations
import argparse
import ctypes
import json
import os
import subprocess
import sys
import tempfile
import time
import urllib.error
import urllib.request
from ctypes import wintypes
from pathlib import Path
ROOT = Path(__file__).resolve().parents[3]
sys.path.insert(0, str(ROOT))
import psutil # noqa: E402
from strata_control import procs, runtime # noqa: E402
from tests.terminal.lifecycle_matrix import install_fake_engine, port_open # noqa: E402
if os.name != "nt":
raise SystemExit("this matrix drives the Windows installer's app")
user32 = ctypes.WinDLL("user32", use_last_error=True)
WM_CLOSE = 0x0010
def main_window(pid: int) -> int | None:
found = []
@ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
def visit(hwnd, _):
owner = wintypes.DWORD()
user32.GetWindowThreadProcessId(hwnd, ctypes.byref(owner))
if owner.value == pid and user32.IsWindowVisible(hwnd):
buf = ctypes.create_unicode_buffer(256)
user32.GetWindowTextW(hwnd, buf, 256)
if buf.value.strip() == "Strata":
found.append(hwnd)
return True
user32.EnumWindows(visit, 0)
return found[0] if found else None
class Scratch:
def __init__(self, root: Path, install: Path):
self.root, self.install = root, install
self.components = root / "components"
self.models = root / "models"
self.mark = root / "mark"
for d in (self.components, self.models, self.mark, root / "local", root / "home", root / "appdata", root / "webview"):
d.mkdir(parents=True, exist_ok=True)
install_fake_engine(self.components)
from tools.strata_tokenizer import BYTE_TO_UNICODE
tok = self.models / "tokenizer"
tok.mkdir()
tokens = [BYTE_TO_UNICODE[i] for i in range(256)] + ["ab", "<|im_start|>", "<|im_end|>", "<|endoftext|>"]
(tok / "vocab.json").write_text(json.dumps({t: i for i, t in enumerate(tokens)}))
(tok / "merges.txt").write_text("a b")
(tok / "token_type.json").write_text(json.dumps([0] * 257 + [3, 3, 3]))
for name in ("stub", "stub2"):
(self.models / f"strata-{name}.json").write_text(json.dumps({
"exe": "component:strata-engine", "backend": "cuda", "tokenizer": str(tok), "args": [], "cwd": str(root),
"model_name": name, "host": "127.0.0.1", "port": 0}))
self.env = {**os.environ, "STRATA_COMPONENTS_DIR": str(self.components), "STRATA_PROFILE_DIR": str(root / "profiles"),
"STRATA_RUNTIME_DIR": str(root / "runtime"), "STRATA_INSTALL_DIR": str(self.models),
"STRATA_MODELS_DIR": str(root / "models-unused"), "STRATA_ENGINE_BACKEND": "cuda",
"STRATA_HOME": str(root / "home"), "APPDATA": str(root / "appdata"), "LOCALAPPDATA": str(root / "local"),
"WEBVIEW2_USER_DATA_FOLDER": str(root / "webview"), "FAKE_ENGINE_MARK": str(self.mark),
"FAKE_ENGINE_SLOW_STEPS": "0", "PYTHONDONTWRITEBYTECODE": "1"}
self.env.pop("STRATA_API_KEY", None)
for key in ("STRATA_COMPONENTS_DIR", "STRATA_PROFILE_DIR", "STRATA_RUNTIME_DIR", "LOCALAPPDATA", "STRATA_ENGINE_BACKEND",
"STRATA_NO_ACQUIRE", "FAKE_ENGINE_MARK"):
os.environ[key] = self.env.get(key, "1") # this process reads the same runtime and records folders
class App:
def __init__(self, scratch: Scratch):
self.s = scratch
self.proc = subprocess.Popen([str(scratch.install / "strata-desktop.exe")], cwd=scratch.install, env=scratch.env,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
self.base = None
def host(self):
for child in psutil.Process(self.proc.pid).children(recursive=True):
if child.name().lower() == "node.exe":
return child
return None
def wait_up(self, seconds=90):
deadline = time.monotonic() + seconds
while time.monotonic() < deadline:
if self.proc.poll() is not None:
raise RuntimeError(f"the app exited with {self.proc.returncode}")
try:
host = self.host()
if host:
for c in host.net_connections(kind="tcp"):
if c.status == "LISTEN" and c.laddr.ip == "127.0.0.1":
base = f"http://127.0.0.1:{c.laddr.port}"
try:
if urllib.request.urlopen(base + "/api/health", timeout=2).status == 200 and main_window(self.proc.pid):
self.base = base
return
except OSError:
pass
except psutil.Error:
pass
time.sleep(0.5)
raise RuntimeError("the app did not come up")
def call(self, path, method="GET", body=None):
req = urllib.request.Request(self.base + path, method=method, data=None if body is None else json.dumps(body).encode(),
headers={"content-type": "application/json"})
try:
with urllib.request.urlopen(req, timeout=120) as r:
return json.loads(r.read() or b"null")
except urllib.error.HTTPError as exc:
raise RuntimeError(f"{method} {path} -> {exc.code} {exc.read()[:300]!r}") from None
def load(self, model, multiple=False):
self.call("/api/native?refresh=1")
started = self.call("/api/native/operations", "POST", {"kind": "load", "args": {"model": model, **({"multiple": True} if multiple else {})}})
deadline = time.monotonic() + 180
while time.monotonic() < deadline:
state = self.call("/api/native")
job = next((j for j in state["jobs"] if j["id"] == started["id"]), None)
if job and job["state"] != "running":
if job["state"] != "done":
raise RuntimeError(f"load {model}: {job['state']} {job.get('error')}")
return state
time.sleep(0.3)
raise RuntimeError("load did not finish")
def tree_of(app: App, slots=("app-1",)) -> tuple[list[procs.Ident], list[int]]:
idents, ports = [], []
members = [psutil.Process(app.proc.pid), *psutil.Process(app.proc.pid).children(recursive=True)]
pids = {p.pid for p in members}
for slot in slots:
state = runtime._read_state(slot)
if state:
pids.add(state["host_pid"])
for r in procs.records():
if r.get("profile") in slots: # the app's own slots; a Terminal service is not part of the app's tree
pids.add(r["pid"])
for pid in pids:
token = procs.start_token(pid)
if token:
idents.append(procs.Ident(pid, token, tuple(procs.command_line(pid) or [])))
return idents, ports
def survivors(idents):
return [f"{i.pid} {' '.join(i.cmd)[:80]}" for i in idents if procs.start_token(i.pid) == i.token]
def wait_gone(idents, seconds):
deadline = time.monotonic() + seconds
left = survivors(idents)
while left and time.monotonic() < deadline:
time.sleep(0.25)
left = survivors(idents)
return left
def lease_free() -> bool:
try:
fd = runtime.acquire_activity_lease(shared=False)
except Exception: # noqa: BLE001
return False
runtime._unlock_file(fd)
os.close(fd)
return True
def control(scratch: Scratch, *argv: str, timeout: int = 300) -> dict:
py = scratch.install / "control" / "python" / "python.exe"
out = subprocess.run([str(py), str(scratch.install / "control" / "strata.py"), *argv, "--output", "json"],
env={**scratch.env, "PYTHONNOUSERSITE": "1", "STRATA_NO_ACQUIRE": "1"},
capture_output=True, text=True, timeout=timeout)
try:
return json.loads(out.stdout)
except ValueError:
return {"ok": False, "raw": out.stdout[-300:] + out.stderr[-300:]}
def stop_cli(scratch: Scratch, slot: str) -> dict:
py = scratch.install / "control" / "python" / "python.exe"
out = subprocess.run([str(py), str(scratch.install / "control" / "strata.py"), "runtime", "stop", "--profile", slot, "--wait", "30",
"--output", "json"], env={**scratch.env, "PYTHONNOUSERSITE": "1", "STRATA_NO_ACQUIRE": "1"},
capture_output=True, text=True, timeout=120)
try:
return json.loads(out.stdout)
except ValueError:
return {"state": "?", "raw": out.stdout[-200:] + out.stderr[-200:]}
def case(name: str, install: Path, how: str) -> dict:
with tempfile.TemporaryDirectory(prefix="strata-app-matrix-", ignore_cleanup_errors=True) as tmp:
saved = dict(os.environ)
scratch = Scratch(Path(tmp), install)
app = App(scratch)
problems, note = [], ""
try:
app.wait_up()
slots = ("app-1", "app-2") if how == "two-engines" else ("app-1",)
terminal = None
if how == "window-x-terminal-engine":
# an engine the Terminal started (its own profile, no tie to the app) must survive the app's close
control(scratch, "profile", "create", "terminal-1", "--from-config", str(scratch.models / "strata-stub2.json"))
terminal = control(scratch, "runtime", "start", "--profile", "terminal-1", "--wait", "60")
if terminal.get("state") != "ready":
raise RuntimeError(f"the Terminal's service did not start: {terminal}")
terminal_pids = {r["pid"] for r in procs.records(profile="terminal-1")} | {runtime._read_state("terminal-1")["host_pid"]}
state = app.load("stub")
if how == "two-engines":
state = app.load("stub2", multiple=True)
engine_ports = [i["port"] for i in state["instances"]]
idents, _ = tree_of(app, slots)
kinds = sorted({("engine" if "engine" in " ".join(i.cmd) else "server" if "serve.server" in " ".join(i.cmd) else
"owner" if "strata_control.runtime" in " ".join(i.cmd) else "app" if "strata-desktop" in i.cmd[0].lower()
else "host" if "node" in i.cmd[0].lower() else "other") for i in idents})
note = f"tree {len(idents)} processes: {', '.join(kinds)}; engine ports {engine_ports}"
t0 = time.monotonic()
if how == "normal-close":
subprocess.run(["taskkill", "/PID", str(app.proc.pid)], capture_output=True)
elif how in ("window-x", "two-engines", "window-x-terminal-engine"):
hwnd = main_window(app.proc.pid)
if not hwnd:
raise RuntimeError("no main window")
user32.PostMessageW(hwnd, WM_CLOSE, 0, 0)
elif how == "kill-app":
subprocess.run(["taskkill", "/F", "/PID", str(app.proc.pid)], capture_output=True)
elif how == "host-crash":
host = app.host()
subprocess.run(["taskkill", "/F", "/PID", str(host.pid)], capture_output=True)
elif how == "second-launch":
second = subprocess.Popen([str(install / "strata-desktop.exe")], cwd=install, env=scratch.env,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
try:
code = second.wait(timeout=30)
except subprocess.TimeoutExpired:
second.kill()
problems.append("the second launch did not exit")
code = None
time.sleep(2)
hosts = [p for p in psutil.process_iter(["name", "pid"]) if p.info["name"].lower() == "node.exe"
and (p.parent() and p.parent().pid == app.proc.pid)]
apps = [p for p in psutil.process_iter(["name"]) if p.info["name"].lower() == "strata-desktop.exe"]
if app.proc.poll() is not None:
problems.append("the first app went away")
if len(apps) != 1:
problems.append(f"{len(apps)} app processes (want 1)")
if len(hosts) != 1:
problems.append(f"{len(hosts)} hosts under the first app (want 1)")
after, _ = tree_of(app, slots)
new = [i for i in after if i.pid not in {x.pid for x in idents}]
if new:
problems.append(f"the second launch started: {[' '.join(i.cmd)[:60] for i in new]}")
note += f"; second launch exit code {code}, app processes {len(apps)}, hosts {len(hosts)}"
subprocess.run(["taskkill", "/PID", str(app.proc.pid)], capture_output=True)
if how == "host-crash":
# the shell and its window stay (nothing here restarts a dead host); the host, Control's owner, the server and
# the engine must not
idents = [i for i in idents if any(k in " ".join(i.cmd) for k in ("strata_control.runtime", "serve.server", "engine", "node.exe"))]
left = wait_gone(idents, 90)
if left:
problems.append(f"processes left: {left}")
for port in engine_ports:
deadline = time.monotonic() + 10
while port_open(port) and time.monotonic() < deadline:
time.sleep(0.2)
if port_open(port):
problems.append(f"engine port {port} still open")
if terminal is None and not lease_free():
problems.append("lease still held")
marks = sorted(p.name for p in scratch.mark.iterdir())
if terminal is not None:
status = control(scratch, "runtime", "status", "--profile", "terminal-1")
alive_pids = [p for p in terminal_pids if procs.start_token(p)]
if status.get("state") != "ready" or len(alive_pids) != len(terminal_pids):
problems.append(f"closing the app disturbed the Terminal's service: {status.get('state')}, alive {alive_pids} of {sorted(terminal_pids)}")
note += f"; the Terminal's service is {status.get('state')} after the app closed"
stopped = control(scratch, "runtime", "stop", "--profile", "terminal-1", "--wait", "30")
if stopped.get("state") != "stopped" or [p for p in terminal_pids if procs.start_token(p)]:
problems.append(f"the Terminal's service did not stop on request: {stopped}")
time.sleep(1)
if not lease_free():
problems.append("lease still held after the Terminal's service stopped")
if how in ("normal-close", "window-x", "two-engines", "second-launch", "window-x-terminal-engine") and not ({"quit", "eof"} & set(marks)):
problems.append(f"the engine was not stopped the clean way (marks {marks})")
seconds = round(time.monotonic() - t0, 1)
if how == "host-crash":
subprocess.run(["taskkill", "/PID", str(app.proc.pid)], capture_output=True)
note += "; the app shell stays open after a host crash (by design today)"
# the slot must be usable again: stopping a lost owner's slot cleans its state; nothing blocks the next load
for slot in slots:
res = stop_cli(scratch, slot)
already = "not managed or is stopped" in json.dumps(res) # nothing was left to stop: the clean outcome
if res.get("state") not in ("stopped",) and not already:
problems.append(f"runtime stop {slot} after the close: {res}")
if runtime._read_state(slot) is not None:
problems.append(f"state of {slot} left behind")
return {"name": f"app-{how}", "ok": not problems, "problems": problems, "seconds": seconds, "marks": marks, "note": note}
except Exception as exc: # noqa: BLE001
return {"name": f"app-{how}", "ok": False, "problems": [f"{type(exc).__name__}: {exc}"] + problems, "note": note}
finally:
for p in psutil.process_iter(["name", "exe"]):
try:
if (p.info["exe"] or "").lower().startswith(str(install).lower()):
p.kill()
except psutil.Error:
pass
for r in procs.records():
try:
psutil.Process(r["pid"]).kill()
except psutil.Error:
pass
os.environ.clear(); os.environ.update(saved)
SCENARIOS = ["normal-close", "window-x", "window-x-terminal-engine", "kill-app", "host-crash", "second-launch", "two-engines"]
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--install", required=True)
ap.add_argument("--scenarios", default="all")
ap.add_argument("--report")
a = ap.parse_args()
names = SCENARIOS if a.scenarios == "all" else a.scenarios.split(",")
results = []
for how in names:
r = case(how, Path(a.install), how)
results.append(r)
print(("PASS " if r["ok"] else "FAIL ") + r["name"], r.get("seconds", ""), r.get("problems") or "", r.get("note", ""), flush=True)
if a.report:
Path(a.report).write_text(json.dumps({"platform": "windows", "engine": "fake", "results": results}, indent=1), encoding="utf-8")
return 0 if all(r["ok"] for r in results) else 1
if __name__ == "__main__":
raise SystemExit(main())
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env node
/**
* The Graphics cards choice of the app, end to end on a box with two cards, through the installed app layout (packaged host + bundled Control + a real
* engine component): the model list carries the options (one rule, strata_control/gpu_fit.py), a model that fits one card recommends that card, then
* load with ONE card -> answers, only that card holds the model; load with BOTH cards (the layer split) -> answers, both cards hold the model.
*
* node cards-e2e.mjs --install <folder with node/ host/ control/> --components <folder holding engine/<package>> --work <scratch> \
* --family qwen --model IQ3_XXS --data <data folder that has the model files> [--smi "nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits"]
*/
import { spawn, spawnSync } from 'node:child_process';
import { existsSync, mkdirSync, readdirSync } from 'node:fs';
import { createServer } from 'node:net';
import { join, resolve } from 'node:path';
const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => (a.startsWith('--') ? [...acc, [a.slice(2), all[i + 1]?.startsWith('--') || all[i + 1] === undefined ? true : all[i + 1]]] : acc), []));
for (const need of ['install', 'components', 'work', 'model']) if (!args[need]) { console.error('missing --' + need); process.exit(2); }
const install = resolve(args.install), work = resolve(args.work), components = resolve(args.components), family = String(args.family ?? 'qwen'), modelId = String(args.model);
const node = join(install, 'node', 'node'), hostEntry = join(install, 'host', 'server', 'index.mjs');
const dirs = { models: join(work, 'models'), profiles: join(work, 'profiles'), runtime: join(work, 'runtime'), home: join(work, 'home'), xdg: join(work, 'xdg'), appdata: join(work, 'appdata') };
for (const d of [dirs.models, dirs.home, dirs.xdg, dirs.appdata]) mkdirSync(d, { recursive: true });
const delay = ms => new Promise(r => setTimeout(r, ms));
let failed = false;
const record = (name, ok, detail = '') => { if (!ok) failed = true; console.log(`${ok ? 'ok ' : 'FAIL'} ${name}${detail ? ' ' + String(detail).slice(0, 500) : ''}`); };
const freePort = async () => { const s = createServer(); await new Promise(r => s.listen(0, '127.0.0.1', r)); const p = s.address().port; await new Promise(r => s.close(r)); return p; };
const env = { ...process.env, STRATA_COMPONENTS_DIR: components, STRATA_PROFILE_DIR: dirs.profiles, STRATA_RUNTIME_DIR: dirs.runtime, STRATA_INSTALL_DIR: dirs.models,
STRATA_HOME: dirs.home, STRATA_MODELS_DIR: join(work, 'models-unused'), APPDATA: dirs.appdata, XDG_CONFIG_HOME: dirs.xdg, PYTHONDONTWRITEBYTECODE: '1' };
delete env.STRATA_API_KEY;
const port = await freePort();
const base = `http://127.0.0.1:${port}`;
const host = spawn(node, [hostEntry], { cwd: join(install, 'host'), stdio: ['ignore', 'pipe', 'pipe'],
env: { ...env, STRATA_PORT: String(port), STRATA_DIST: join(install, 'host', 'dist'), NODE_ENV: 'production', STRATA_PACKAGED: '1', STRATA_REAL_ENGINE: '1', STRATA_NO_ACQUIRE: '1',
STRATA_SHARE_PORT: String(await freePort()), STRATA_TUNNEL_PORT: String(await freePort()) } });
let hostLog = '';
host.stdout.on('data', c => { hostLog += c; }); host.stderr.on('data', c => { hostLog += c; });
const call = async (path, method = 'GET', body) => {
const response = await fetch(base + path, { method, headers: { 'content-type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body) });
return { status: response.status, body: await response.json().catch(() => null) };
};
async function waitJob(id, seconds) {
let last = '';
for (let i = 0; i < seconds * 2; i++) {
const state = (await call('/api/native')).body;
const job = state.jobs.find(j => j.id === id);
if (job && job.summary !== last) { last = job.summary; console.log(' ... ' + String(last).slice(0, 120)); }
if (job && job.state !== 'running') return { job, state };
await delay(500);
}
throw new Error('operation did not finish');
}
const smi = () => {
const cmd = String(args.smi ?? 'nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits').split(' ');
const r = spawnSync(cmd[0], cmd.slice(1), { encoding: 'utf8' });
return (r.stdout ?? '').split('\n').map(s => Number(s.trim())).filter(Number.isFinite);
};
async function chatPong(instance) {
const chat = await call('/v1/chat/completions', 'POST', { model: instance?.apiModelId ?? modelId.toLowerCase(), max_tokens: 400, stream: false, temperature: 0, messages: [{ role: 'user', content: 'Reply with exactly one word: pong' }] });
const m = chat.body?.choices?.[0]?.message ?? {};
return { ok: chat.status === 200 && /pong/i.test(`${m.content ?? ''}`), text: `${m.content ?? ''}`.slice(0, 60), status: chat.status };
}
async function main() {
let up = false;
for (let i = 0; i < 200 && !up; i++) { try { up = (await fetch(base + '/api/health')).ok; } catch { /* not yet */ } if (!up) await delay(150); }
record('host healthy', up, up ? base : hostLog.slice(-500));
if (!up) return;
const packages = existsSync(join(components, 'engine')) ? readdirSync(join(components, 'engine')) : [];
record('an engine component is installed', packages.length > 0, packages.join(', '));
// ---- the model list carries the choice
const native = (await call('/api/native?refresh=1')).body;
const gpus = native.hardware?.gpus ?? [];
record('the host sees two or more graphics cards', gpus.filter(g => g.usable !== false).length >= 2, JSON.stringify(gpus.map(g => ({ i: g.index, n: g.name, gb: g.vram_gb }))));
const size = native.catalog?.families?.find(f => f.family === family)?.sizes.find(s => s.model === modelId);
const opt = size?.gpu_options;
record('the model list offers the cards choice for this model', !!opt && opt.options.length >= 2, JSON.stringify(opt?.options?.map(o => ({ key: o.key, cards: o.cards, verdict: o.verdict, rec: o.recommended }))));
record('a model that fits one card recommends the single largest card (the split is offered, not forced)', opt?.recommended === 'single' && opt.options.find(o => o.key === 'split'), `recommended=${opt?.recommended}`);
const single = opt?.options.find(o => o.key === 'single')?.cards ?? [0];
const both = opt?.options.find(o => o.key === 'split')?.cards ?? [0, 1];
// ---- Download and prepare with the recommended card
const values = { family, model: modelId, context: 32768, vision: 'no', gpu: single[0], ...(args.data ? { data_dir: resolve(args.data) } : {}) };
const started = await call('/api/native/operations', 'POST', { kind: 'install', args: values });
record('the install operation starts', started.status === 200 || started.status === 202, JSON.stringify(started.body).slice(0, 160));
const done = await waitJob(started.body.id, 3600);
record('Download and prepare finishes', done.job.state === 'done', `${done.job.state} ${done.job.error ?? done.job.summary}`);
const installed = (await call('/api/native?refresh=1')).body.installed?.models?.find(m => String(m.model).toLowerCase() === modelId.toLowerCase());
const model = installed?.model ?? modelId.toLowerCase();
for (const [label, cards] of [['ONE card', single], ['BOTH cards (layer split)', both]]) {
const before = smi();
const load = await call('/api/native/operations', 'POST', { kind: 'load', args: { model, gpus: cards } });
const loaded = await waitJob(load.body.id, 900);
const instance = loaded.state.instances[0];
record(`load with ${label} starts the engine`, loaded.job.state === 'done' && instance?.status === 'ready', `${loaded.job.state} ${loaded.job.error ?? ''}; instance=${instance?.status}; gpus=${JSON.stringify(instance?.gpus)}`);
record(`the instance runs on exactly the chosen card(s)`, JSON.stringify([...(instance?.gpus ?? [])].sort()) === JSON.stringify([...cards].sort()), `want ${JSON.stringify(cards)} got ${JSON.stringify(instance?.gpus)}`);
await delay(3000);
const used = smi();
const holding = used.map((mb, i) => (mb - (before[i] ?? 0)) > 1000 ? i : -1).filter(i => i >= 0);
record(`only the chosen card(s) hold the model (graphics memory in use: ${used.join(', ')} MiB)`, JSON.stringify(holding) === JSON.stringify([...cards].sort()), `holding ${JSON.stringify(holding)}`);
const pong = await chatPong(instance);
record(`it answers (${label})`, pong.ok, `${pong.status} ${JSON.stringify(pong.text)}`);
const unloaded = await call(`/api/native/instances/${instance?.id}/unload`, 'POST', {});
record(`unload (${label})`, unloaded.status === 200, unloaded.status);
await delay(4000);
}
}
try { await main(); } catch (e) { record('run', false, e.stack ?? e); }
host.kill();
await delay(1500);
console.log(failed ? 'CARDS-E2E FAIL' : 'CARDS-E2E PASS');
process.exit(failed ? 1 : 0);
+72
View File
@@ -0,0 +1,72 @@
# Fetch and verify the private Node runtime the Windows desktop bundle ships (win-x64).
#
# The PowerShell twin of fetch-node.sh: same pinned version, same SHA-256 pin, same refusal to
# install anything whose digest does not match. Not committed output: node.exe is a ~90 MB binary.
#
# Usage:
# powershell -NoProfile -ExecutionPolicy Bypass -File apps\desktop\scripts\fetch-node.ps1
# ... -File fetch-node.ps1 -Check # verify what is already installed
# ... -File fetch-node.ps1 -FromFile node.zip # verify and install a zip you already have (offline)
#
# The Node version must satisfy the App Host's own engines.node (>= 24.12.0).
[CmdletBinding()]
param(
[switch]$Check,
[string]$FromFile = '',
[string]$Dest = ''
)
$ErrorActionPreference = 'Stop'
$NodeVersion = if ($env:STRATA_NODE_VERSION) { $env:STRATA_NODE_VERSION } else { '24.12.0' }
$Target = 'win-x64'
# SHA-256 of node-v24.12.0-win-x64.zip from nodejs.org's SHASUMS256.txt; keep equal to fetch-node.sh.
$Expected = '9c125f61ae947b52e779095830f9cac267846a043ef7192183c84016aaad2812'
if (-not $Dest) { $Dest = Join-Path $PSScriptRoot '..\src-tauri\resources\node' }
$Installed = Join-Path $Dest 'node.exe'
function Fail([string]$Message) { [Console]::Error.WriteLine("fetch-node: $Message"); exit 1 }
if ($Check) {
if (-not (Test-Path -LiteralPath $Installed)) { Fail "not installed at $Installed" }
$got = (& $Installed --version).Trim()
Write-Output "fetch-node: $Installed is $got"
if ($got -ne "v$NodeVersion") { Fail "expected v$NodeVersion" }
exit 0
}
$Archive = "node-v$NodeVersion-$Target.zip"
$Work = Join-Path ([IO.Path]::GetTempPath()) ("strata-node-" + [Guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $Work | Out-Null
try {
$Zip = Join-Path $Work $Archive
if ($FromFile) {
Copy-Item -LiteralPath $FromFile -Destination $Zip
} else {
$Url = "https://nodejs.org/dist/v$NodeVersion/$Archive"
Write-Output "fetch-node: $Url"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$ProgressPreference = 'SilentlyContinue'
Invoke-WebRequest -UseBasicParsing -Uri $Url -OutFile $Zip
}
$Actual = (Get-FileHash -Algorithm SHA256 -LiteralPath $Zip).Hash.ToLowerInvariant()
if ($NodeVersion -ne '24.12.0') {
# another version has no recorded digest: say so rather than implying a verification
[Console]::Error.WriteLine("fetch-node: WARNING - no recorded digest for v$NodeVersion; this download is NOT authenticated.")
[Console]::Error.WriteLine("fetch-node: computed sha256 $Actual - record it in this script and fetch-node.sh.")
} elseif ($Actual -ne $Expected) {
Fail "REFUSING - $Archive hashes $Actual, expected $Expected"
} else {
Write-Output 'fetch-node: sha256 verified against the recorded digest'
}
Add-Type -AssemblyName System.IO.Compression.FileSystem
$zipFile = [IO.Compression.ZipFile]::OpenRead($Zip)
try {
$entry = $zipFile.GetEntry("node-v$NodeVersion-$Target/node.exe")
if (-not $entry) { Fail "node.exe is not inside $Archive" }
New-Item -ItemType Directory -Force -Path $Dest | Out-Null
[IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $Installed, $true)
} finally { $zipFile.Dispose() }
Write-Output "fetch-node: installed at $Installed"
} finally {
Remove-Item -LiteralPath $Work -Recurse -Force -ErrorAction SilentlyContinue
}
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env bash
# Fetch and verify the private Node runtime the desktop bundle ships.
#
# The App Host is an ordinary Node program, and the packaging requirement is that an end user
# supplies no runtime of their own. The desktop bundle therefore carries a pinned standalone Node
# beside the host.
#
# It is NOT committed. It is a ~117 MB platform binary: committing it would bloat every clone, it
# cannot be reviewed in a diff, and it is not source. This script is the reproducible substitute —
# it downloads the official build for the target and refuses to install anything whose SHA-256 does
# not match the recorded digest.
#
# Usage:
# apps/desktop/scripts/fetch-node.sh # host platform
# apps/desktop/scripts/fetch-node.sh --target linux-x64
# apps/desktop/scripts/fetch-node.sh --check # verify what is already installed
#
# The Node version must satisfy the App Host's own `engines.node` (>= 24.12.0).
set -euo pipefail
NODE_VERSION="${STRATA_NODE_VERSION:-24.12.0}"
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEST="$HERE/../src-tauri/resources/node"
# Published SHA-256 of each official nodejs.org archive, recorded so a tampered or truncated
# download is refused rather than shipped. Add a row when bumping the version.
# Taken from nodejs.org's own SHASUMS256.txt for v24.12.0, which is published over HTTPS beside
# the archives. Recorded here so the fetch is verified against a value that does not come from the
# same response as the payload.
digest_for() {
case "$1" in
darwin-arm64) echo "319f221adc5e44ff0ed57e8a441b2284f02b8dc6fc87b8eb92a6a93643fd8080" ;;
linux-x64) echo "6159227e0af7d7c3c6bb2fa900452b04a6cb8841a702a79acc613209d70b04d0" ;;
linux-arm64) echo "9b2a2eeb98a8eb37361224e2a1d060300ad2dd143af58dfdb16de785df0f1228" ;;
win-x64) echo "9c125f61ae947b52e779095830f9cac267846a043ef7192183c84016aaad2812" ;;
*) echo "" ;;
esac
}
target() {
if [ "${1:-}" = "--target" ]; then echo "${2:?--target needs a value}"; return; fi
case "$(uname -s)-$(uname -m)" in
Darwin-arm64) echo darwin-arm64 ;;
Linux-x86_64) echo linux-x64 ;;
Linux-aarch64) echo linux-arm64 ;;
MINGW*|MSYS*|CYGWIN*) echo win-x64 ;;
*) echo "unsupported host: $(uname -s)-$(uname -m)" >&2; exit 2 ;;
esac
}
TARGET="$(target "$@")"
EXE="node"
case "$TARGET" in win-*) EXE="node.exe" ;; esac
INSTALLED="$DEST/$EXE"
if [ "${1:-}" = "--check" ]; then
if [ ! -x "$INSTALLED" ]; then echo "fetch-node: not installed at $INSTALLED" >&2; exit 1; fi
got="$("$INSTALLED" --version)"
echo "fetch-node: $INSTALLED is $got"
case "$got" in "v$NODE_VERSION") exit 0 ;; *) echo "fetch-node: expected v$NODE_VERSION" >&2; exit 1 ;; esac
fi
# The official archive for this target.
case "$TARGET" in
darwin-arm64) ARCHIVE="node-v$NODE_VERSION-darwin-arm64.tar.gz" ;;
darwin-x64) ARCHIVE="node-v$NODE_VERSION-darwin-x64.tar.gz" ;;
linux-x64) ARCHIVE="node-v$NODE_VERSION-linux-x64.tar.gz" ;;
linux-arm64) ARCHIVE="node-v$NODE_VERSION-linux-arm64.tar.gz" ;;
win-x64) ARCHIVE="node-v$NODE_VERSION-win-x64.zip" ;;
*) echo "fetch-node: no archive known for $TARGET" >&2; exit 2 ;;
esac
URL="https://nodejs.org/dist/v$NODE_VERSION/$ARCHIVE"
EXPECTED="$(digest_for "$TARGET")"
WORK="$(mktemp -d "${TMPDIR:-/tmp}/strata-node.XXXXXX")" # GNU mktemp needs the X template; BSD accepts it too
trap 'rm -rf "$WORK"' EXIT
echo "fetch-node: $URL"
curl -fsSL -o "$WORK/$ARCHIVE" "$URL"
ACTUAL="$(shasum -a 256 "$WORK/$ARCHIVE" 2>/dev/null | cut -d' ' -f1 || sha256sum "$WORK/$ARCHIVE" | cut -d' ' -f1)"
if [ -n "$EXPECTED" ]; then
if [ "$ACTUAL" != "$EXPECTED" ]; then
echo "fetch-node: REFUSING — $ARCHIVE hashes $ACTUAL, expected $EXPECTED" >&2
exit 1
fi
echo "fetch-node: sha256 verified against the recorded digest"
else
# Without a recorded digest the download cannot be authenticated. Say so loudly and keep the
# computed value so it can be recorded, rather than implying a verification that did not happen.
echo "fetch-node: WARNING — no recorded digest for $TARGET; this download is NOT authenticated." >&2
echo "fetch-node: computed sha256 $ACTUAL — record it in $(basename "${BASH_SOURCE[0]}") to enable verification." >&2
fi
mkdir -p "$DEST"
case "$ARCHIVE" in
*.zip) unzip -q -j "$WORK/$ARCHIVE" "node-v$NODE_VERSION-win-x64/node.exe" -d "$DEST" ;;
*) tar xzf "$WORK/$ARCHIVE" -C "$WORK"
cp "$WORK/node-v$NODE_VERSION-$TARGET/bin/node" "$DEST/node" ;;
esac
chmod +x "$DEST/$EXE"
echo "fetch-node: installed $("$DEST/$EXE" --version) at $DEST/$EXE"
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env node
/**
* The first run of the installed app, headless: the host as the Tauri shell starts it (STRATA_PACKAGED=1, STRATA_NO_ACQUIRE=1,
* the staged Control, nothing installed, nothing in the per-user folders) and Chrome on its onboarding.
*
* node apps/desktop/scripts/first-run-ui.mjs [--control <staged control>] [--node <node.exe>] [--work <scratch>] [--shots <dir>]
*
* Checks, one line each (exit 1 if any fails):
* the host answers; /api/native has a real catalog (families with sizes), a recommendation and the GPUs of this PC
* (from Control's bundled installer on Control's Python); the page shows "Let's get started", and after it the model
* list with sizes (no "couldn't show this screen"); no page error; the install plan for the recommended model is a
* plan (nothing is downloaded or started: a plan changes nothing); the spy sees no pip/git/compiler/engine fetch.
* Per-user folders (STRATA_HOME, APPDATA, LOCALAPPDATA, the components and install folders) are all under --work.
* No model is downloaded and no engine runs.
*/
import { spawn } from 'node:child_process';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
import { createServer } from 'node:net';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => (a.startsWith('--') ? [...acc, [a.slice(2), all[i + 1]?.startsWith('--') ? true : all[i + 1]]] : acc), []));
const here = dirname(fileURLToPath(import.meta.url));
const repoHost = resolve(here, '../../host');
const staged = resolve(here, '../src-tauri/resources');
const control = resolve(args.control ?? join(staged, 'control'));
const nodeExe = resolve(args.node ?? join(staged, 'node', process.platform === 'win32' ? 'node.exe' : 'node'));
const work = resolve(args.work ?? mkdtempSync(join(tmpdir(), 'strata-first-run-')));
const shots = resolve(args.shots ?? join(work, 'shots'));
mkdirSync(work, { recursive: true }); mkdirSync(shots, { recursive: true });
const spy = pathToFileURL(join(repoHost, 'tests/helpers/acquire-spy.mjs')).href;
const spyLog = join(work, 'spy.log'); writeFileSync(spyLog, '');
const { chromium } = await import(pathToFileURL(join(repoHost, 'node_modules/playwright-core/index.mjs')).href).catch(() => import('playwright-core'));
const CHROME = process.env.CHROME ?? ['C:/Program Files/Google/Chrome/Application/chrome.exe', '/usr/bin/google-chrome', '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'].find(existsSync);
let failed = false;
const check = (ok, label, detail = '') => { console.log(`${ok ? 'ok ' : 'FAIL'} ${label}${detail ? ' ' + detail : ''}`); if (!ok) failed = true; };
const delay = ms => new Promise(r => setTimeout(r, ms));
const freePort = async () => { const s = createServer(); await new Promise(r => s.listen(0, '127.0.0.1', r)); const p = s.address().port; await new Promise(r => s.close(r)); return p; };
for (const need of [nodeExe, join(control, 'strata.py'), join(repoHost, 'dist', 'index.html')]) if (!existsSync(need)) { console.error(`missing ${need} (run stage-control.py, fetch-node and "npm run build" in apps/host first)`); process.exit(2); }
const dirs = { home: join(work, 'home'), appdata: join(work, 'appdata'), local: join(work, 'local'), components: join(work, 'local', 'Strata', 'components') };
for (const d of Object.values(dirs)) mkdirSync(d, { recursive: true });
const port = await freePort();
const env = {
...process.env,
STRATA_PORT: String(port), STRATA_DIST: join(repoHost, 'dist'), STRATA_PACKAGED: '1', STRATA_REAL_ENGINE: '1', STRATA_NO_ACQUIRE: '1',
STRATA_CONTROL_DIR: control, STRATA_HOME: dirs.home, STRATA_MODELS_DIR: join(work, 'models'), STRATA_COMPONENTS_DIR: dirs.components,
APPDATA: dirs.appdata, LOCALAPPDATA: dirs.local, XDG_CONFIG_HOME: join(work, 'xdg'), XDG_DATA_HOME: join(work, 'xdgdata'),
STRATA_PROFILE_DIR: join(work, 'profiles'), STRATA_RUNTIME_DIR: join(work, 'runtime'), ACQUIRE_SPY_LOG: spyLog,
};
delete env.STRATA_INSTALL_DIR;
mkdirSync(env.STRATA_MODELS_DIR, { recursive: true });
const host = spawn(nodeExe, [...(args['no-spy'] === undefined ? ['--import', spy] : []), join(repoHost, 'server', 'index.mjs')], { cwd: repoHost, env, stdio: ['ignore', 'pipe', 'pipe'] });
let hostLog = '';
host.stdout.on('data', c => { hostLog += c; }); host.stderr.on('data', c => { hostLog += c; });
const base = `http://127.0.0.1:${port}`;
const api = async (path, init) => { const r = await fetch(base + path, init); return { status: r.status, body: await r.json().catch(() => null) }; };
let browser;
try {
let up = false;
for (let i = 0; i < 150 && !up; i++) { try { up = (await api('/api/health')).status === 200; } catch { await delay(200); } }
check(up, 'the packaged host starts', `port ${port}`);
if (!up) throw new Error(hostLog.slice(-2000));
const native = await api('/api/native?refresh=1');
const state = native.body;
check(native.status === 200 && state?.available === true, 'the host reads Control', state?.error ?? '');
const sizes = (state?.catalog?.families ?? []).reduce((n, f) => n + (f.sizes?.length ?? 0), 0);
check((state?.catalog?.families?.length ?? 0) > 0 && sizes >= 3, 'the catalog has real families and sizes', `${state?.catalog?.families?.length} families, ${sizes} sizes`);
check(!!state?.catalog?.recommendation, 'the catalog has a recommendation', JSON.stringify(state?.catalog?.recommendation ?? null).slice(0, 120));
check(Array.isArray(state?.hardware?.gpus), 'the hardware lists GPUs', (state?.hardware?.gpus ?? []).map(g => `${g.name} ${g.vram_gb} GB`).join('; ') || 'none on this PC');
check(state?.installDir && realpathSync.native(state.installDir).toLowerCase().startsWith(realpathSync.native(work).toLowerCase()), 'models go to a writable per-user folder, not the bundle', state?.installDir);
const hw = (await api('/api/hardware')).body;
console.log('info host hardware probe:', JSON.stringify((hw?.gpus ?? []).map(g => ({ name: g.name, backend: g.backend, vramGb: g.vramGb, unified: g.unified }))));
check(!state?.installerError, 'the bundled installer answers', state?.installerError ?? '');
browser = await chromium.launch({ executablePath: CHROME, headless: true });
const page = await browser.newPage({ viewport: { width: 1400, height: 950 } });
const problems = [];
page.on('pageerror', e => problems.push(`pageerror: ${e.message}`));
await page.goto(base, { waitUntil: 'domcontentloaded' });
await page.waitForSelector('text=/Let.s get started|Choose a version|Connect your Strata/i', { timeout: 30000 }).catch(() => {});
await page.screenshot({ path: join(shots, '1-welcome.png') });
const broken = await page.locator('text=/couldn.t show this screen/i').count();
check(broken === 0, 'the first screen renders (not "Strata couldn\'t show this screen")');
const start = page.getByRole('button', { name: /get started/i });
check(await start.count() > 0, 'the welcome step offers "Let\'s get started"');
if (await start.count()) await start.first().click();
await page.waitForSelector('text=/Choose a version/i', { timeout: 30000 }).catch(() => {});
await delay(1200);
await page.screenshot({ path: join(shots, '2-models.png') });
check(await page.locator('text=/couldn.t show this screen/i').count() === 0, 'the model step renders');
check(await page.getByText(/Choose a version/i).count() > 0, 'the model step lists the versions');
const labels = await page.locator('[role=radio]').allInnerTexts();
check(labels.length >= 3, 'the versions are the real catalog\'s sizes', labels.map(l => l.replace(/\s+/g, ' ').slice(0, 40)).join(' | '));
check(labels.some(l => /\d+\.\d\s*GB/.test(l)), 'each version shows its download size');
check(problems.length === 0, 'no page error', problems.join('; '));
const recommended = state?.catalog?.recommendation;
if (recommended?.model) {
const plan = await api('/api/native/plan', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ family: recommended.family, model: recommended.model, context: recommended.context, data_dir: join(work, 'Strata-data') }) });
check(plan.status === 200 && !!plan.body?.plan, 'the install plan for the recommended model is a plan (nothing downloaded)', plan.body?.summary?.slice(0, 100) ?? JSON.stringify(plan.body).slice(0, 150));
check(!existsSync(join(work, 'Strata-data', 'models')), 'a plan wrote no model files');
}
} catch (error) {
check(false, 'the run', String(error?.message ?? error).slice(0, 600));
} finally {
await browser?.close().catch(() => {});
host.kill();
await new Promise(r => host.once('exit', r)); // only the PID this script started
const attempts = readFileSync(spyLog, 'utf8').split('\n').filter(Boolean);
check(attempts.length === 0, 'the spy saw no pip, git, compiler, engine or internet access', attempts.slice(0, 3).join(' '));
console.log(`shots in ${shots}`);
if (!args.work) rmSync(work, { recursive: true, force: true, maxRetries: 3 });
}
process.exit(failed ? 1 : 0);
+133
View File
@@ -0,0 +1,133 @@
/**
* Turn a pnpm "isolated" node_modules (a virtual store in `.pnpm` full of links) into an ordinary hoisted tree of real folders.
*
* Why: the installer copies the Harness folder file by file. pnpm's layout is thousands of links (junctions on Windows); a copy
* that follows them duplicates every package once per dependent and one that does not breaks on the first dangling link. A hoisted
* tree has each package once (nested only where two versions of the same name are needed), no links, and Node resolves it
* exactly as it resolves any npm install.
*
* Algorithm (the one npm uses, in short): the packages the root names come first, at the top. Then, walking each placed package's
* own dependencies: look the name up from the package's own folder upwards; if the same instance is already visible, nothing to
* do; if a DIFFERENT instance of that name is visible, nest this one inside the dependent; if none is visible, hoist it to the top.
*
* @param {string} out folder holding `node_modules`
* @param {{skip?: RegExp}} [options] package names never copied (the caller removes them anyway)
* @returns {{packages:number, copied:number, nested:number}}
*/
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync } from 'node:fs';
import { dirname, join, relative, sep } from 'node:path';
export function flattenPnpm(out, { skip = null } = {}) {
const nm = join(out, 'node_modules');
const store = join(nm, '.pnpm');
const flatRoot = join(out, 'node_modules.flat');
rmSync(flatRoot, { recursive: true, force: true });
mkdirSync(join(flatRoot, 'node_modules'), { recursive: true });
const manifest = dir => { try { return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')); } catch { return null; } };
const nameOf = dir => manifest(dir)?.name ?? null;
/**
* What makes two instances "the same package": name and version. pnpm keeps one copy per set of peer packages (the same workspace
* package under many hashed folders); in one hoisted tree they are one package.
*/
const identity = dir => { const m = manifest(dir); return m ? `${m.name}@${m.version}` : dir; };
/** The sibling links of a store instance: its dependencies, by name -> real instance folder. */
function storeDeps(instance, name) {
const container = name.startsWith('@') ? dirname(dirname(instance)) : dirname(instance);
const found = new Map();
for (const e of readdirSync(container, { withFileTypes: true })) {
if (e.name === '.bin') continue;
const entries = e.name.startsWith('@') ? readdirSync(join(container, e.name), { withFileTypes: true }).map(x => [`${e.name}/${x.name}`, join(container, e.name, x.name)]) : [[e.name, join(container, e.name)]];
for (const [dep, p] of entries) {
if (dep === name) continue;
try { if (lstatSync(p).isSymbolicLink() || existsSync(p)) found.set(dep, realpathSync(p)); } catch { /* a dangling link: the package was removed on purpose */ }
}
}
return found;
}
/** Every instance in the store by package name, for packages that arrived without a store entry (copied peers). */
const byName = new Map();
if (existsSync(store)) {
for (const id of readdirSync(store, { withFileTypes: true })) {
if (!id.isDirectory()) continue;
const container = join(store, id.name, 'node_modules');
if (!existsSync(container)) continue;
for (const e of readdirSync(container, { withFileTypes: true })) {
if (e.name === '.bin') continue;
const dirs = e.name.startsWith('@') ? readdirSync(join(container, e.name)).map(x => join(container, e.name, x)) : [join(container, e.name)];
for (const d of dirs) {
let real; try { real = realpathSync(d); } catch { continue; }
if (!real.startsWith(store)) continue; // a link to a workspace package outside the store is handled through the instance table
const n = nameOf(real);
if (n && !byName.has(n)) byName.set(n, real);
}
}
}
}
const placed = new Map(); // folder (in the flat tree) -> real instance folder it holds
let copied = 0, nested = 0;
const visibleFrom = (chain, name) => {
for (const dir of chain) {
const slot = join(dir, 'node_modules', ...name.split('/'));
if (placed.has(slot)) return { slot, instance: placed.get(slot) };
}
return null;
};
const copyInto = (slot, instance) => {
mkdirSync(dirname(slot), { recursive: true });
cpSync(instance, slot, { recursive: true, dereference: true, filter: f => !relative(instance, f).split(sep).includes('node_modules') });
placed.set(slot, identity(instance));
copied++;
};
const queue = []; // { slotDir (folder of the placed package), chain (folders searched upwards), instance, name }
const rootChain = [flatRoot];
const place = (name, instance, chain, forceHere) => {
if (skip?.test(name)) return;
const seen = visibleFrom(chain, name);
if (seen && seen.instance === identity(instance)) return;
const hoist = !seen && !forceHere;
const base = hoist ? flatRoot : chain[0];
const slot = join(base, 'node_modules', ...name.split('/'));
if (placed.has(slot)) return; // another instance already sits where this one would go (it nests below, handled by the caller's chain)
copyInto(slot, instance);
if (!hoist && base !== flatRoot) nested++;
queue.push({ slotDir: slot, chain: [slot, ...(hoist ? rootChain : chain)], instance, name });
};
// 1. the packages the root names
const rootEntries = [];
for (const e of readdirSync(nm, { withFileTypes: true })) {
if (e.name === '.pnpm' || e.name === '.bin' || e.name.startsWith('.')) continue;
const entries = e.name.startsWith('@') ? readdirSync(join(nm, e.name)).map(x => [`${e.name}/${x}`, join(nm, e.name, x)]) : [[e.name, join(nm, e.name)]];
for (const [name, p] of entries) rootEntries.push([name, p]);
}
for (const [name, p] of rootEntries) {
let real; try { real = realpathSync(p); } catch { continue; }
place(name, real, rootChain, true);
}
// 2. their dependencies, breadth first
while (queue.length) {
const { slotDir, chain, instance, name } = queue.shift();
let wants;
if (instance.startsWith(store)) wants = storeDeps(instance, name);
else {
// a package copied in without a store entry: its declared requirements, taken from the store by name when not visible yet
const m = manifest(instance) ?? {};
wants = new Map();
for (const dep of [...Object.keys(m.dependencies ?? {}), ...Object.keys(m.peerDependencies ?? {}), ...Object.keys(m.optionalDependencies ?? {})]) {
if (visibleFrom(chain, dep)) continue;
const real = byName.get(dep);
if (real) wants.set(dep, real);
}
}
for (const [dep, real] of wants) place(dep, real, chain, false);
}
rmSync(nm, { recursive: true, force: true });
renameSync(join(flatRoot, 'node_modules'), nm);
rmSync(flatRoot, { recursive: true, force: true });
return { packages: placed.size, copied, nested };
}
+79
View File
@@ -0,0 +1,79 @@
/**
* Which files and folders of the staged Harness are dropped, and the check that nothing the Harness imports is gone.
*
* The 2026-10-17 tester report: on Linux and Mac every agent chat failed ("session/create ... waiting for workflowEngine").
* Cause: the old rule "drop any folder named after another operating system" also removed the Harness's own JavaScript package
* `@deepseek-ai/dsh-win32-process`. It is plain JS (no binary), and `dsh-subprocess-local` and `dsh-sandbox-local` import it by
* name on every platform, so those two plugins failed to load, and everything waiting on them (shell, subprocess, ptcRuntime,
* workflowEngine) never started. Windows kept it (the rule differs there), so the Windows tests passed.
*/
import { existsSync, readFileSync, readdirSync } from 'node:fs';
import { join, sep } from 'node:path';
export const OTHER_OS = {
win32: /(^|[\\/._-])(darwin|linux|android|freebsd|musl)([\\/._-]|$)/i,
darwin: /(^|[\\/._-])(win32|linux|android|freebsd|musl)([\\/._-]|$)/i,
// Linux: the AppImage bundler (linuxdeploy) resolves the libraries of every ELF file under resources and stops on one it
// cannot satisfy (koffi's musl build wants libc.musl). The app is glibc x86_64, so musl and other-CPU builds are dropped.
linux: /(^|[\\/._-])(win32|darwin|android|freebsd|musl|arm64|aarch64|armv7|arm|ia32|riscv64|ppc64|s390x|loong64)([\\/._-]|$)/i,
};
const NATIVE_FOLDER = /prebuild|bin|native|platform|-(win32|darwin|linux)/i;
/** The Harness's own JavaScript packages (`@deepseek-ai/dsh-*`) are code every platform imports by name; never drop one. */
export function isHarnessCodePackage(path) {
const parts = path.split(sep);
const i = parts.lastIndexOf('@deepseek-ai');
return i >= 0 && i === parts.length - 2 && parts[i + 1].startsWith('dsh-');
}
/** True when the folder `path` (a directory named `name`) is another operating system's prebuilt binary and can go. */
export function isOtherOsBinaryFolder(platform, name, path) {
const rule = OTHER_OS[platform] ?? OTHER_OS.linux;
if (isHarnessCodePackage(path)) return false;
return rule.test(name) && NATIVE_FOLDER.test(path);
}
/** Imports the Windows copy (which works) is also without: browser-side or dev-only bundles, and the document converter the app drops on purpose. */
const KNOWN_ABSENT = new Set([
'@deepseek-ai/libreoffice-kit', '@deepseek-ai/dsh-client-ui-dockkit', '@deepseek-ai/dsh-client-web',
'@deepseek-ai/dsh-experimental-webworker-runtime', '@deepseek-ai/dsh-remote-mock', '@deepseek-ai/dsh-client-test-runtime',
]);
function jsFiles(dir, out = []) {
for (const e of readdirSync(dir, { withFileTypes: true })) {
const p = join(dir, e.name);
if (e.isSymbolicLink()) continue;
if (e.isDirectory()) jsFiles(p, out);
else if (/\.(m?js|cjs)$/.test(e.name)) out.push(p);
}
return out;
}
/**
* Every bare `@deepseek-ai/<package>` the staged Harness's own packages import (static `from "x"` and `import("x")`) must exist
* in the staged node_modules. Returns the list of `{from, missing}` problems (empty = good). Plugins that are only loaded on
* another platform still count: Node resolves a static import whether or not the code runs.
*/
export function missingHarnessImports(harnessDir) {
const scope = join(harnessDir, 'node_modules', '@deepseek-ai');
if (!existsSync(scope)) return [{ from: scope, missing: '@deepseek-ai (no installed packages)' }];
const problems = [];
const seen = new Set();
const re = /(?:from|import)\s*\(?\s*["'](@deepseek-ai\/[^"'/]+)(?:\/[^"']*)?["']/g;
for (const pkg of readdirSync(scope)) {
const lib = join(scope, pkg, 'lib');
if (!existsSync(lib)) continue;
for (const file of jsFiles(lib)) {
const text = readFileSync(file, 'utf8');
for (const m of text.matchAll(re)) {
const name = m[1];
const key = `${pkg}>${name}`;
if (seen.has(key) || KNOWN_ABSENT.has(name)) continue;
seen.add(key);
if (!existsSync(join(harnessDir, 'node_modules', ...name.split('/')))
&& !existsSync(join(scope, pkg, 'node_modules', ...name.split('/')))) problems.push({ from: `${pkg}`, missing: name });
}
}
}
return problems;
}
+310
View File
@@ -0,0 +1,310 @@
#!/usr/bin/env node
/**
* Prove the INSTALLED app layout can start and stop an engine with nothing acquired, using a stub engine.
*
* node apps/desktop/scripts/installed-flow.mjs --install <folder the installer wrote> [--work <scratch>] [--spy <acquire-spy.mjs>]
*
* It starts the installed host with the installed node.exe exactly as the Tauri shell does (STRATA_PACKAGED=1,
* STRATA_NO_ACQUIRE=1; Control found beside the host), against a scratch component folder holding a fake engine
* package and a scratch install folder holding one run config. Everything per-user (profiles, runtime state, host data,
* components) lives under --work, so the real profile of whoever runs this is never read or written.
*
* Steps (each one printed, the run exits 1 if any fails):
* host health, Control status (engine usable, acquiring forbidden), models from Control, load through the host
* (profile + `runtime start` under the owner runtime), a chat answer from the stub engine, a second client attaches to
* the same owner/instance, unload stops it, no engine for this PC is a notice with nothing started, the Tauri-style
* clean shutdown stops the engines the host started, a hard-killed host takes its engine with it.
* With --spy the host runs under tests/helpers/acquire-spy.mjs and the run fails on any attempt to reach the internet or
* run an installer.
*
* REAL mode (a test box with a GPU): --components <folder holding engine/<package>> --models <folder with strata-<id>.json
* whose engine is "component:strata-engine"> --model <id> [--backend cuda]. The same host and Control, a real engine and a
* real model: load, a real answer, ownership, unload, the missing-engine notice, load again, clean shutdown. The stub-only
* steps (the marker file, the hard-kill report) are left out.
*/
import { spawn, spawnSync } from 'node:child_process';
import { createHash, randomBytes } from 'node:crypto';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync, chmodSync } from 'node:fs';
import { createServer } from 'node:net';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => (a.startsWith('--') ? [...acc, [a.slice(2), all[i + 1]?.startsWith('--') || all[i + 1] === undefined ? true : all[i + 1]]] : acc), []));
if (!args.install) { console.error('usage: installed-flow.mjs --install <folder> [--work <dir>] [--spy <acquire-spy.mjs>]'); process.exit(2); }
const install = resolve(args.install);
const isWin = process.platform === 'win32';
const node = join(install, 'node', isWin ? 'node.exe' : 'node');
const hostEntry = join(install, 'host', 'server', 'index.mjs');
const controlDir = join(install, 'control');
const python = isWin ? join(controlDir, 'python', 'python.exe') : join(controlDir, 'python', 'bin', 'python3');
const real = !!args.components;
const modelId = String(args.model ?? 'stub');
const work = resolve(args.work ?? mkdtempSync(join(tmpdir(), 'strata-installed-flow-')));
mkdirSync(work, { recursive: true });
const dirs = { components: real ? resolve(args.components) : join(work, 'components'), models: real ? resolve(args.models) : join(work, 'models'), profiles: join(work, 'profiles'), runtime: join(work, 'runtime'), home: join(work, 'home') };
// the runtime and profile folders are created by Control itself, with the owner-only ACL it insists on
for (const [name, d] of Object.entries(dirs)) if (!['runtime', 'profiles'].includes(name) && !(real && ['components', 'models'].includes(name))) mkdirSync(d, { recursive: true });
const steps = [];
const record = (name, ok, detail = '') => { steps.push({ name, ok: !!ok, detail: String(detail) }); console.log(`${ok ? 'ok ' : 'FAIL'} ${name}${detail ? ' ' + detail : ''}`); };
const delay = ms => new Promise(r => setTimeout(r, ms));
const freePort = async () => { const s = createServer(); await new Promise(r => s.listen(0, '127.0.0.1', r)); const p = s.address().port; await new Promise(r => s.close(r)); return p; };
const sha = buf => createHash('sha256').update(buf).digest('hex');
// ------------------------------------------------------------------------------ fixtures
const baseEnv = {
...process.env,
STRATA_COMPONENTS_DIR: dirs.components, STRATA_PROFILE_DIR: dirs.profiles, STRATA_RUNTIME_DIR: dirs.runtime,
STRATA_INSTALL_DIR: dirs.models, STRATA_ENGINE_BACKEND: String(args.backend ?? 'cuda'), STRATA_HOME: dirs.home, STRATA_MODELS_DIR: real ? join(work, 'models-unused') : dirs.models,
APPDATA: join(work, 'appdata'), XDG_CONFIG_HOME: join(work, 'xdg'), PYTHONDONTWRITEBYTECODE: '1',
};
delete baseEnv.STRATA_API_KEY;
mkdirSync(baseEnv.APPDATA, { recursive: true });
const mark = join(work, 'engine-ran.json');
const FAKE_ENGINE = `import os, sys, json
from pathlib import Path
Path(os.environ['FAKE_ENGINE_MARK']).write_text(json.dumps({'pid': os.getpid(), 'script': os.path.abspath(__file__), 'argv': sys.argv[1:]}))
print('READY 4096 stop', flush=True)
for line in sys.stdin:
if line.strip() == 'QUIT': break
if line.startswith(('GEN ', 'BGEN ')):
slot = int(line.split()[1]) if line.startswith('BGEN ') else None
print('T 97', flush=True)
print('DONE 1 1 1.0 1.0 length 0 0 0', flush=True)
if slot is not None: print(f'BADM {slot} 0', flush=True)
`;
function installFakeEngine() {
const platform = isWin ? 'windows-x86_64' : 'linux-x86_64';
const folder = join(dirs.components, 'engine', `0.1.45-${platform}-cuda`);
mkdirSync(folder, { recursive: true });
writeFileSync(join(folder, 'engine_main.py'), FAKE_ENGINE);
const entry = isWin ? 'engine.cmd' : 'engine';
writeFileSync(join(folder, entry), isWin
? `@echo off\r\n"${python}" "%~dp0engine_main.py" %*\r\n`
: `#!/bin/sh\nexec "${python}" "$(dirname "$0")/engine_main.py" "$@"\n`);
if (!isWin) chmodSync(join(folder, entry), 0o755);
const files = [entry, 'engine_main.py'].map(p => { const b = readFileSync(join(folder, p)); return { path: p, sha256: sha(b), bytes: b.length }; });
writeFileSync(join(folder, 'engine-component.json'), JSON.stringify({ schema_version: 1, id: 'strata-engine', version: '0.1.45', platform, backend: 'cuda', entrypoint: entry, files }));
return folder;
}
function makeTokenizerAndConfig() {
const tok = join(dirs.models, 'tokenizer');
mkdirSync(tok, { recursive: true });
const script = `import json, sys\nfrom tools.strata_tokenizer import BYTE_TO_UNICODE\ntok = sys.argv[1]\ntokens = [BYTE_TO_UNICODE[i] for i in range(256)] + ['ab', '<|im_start|>', '<|im_end|>', '<|endoftext|>']\njson.dump({t: i for i, t in enumerate(tokens)}, open(tok + '/vocab.json', 'w'))\nopen(tok + '/merges.txt', 'w').write('a b')\njson.dump([0] * 257 + [3, 3, 3], open(tok + '/token_type.json', 'w'))\n`;
const done = spawnSync(python, ['-c', script, tok], { cwd: controlDir, env: { ...baseEnv, PYTHONNOUSERSITE: '1' }, encoding: 'utf8' });
if (done.status !== 0) throw new Error('tokenizer fixture failed: ' + done.stderr);
writeFileSync(join(dirs.models, 'strata-stub.json'), JSON.stringify({ exe: 'component:strata-engine', backend: 'cuda', tokenizer: tok, args: [], cwd: work, model_name: 'stub', host: '127.0.0.1', port: 0 }));
}
const control = (...argv) => spawnSync(python, [join(controlDir, 'strata.py'), ...argv, '--output', 'json'], { cwd: controlDir, env: { ...baseEnv, PYTHONNOUSERSITE: '1', STRATA_NO_ACQUIRE: '1', FAKE_ENGINE_MARK: mark }, encoding: 'utf8', timeout: 120000 });
const controlJson = (...argv) => { const r = control(...argv); let json = null; try { json = JSON.parse(r.stdout.slice(r.stdout.indexOf('{'))); } catch { /* not json */ } return { code: r.status, json, text: r.stdout + r.stderr }; };
const alive = pid => { try { process.kill(pid, 0); return true; } catch { return false; } };
// ------------------------------------------------------------------------------ the host
let host = null, port = 0, base = '', spyLog = null, shutdownToken = randomBytes(32).toString('hex');
async function startHost() {
port = await freePort(); base = `http://127.0.0.1:${port}`;
const hostArgs = [...(args.spy ? ['--import', pathToFileURL(resolve(args.spy)).href] : []), hostEntry];
if (args.spy) { spyLog = join(work, 'acquire-spy.log'); writeFileSync(spyLog, ''); }
host = spawn(node, hostArgs, {
cwd: join(install, 'host'), stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true,
env: { ...baseEnv, STRATA_PORT: String(port), STRATA_DIST: join(install, 'host', 'dist'), NODE_ENV: 'production', STRATA_PACKAGED: '1', STRATA_REAL_ENGINE: '1',
STRATA_NO_ACQUIRE: '1', STRATA_SHUTDOWN_TOKEN: shutdownToken, FAKE_ENGINE_MARK: mark, STRATA_SHARE_PORT: String(await freePort()), STRATA_TUNNEL_PORT: String(await freePort()),
...(spyLog ? { ACQUIRE_SPY_LOG: spyLog } : {}) },
});
let log = '';
host.stdout.on('data', c => { log += c; }); host.stderr.on('data', c => { log += c; });
host.hostLog = () => log;
for (let i = 0; i < 200; i++) { try { if ((await fetch(base + '/api/health')).ok) return true; } catch { /* not yet */ } await delay(150); }
return false;
}
/** The host started the way the Tauri shell starts it, by a stand-in shell (a node process) that can then be killed hard. */
async function startHostViaShell() {
port = await freePort(); base = `http://127.0.0.1:${port}`;
const spec = { command: node, args: [hostEntry], options: {
cwd: join(install, 'host'), stdio: 'ignore', windowsHide: true,
env: { ...baseEnv, STRATA_PORT: String(port), STRATA_DIST: join(install, 'host', 'dist'), NODE_ENV: 'production', STRATA_PACKAGED: '1', STRATA_REAL_ENGINE: '1',
STRATA_NO_ACQUIRE: '1', STRATA_SHUTDOWN_TOKEN: shutdownToken, STRATA_EXIT_WITH_PARENT: '1', FAKE_ENGINE_MARK: mark,
STRATA_SHARE_PORT: String(await freePort()), STRATA_TUNNEL_PORT: String(await freePort()) } } };
const shell = spawn(node, ['-e', "const s=JSON.parse(process.env.HOST_SPEC);const c=require('node:child_process').spawn(s.command,s.args,s.options);console.log(c.pid);setInterval(()=>{},1000);"],
{ env: { ...process.env, HOST_SPEC: JSON.stringify(spec) }, stdio: ['ignore', 'pipe', 'inherit'], windowsHide: true });
const hostPid = await new Promise(r => shell.stdout.once('data', c => r(Number(String(c).trim()))));
for (let i = 0; i < 200; i++) { try { if ((await fetch(base + '/api/health')).ok) return { shell, hostPid }; } catch { /* not yet */ } await delay(150); }
return { shell, hostPid, failed: true };
}
const call = async (path, method = 'GET', body) => {
const response = await fetch(base + path, { method, headers: { 'content-type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body) });
return { status: response.status, body: await response.json().catch(() => null) };
};
async function waitJob(id, seconds = 120) {
for (let i = 0; i < seconds * 5; i++) {
const state = (await call('/api/native')).body;
const job = state.jobs.find(j => j.id === id);
if (job && job.state !== 'running') return { job, state };
await delay(200);
}
throw new Error('operation did not finish');
}
async function stopHost(hard = false) {
if (!host || host.exitCode !== null) return;
if (hard) { if (isWin) spawnSync('taskkill', ['/PID', String(host.pid), '/T', '/F'], { windowsHide: true }); else host.kill('SIGKILL'); }
else host.kill();
for (let i = 0; i < 100 && host.exitCode === null; i++) await delay(100);
}
async function main() {
for (const need of [node, hostEntry, python, join(controlDir, 'strata.py')]) if (!existsSync(need)) { record('installed layout', false, `missing ${need}`); return; }
record('installed layout', true, install);
if (real) {
const packages = existsSync(join(dirs.components, 'engine')) ? readdirSync(join(dirs.components, 'engine')) : [];
record('fixtures', packages.length > 0 && existsSync(join(dirs.models, `strata-${modelId}.json`)), `engine package(s) ${packages.join(', ')} in ${dirs.components}; run config strata-${modelId}.json in ${dirs.models}`);
} else {
installFakeEngine();
makeTokenizerAndConfig();
record('fixtures', true, `stub engine package in ${dirs.components}; run config strata-stub.json`);
}
if (!(await startHost())) { record('host healthy', false, host.hostLog().slice(-600)); return; }
record('host healthy', true, base);
const status = (await call('/api/control/status')).body;
record('Control status: bundled, engine usable, acquiring forbidden', status.control?.python?.startsWith(controlDir) && status.engine?.usable === true && status.acquisition?.forbidden === true,
`python=${status.control?.python}; engine=${status.engine?.engine?.version}; forbidden=${status.acquisition?.forbidden}`);
const native = (await call('/api/native?refresh=1')).body;
const model = native.installed?.models?.find(m => m.model === modelId);
record('models come from Control', native.available && model?.ready === true && native.packaged === true, `available=${native.available}; model ready=${model?.ready}; ${native.summary ?? native.error}`);
// ---- load
const started = (await call('/api/native/operations', 'POST', { kind: 'load', args: { model: modelId } })).body;
const loaded = await waitJob(started.id, 400);
const instance = loaded.state.instances[0];
record('load through the host starts the engine under Control', loaded.job.state === 'done' && instance?.status === 'ready', `job=${loaded.job.state} ${loaded.job.error ?? ''}; instance=${instance?.status}`);
let ran = existsSync(mark) ? JSON.parse(readFileSync(mark, 'utf8')) : null;
if (real) {
const listed = spawnSync('ps', ['-eo', 'pid,args'], { encoding: 'utf8' }).stdout.split('\n').filter(l => l.includes(`${dirs.components}/engine/`) && l.includes('--serve'));
ran = listed.length ? { pid: Number(listed[0].trim().split(/\s+/)[0]), script: listed[0].trim().split(/\s+/)[1] } : null;
record('the engine process running is the binary from the component folder', !!ran, listed[0] ? listed[0].trim().slice(0, 160) : 'no engine process under the component folder');
} else {
record('the engine that ran is the one in the component folder', ran && ran.script.toLowerCase().startsWith(dirs.components.toLowerCase()), ran?.script ?? 'engine never ran');
}
const runtime = controlJson('runtime', 'status', '--profile', 'app-1');
record('a second client sees the same owner and instance (ownership)', runtime.json?.state === 'ready' && !!runtime.json.owner_id && !!runtime.json.instance_id && runtime.json.host_pid !== host.pid,
`state=${runtime.json?.state}; owner=${String(runtime.json?.owner_id).slice(0, 8)}; instance=${String(runtime.json?.instance_id).slice(0, 8)}; owner host pid ${runtime.json?.host_pid} is not the app host (${host.pid})`);
// ---- answer
const chat = await call('/v1/chat/completions', 'POST', real
? { model: instance?.apiModelId ?? modelId, messages: [{ role: 'user', content: 'Reply with exactly one word: pong' }], max_tokens: 200, temperature: 0, reasoning_effort: 'none', stream: false }
: { model: instance?.apiModelId ?? modelId, messages: [{ role: 'user', content: 'hi' }], max_tokens: 4, stream: false });
const message = chat.body?.choices?.[0]?.message ?? {};
const answer = `${message.content ?? ''}${message.reasoning_content ?? ''}`; // the stub emits token 97 ("a"); the server files it as reasoning
record(real ? 'the real model answers through the host (the answer text is checked)' : 'the engine answers through the host (the stub token, not an error)',
chat.status === 200 && (real ? /pong/i.test(answer) : answer === 'a'), `status=${chat.status}; answer=${JSON.stringify(answer.slice(0, 200))}${chat.status === 200 ? '' : ' ' + JSON.stringify(chat.body).slice(0, 160)}`);
// ---- unload
const unloaded = await call(`/api/native/instances/${instance.id}/unload`, 'POST', {});
await delay(500);
const after = controlJson('runtime', 'status', '--profile', 'app-1');
for (let i = 0; i < 100 && ran && alive(ran.pid); i++) await delay(100); // a real engine takes a moment to release the GPU
record('unload stops the engine', unloaded.status === 200 && after.json?.state === 'stopped' && (!ran || !alive(ran.pid)), `status=${unloaded.status}; runtime=${after.json?.state}; engine pid ${ran?.pid} alive=${ran ? alive(ran.pid) : 'n/a'}`);
// ---- no engine for this PC
const hidden = join(dirs.components, real ? 'engine.hidden-by-flow' : 'engine.gone');
renameSync(join(dirs.components, 'engine'), hidden);
rmSync(mark, { force: true });
const missing = await waitJob((await call('/api/native/operations', 'POST', { kind: 'load', args: { model: modelId } })).body.id);
record('no engine for this PC is a notice and nothing is started', missing.job.state === 'error' && /No engine for this PC/.test(missing.job.error) && /never downloads/.test(missing.job.error) && !existsSync(mark),
`${missing.job.state}: ${String(missing.job.error).slice(0, 160)}`);
const refused = await waitJob((await call('/api/native/operations', 'POST', { kind: 'install', args: { family: 'qwen', model: 'IQ2_XS' } })).body.id);
record('installing from the app is refused (Terminal only)', refused.job.state === 'error' && /Terminal/.test(refused.job.error), refused.job.error);
const binary = await call('/api/engine/binary', 'POST');
record('the llama.cpp download route is refused', binary.status === 409 && /never downloads/.test(binary.body?.error ?? ''), `${binary.status} ${binary.body?.error}`);
// ---- clean shutdown (what the Tauri shell does on exit)
renameSync(hidden, join(dirs.components, 'engine'));
rmSync(mark, { force: true });
const again = await waitJob((await call('/api/native/operations', 'POST', { kind: 'load', args: { model: modelId } })).body.id, 400);
let ran2 = existsSync(mark) ? JSON.parse(readFileSync(mark, 'utf8')) : null;
if (real) {
const listed = spawnSync('ps', ['-eo', 'pid,args'], { encoding: 'utf8' }).stdout.split('\n').filter(l => l.includes(`${dirs.components}/engine/`) && l.includes('--serve'));
ran2 = listed.length ? { pid: Number(listed[0].trim().split(/\s+/)[0]) } : null;
}
record('load again after the engine package returns', again.job.state === 'done', again.job.error ?? '');
const wrong = await fetch(base + '/api/control/shutdown', { method: 'POST', headers: { 'x-strata-shutdown': 'nope' } });
record('shutdown without the shell\'s secret is refused', wrong.status === 403, String(wrong.status));
const exited = new Promise(r => host.once('exit', code => r(code)));
const stopping = await fetch(base + '/api/control/shutdown', { method: 'POST', headers: { 'x-strata-shutdown': shutdownToken } });
const code = await Promise.race([exited, delay(90000).then(() => 'timeout')]);
for (let i = 0; i < 100 && ran2 && alive(ran2.pid); i++) await delay(100);
const final = controlJson('runtime', 'status', '--profile', 'app-1');
record('clean shutdown stops the engine the app started and exits', stopping.status === 202 && code === 0 && final.json?.state === 'stopped' && (!ran2 || !alive(ran2.pid)), `status=${stopping.status}; host exit=${code}; runtime=${final.json?.state}; engine alive=${ran2 ? alive(ran2.pid) : 'n/a'}`);
// ---- hard kill: an engine whose app host dies ends with it (the owner follows the host: runtime start --die-with-pid)
if (!(await startHost())) { record('host restarts', false, host.hostLog().slice(-400)); return; }
await call('/api/native?refresh=1');
rmSync(mark, { force: true });
const third = await waitJob((await call('/api/native/operations', 'POST', { kind: 'load', args: { model: modelId } })).body.id, 400);
let ran3 = existsSync(mark) ? JSON.parse(readFileSync(mark, 'utf8')) : null;
if (real) {
const listed = spawnSync('ps', ['-eo', 'pid,args'], { encoding: 'utf8' }).stdout.split('\n').filter(l => l.includes(`${dirs.components}/engine/`) && l.includes('--serve'));
ran3 = listed.length ? { pid: Number(listed[0].trim().split(/\s+/)[0]) } : null;
record('(real) the engine is up before the host is killed', !!ran3, listed[0] ? listed[0].trim().slice(0, 120) : 'no engine process');
}
await stopHost(true);
for (let i = 0; i < 600 && ran3 && alive(ran3.pid); i++) await delay(100); // the owner notices within a second, drains, stops
const orphan = controlJson('runtime', 'status', '--profile', 'app-1');
record('a hard-killed app host takes its engine with it', third.job.state === 'done' && (!ran3 || !alive(ran3.pid)), `engine alive=${ran3 ? alive(ran3.pid) : 'n/a'}; runtime=${orphan.json?.state ?? orphan.json?.error?.message}`);
const cleanup = controlJson('runtime', 'stop', '--profile', 'app-1');
record('nothing is left to stop afterwards', /stopped|not managed/.test(JSON.stringify(cleanup.json)) && controlJson('runtime', 'status', '--profile', 'app-1').json?.state === 'stopped', JSON.stringify(cleanup.json).slice(0, 160));
// ---- the shell itself is killed (what the desktop shell's crash or kill looks like to the host): the host leaves, and
// its engine with it, with nothing announced. Windows also has the job object; this is the path that works everywhere.
const started2 = await startHostViaShell();
if (started2.failed) { record('host starts under a stand-in shell', false, ''); try { started2.shell.kill('SIGKILL'); } catch { /* gone */ } }
else {
await call('/api/native?refresh=1');
rmSync(mark, { force: true });
const fourth = await waitJob((await call('/api/native/operations', 'POST', { kind: 'load', args: { model: modelId } })).body.id, 400);
let ran4 = existsSync(mark) ? JSON.parse(readFileSync(mark, 'utf8')) : null;
if (real) {
const listed = spawnSync('ps', ['-eo', 'pid,args'], { encoding: 'utf8' }).stdout.split('\n').filter(l => l.includes(`${dirs.components}/engine/`) && l.includes('--serve'));
ran4 = listed.length ? { pid: Number(listed[0].trim().split(/\s+/)[0]) } : null;
}
if (isWin) spawnSync('taskkill', ['/PID', String(started2.shell.pid), '/F'], { windowsHide: true }); else started2.shell.kill('SIGKILL');
for (let i = 0; i < 100 && alive(started2.hostPid); i++) await delay(100);
const hostGone = !alive(started2.hostPid);
for (let i = 0; i < 900 && ran4 && alive(ran4.pid); i++) await delay(100);
const engineGone = !ran4 || !alive(ran4.pid);
record('a killed shell takes the host and its engine with it', fourth.job.state === 'done' && hostGone && engineGone,
`host pid ${started2.hostPid} gone=${hostGone}; engine pid ${ran4?.pid} gone=${engineGone}; runtime=${controlJson('runtime', 'status', '--profile', 'app-1').json?.state}`);
controlJson('runtime', 'stop', '--profile', 'app-1');
}
await finishSpy();
}
async function finishSpy() {
if (spyLog) {
const attempts = readFileSync(spyLog, 'utf8').split('\n').filter(Boolean);
record('no acquisition attempt by the host during the whole run (spy)', attempts.length === 0, attempts.join(' | '));
}
}
try { await main(); }
catch (error) { record('flow', false, error.stack ?? error.message); }
finally {
try { const hid = join(dirs.components, 'engine.hidden-by-flow'); if (real && existsSync(hid) && !existsSync(join(dirs.components, 'engine'))) renameSync(hid, join(dirs.components, 'engine')); } catch { /* reported by the run */ }
await stopHost(true);
controlJson('runtime', 'stop', '--profile', 'app-1');
controlJson('runtime', 'stop', '--profile', 'app-2');
}
const failed = steps.filter(s => !s.ok);
writeFileSync(join(work, 'installed-flow.json'), JSON.stringify({ ok: failed.length === 0, install, work, steps }, null, 1));
console.log(`\n${steps.length - failed.length}/${steps.length} ok; report ${join(work, 'installed-flow.json')}`);
if (!args.keep && !args.work && failed.length === 0) { try { rmSync(work, { recursive: true, force: true }); } catch { /* leave it */ } }
process.exit(failed.length ? 1 : 0);
@@ -0,0 +1,73 @@
#!/usr/bin/env node
/**
* Mock-ups of the Windows installer in Strata's own look: four screens (welcome, where, installing, done), light and dark, drawn with
* the app's built CSS (apps/host/dist) and brand files, then photographed with Chrome. Nothing here is built or shipped: it is what
* the owner is shown before an installer is chosen.
*
* node apps/desktop/scripts/installer-mockups.mjs [--out <folder>]
*/
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => (a.startsWith('--') ? [...acc, [a.slice(2), all[i + 1]]] : acc), []));
const here = dirname(fileURLToPath(import.meta.url));
const host = resolve(here, '../../host');
const out = resolve(args.out ?? join(here, '../installer-mockups'));
mkdirSync(out, { recursive: true });
const dist = join(host, 'dist');
const css = readdirSync(join(dist, 'assets')).filter(f => f.endsWith('.css')).map(f => readFileSync(join(dist, 'assets', f), 'utf8')).join('\n');
const logo = theme => readFileSync(join(dist, 'brand', `strata-lockup-${theme}.svg`), 'utf8');
const { chromium } = await import(pathToFileURL(join(host, 'node_modules/playwright-core/index.mjs')).href).catch(() => import('playwright-core'));
const CHROME = process.env.CHROME ?? ['C:/Program Files/Google/Chrome/Application/chrome.exe', '/usr/bin/google-chrome'].find(existsSync);
const check = '<svg class="st-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>';
const folder = '<svg class="st-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>';
const screens = {
welcome: theme => `<header class="onboard-header"><span class="mock-logo">${logo(theme)}</span></header>
<div class="onboard-welcome-copy"><h1>State of the art. At home.</h1><p>Strata runs AI on your own computer.<br>Setting it up takes a few minutes.</p>
<div class="onboard-benefits"><span>${check}Private</span><span>${check}Fast</span><span>${check}Easy to use</span></div></div>
<footer class="onboard-footer onboard-footer--welcome"><button class="st-btn st-btn--primary">Install Strata</button><small>Installs for you only. No administrator needed.</small></footer>`,
where: theme => `<header class="onboard-header"><span class="mock-logo">${logo(theme)}</span></header>
<div class="mock-body"><h2 class="mock-title">Where should Strata go?</h2><p class="mock-sub">This is the right place for most people.</p>
<div class="onboard-folder">${folder}<div><strong>Your Strata folder</strong><small>C:\\Users\\Alex\\AppData\\Local\\Strata</small></div><button class="st-btn st-btn--sm">Change</button></div>
<p class="mock-note">About 310 MB. Your models and chats are kept somewhere else and are never removed by an update.</p></div>
<footer class="onboard-footer"><button class="st-btn st-btn--ghost">Back</button><span></span><button class="st-btn st-btn--primary">Install</button></footer>`,
installing: theme => `<header class="onboard-header"><span class="mock-logo">${logo(theme)}</span></header>
<div class="mock-body"><h2 class="mock-title">Installing Strata…</h2><p class="mock-sub">Copying the app and its engine room. You can keep working meanwhile.</p>
<div class="onboard-progress" style="--p:62%"><progress max="100" value="62"></progress></div>
<div class="onboard-progress-meta"><span>62% · 190 / 310 MB</span><div class="onboard-progress-actions"><span>About 1 min left</span><button>Cancel</button></div></div></div>
<footer class="onboard-footer"><span></span><span class="mock-hint">Next: choose your first model inside Strata.</span><button class="st-btn st-btn--primary" disabled>Installing…</button></footer>`,
done: theme => `<header class="onboard-header"><span class="mock-logo">${logo(theme)}</span></header>
<div class="onboard-welcome-copy"><h1>Strata is ready.</h1><p>Open it to pick your first model. Strata downloads it for you and gets it ready.</p></div>
<footer class="onboard-footer onboard-footer--welcome"><button class="st-btn st-btn--primary">Open Strata</button><small>A shortcut was added to your Start menu and desktop.</small></footer>`,
};
const page = (name, theme) => `<!doctype html><html data-theme="${theme}"><head><meta charset="utf-8"><style>${css}
body{background:${theme === 'dark' ? '#0b0d0e' : '#e9edef'};margin:0;display:grid;place-items:center;min-height:100vh}
.mock-window{width:720px;border-radius:14px;overflow:hidden;background:var(--st-surface,#fff);border:1px solid var(--st-line,#dde);box-shadow:0 30px 80px #0003}
.mock-titlebar{height:34px;display:flex;align-items:center;justify-content:space-between;padding:0 14px;font-size:12px;color:var(--st-ink-muted);background:var(--st-surface-2,#f3f5f6);border-bottom:1px solid var(--st-line-soft,#e6e9eb)}
.mock-logo svg{height:22px;width:auto;display:block}
.mock-body{padding:8px 36px 28px}.mock-title{font-size:28px;margin:16px 0 8px}.mock-sub{color:var(--st-ink-muted);margin:0 0 22px;line-height:1.6}
.mock-note{color:var(--st-ink-muted);font-size:12px;margin-top:16px;line-height:1.6}.mock-hint{font-size:12px;color:var(--st-ink-muted)}
.onboard-progress progress{width:100%}
.guided-onboarding{min-height:430px;display:flex;flex-direction:column}.onboard-footer{margin-top:auto}
</style></head><body><div class="mock-window"><div class="mock-titlebar"><span>Strata setup</span><span>– ☐ ✕</span></div>
<div class="setup guided-onboarding" data-step="${name === 'welcome' || name === 'done' ? 1 : 2}">${screens[name](theme)}</div></div></body></html>`;
const browser = await chromium.launch({ executablePath: CHROME, headless: true });
const written = [];
for (const theme of ['light', 'dark']) {
for (const name of Object.keys(screens)) {
const tab = await browser.newPage({ viewport: { width: 860, height: 620 }, deviceScaleFactor: 1 });
await tab.setContent(page(name, theme));
await tab.waitForTimeout(250);
const file = join(out, `${name}-${theme}.png`);
await tab.screenshot({ path: file });
written.push(file);
await tab.close();
}
}
await browser.close();
console.log(written.join('\n'));
+126
View File
@@ -0,0 +1,126 @@
#!/usr/bin/env node
/**
* "Use your own GGUF" end to end on a box, through the installed app layout (packaged host + bundled Control + a real engine component):
* preview a folder of a supported model, preview an unsupported GGUF (must be refused in plain terms), prepare the supported one
* (pack, from the folder, nothing copied), load it, get a chat answer, unload, shut down, and check nothing is left running.
*
* node local-gguf-e2e.mjs --root /data/ui4-e2e --good /data/141-gyro/models/IQ2_XS --bad /data/ui4-e2e/bad [--context 8192] [--backend cuda|metal]
* (the app folder is <root>/app: node/, host/, control/ ...; <root>/components gets the engine, from <root>/app/engine-bundle when the app carries one)
*/
import { spawn, spawnSync } from 'node:child_process';
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { createServer } from 'node:net';
import { join, resolve } from 'node:path';
const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => (a.startsWith('--') ? [...acc, [a.slice(2), all[i + 1]]] : acc), []));
const root = resolve(args.root);
const good = args.good, bad = args.bad;
const context = Number(args.context ?? 8192);
const app = join(root, 'app');
const dirs = { components: join(root, 'components'), install: join(root, 'install'), profiles: join(root, 'profiles'), runtime: join(root, 'runtime'), home: join(root, 'home'), xdg: join(root, 'xdg'), appdata: join(root, 'appdata') };
for (const d of [dirs.install, dirs.home, dirs.xdg, dirs.appdata]) mkdirSync(d, { recursive: true });
const delay = ms => new Promise(r => setTimeout(r, ms));
const steps = [];
let failed = false;
const record = (name, ok, detail = '') => { steps.push({ name, ok: !!ok, detail: String(detail).slice(0, 400) }); console.log(`${ok ? 'ok ' : 'FAIL'} ${name}${detail ? ' ' + String(detail).slice(0, 300) : ''}`); if (!ok) failed = true; };
const freePort = async () => { const s = createServer(); await new Promise(r => s.listen(0, '127.0.0.1', r)); const p = s.address().port; await new Promise(r => s.close(r)); return p; };
const env = { ...process.env, STRATA_COMPONENTS_DIR: dirs.components, STRATA_PROFILE_DIR: dirs.profiles, STRATA_RUNTIME_DIR: dirs.runtime, STRATA_INSTALL_DIR: dirs.install, STRATA_HOME: dirs.home, STRATA_MODELS_DIR: join(root, 'Strata-data'), STRATA_ENGINE_BACKEND: args.backend ?? 'cuda', XDG_CONFIG_HOME: dirs.xdg, APPDATA: dirs.appdata, PYTHONDONTWRITEBYTECODE: '1' };
delete env.STRATA_API_KEY;
const port = await freePort();
const base = `http://127.0.0.1:${port}`;
const host = spawn(join(app, 'node', 'node'), [join(app, 'host', 'server', 'index.mjs')], { cwd: join(app, 'host'), stdio: ['ignore', 'pipe', 'pipe'], detached: false,
env: { ...env, STRATA_PORT: String(port), STRATA_DIST: join(app, 'host', 'dist'), NODE_ENV: 'production', STRATA_PACKAGED: '1', STRATA_REAL_ENGINE: '1', STRATA_NO_ACQUIRE: '1', STRATA_EXIT_WITH_PARENT: '1',
STRATA_SHARE_PORT: String(await freePort()), STRATA_TUNNEL_PORT: String(await freePort()), STRATA_WORKSPACE: join(root, 'workspace') } });
mkdirSync(join(root, 'workspace'), { recursive: true });
let hostLog = ''; host.stdout.on('data', c => { hostLog += c; }); host.stderr.on('data', c => { hostLog += c; });
const call = async (path, method = 'GET', body) => { const r = await fetch(base + path, { method, headers: { 'content-type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body) }); return { status: r.status, body: await r.json().catch(() => null) }; };
async function waitJob(id, seconds) {
const until = Date.now() + seconds * 1000;
let last = '';
while (Date.now() < until) {
const state = (await call('/api/native')).body;
const job = state.jobs.find(j => j.id === id);
if (job && job.state !== 'running') return { job, state };
if (job && job.summary !== last) { last = job.summary; console.log(' ...', String(job.summary).slice(0, 140)); }
await delay(2000);
}
throw new Error('operation did not finish in time');
}
const engineProcs = () => spawnSync('ps', ['-eo', 'pid,args'], { encoding: 'utf8' }).stdout.split('\n').filter(l => l.includes(join(dirs.components, 'engine')) && /\/strata( |$)/.test(l));
async function main() {
let up = false;
for (let i = 0; i < 200 && !up; i++) { try { up = (await fetch(base + '/api/health')).ok; } catch { await delay(150); } }
record('host healthy', up, up ? base : hostLog.slice(-500));
if (!up) return;
let status = (await call('/api/control/status')).body;
for (let i = 0; i < 180 && status?.engineSetup; i++) { await delay(1000); status = (await call('/api/control/status')).body; } // the engine that came with the app is copied on the first start
record('Control status: bundled, engine usable', status?.engine?.usable === true, `engine=${status?.engine?.engine?.version} ${status?.engine?.notice ?? ''}`);
await call('/api/native?refresh=1');
// ---- the unsupported GGUF: a plain refusal
const badPreview = await call('/api/native/local/preview', 'POST', { path: bad });
const badIssues = badPreview.body?.preview?.issues ?? badPreview.body?.issues ?? [];
const badStatus = badPreview.body?.preview?.file_set_status;
console.log(' bad preview:', badPreview.status, JSON.stringify(badPreview.body).slice(0, 400));
record('an unsupported GGUF is refused, nothing adopted', badPreview.status === 200 ? (badStatus !== 'plausible' || !badPreview.body.preview.candidate_selection) : badPreview.status >= 400, `status=${badPreview.status}; file_set_status=${badStatus}; issues=${JSON.stringify(badIssues)}`);
// ---- the supported folder
const goodPreview = await call('/api/native/local/preview', 'POST', { path: good });
const selection = goodPreview.body?.preview?.candidate_selection;
console.log(' good preview:', JSON.stringify(goodPreview.body).slice(0, 500));
record('a folder of a supported model is recognised', goodPreview.status === 200 && goodPreview.body?.preview?.file_set_status === 'plausible' && !!selection, `family=${selection?.family}; model=${selection?.model}; files=${goodPreview.body?.preview?.file_count}`);
if (!selection || args.only === 'preview') return;
// ---- prepare (what the page's button does): install from the folder, nothing copied or downloaded
const native0 = (await call('/api/native?refresh=1')).body;
const catalogSize = native0.catalog?.families?.find(f => f.family === selection.family)?.sizes.find(s => s.model === selection.model);
record('the catalog lists the recognised version', !!catalogSize, catalogSize ? `id=${catalogSize.id}` : `no ${selection.family}/${selection.model} in the catalog`);
const started = await call('/api/native/operations', 'POST', { kind: 'install', args: { family: selection.family, model: selection.model, gguf_dir: good, context, vision: 'no' } });
record('prepare starts', started.status < 300 && !!started.body?.id, `status=${started.status} ${JSON.stringify(started.body).slice(0, 200)}`);
if (!started.body?.id) return;
const prepared = await waitJob(started.body.id, 3600);
record('prepare finishes', prepared.job.state === 'done', `${prepared.job.state}: ${prepared.job.error ?? prepared.job.summary}`);
if (prepared.job.state !== 'done') return;
const native1 = (await call('/api/native?refresh=1')).body;
const modelId = catalogSize?.id ?? `${selection.family === 'qwen' ? '' : selection.family + '-'}${String(selection.model).toLowerCase()}`;
const entry = native1.installed?.models?.find(m => m.model === modelId);
record('the model is ready and points at the folder, not a copy', entry?.ready === true, `model=${modelId}; ready=${entry?.ready}; dataFolder=${entry?.dataFolder}`);
if (!entry?.ready) return;
// ---- load
const load = await call('/api/native/operations', 'POST', { kind: 'load', args: { model: modelId } });
const loaded = await waitJob(load.body.id, 1800);
const instance = loaded.state.instances[0];
record('load starts the engine from the component folder', loaded.job.state === 'done' && instance?.status === 'ready', `job=${loaded.job.state} ${loaded.job.error ?? ''}; instance=${instance?.status}`);
if (instance?.status !== 'ready') return;
record('one engine process is running', engineProcs().length === 1, engineProcs().map(l => l.trim().slice(0, 140)).join(' | '));
// ---- a chat answer, checked
const chat = await fetch(base + '/api/chat/stream', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ sessionId: 'e2e-chat', message: 'Reply with exactly one word: pong', history: [], nativeModelId: instance.id, thinking: 'Off', agent: false, workspaceId: null }) });
let text = '', done = null;
for await (const chunk of chat.body) {
for (const line of Buffer.from(chunk).toString('utf8').split('\n')) {
if (!line.trim()) continue;
try { const f = JSON.parse(line); if (f.type === 'delta') text += f.text ?? ''; if (f.type === 'done') done = f; if (f.type === 'error') text += ` [error: ${f.message}]`; } catch { /* partial line */ }
}
}
record('the model answers through the app (the answer text is checked)', chat.status === 200 && /pong/i.test(text), `status=${chat.status}; answer=${JSON.stringify(text.slice(0, 120))}; tokensOut=${done?.tokensOut}; tps=${done?.tps}`);
// ---- unload, shutdown, nothing left
const unload = await call(`/api/native/instances/${instance.id}/unload`, 'POST');
await delay(3000);
record('unload stops the engine', unload.status < 300 && engineProcs().length === 0, `status=${unload.status}; engines left=${engineProcs().length}`);
}
try { await main(); } catch (e) { record('the flow', false, e?.stack ?? e); }
host.kill();
for (let i = 0; i < 100 && host.exitCode === null; i++) await delay(100);
if (host.exitCode === null) host.kill('SIGKILL');
await delay(2000);
const left = spawnSync('ps', ['-eo', 'pid,args'], { encoding: 'utf8' }).stdout.split('\n').filter(l => (l.includes(dirs.install) || l.includes(join(app, 'host'))) && !l.includes('ps -eo'));
record('nothing of this run is left running', left.length === 0, left.join(' | ').slice(0, 300));
writeFileSync(join(root, 'e2e-result.json'), JSON.stringify({ steps, hostLogTail: hostLog.slice(-2000) }, null, 1));
console.log(failed ? 'FAILED' : 'ALL OK');
process.exit(failed ? 1 : 0);
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
# Build the Strata DMG without a Finder session.
#
# Tauri's default DMG step runs an AppleScript that asks Finder to arrange the window (icon
# positions, background). Finder AppleEvents time out in a non-GUI or sandboxed context — observed
# here as `Finder got an error: AppleEvent timed out. (-1712)` — so the default path cannot be
# relied on for an automated build. The upstream `create-dmg` script already supports exactly this
# case via `--skip-jenkins`, which produces a plain, correct DMG with the app and the Applications
# drop link and no cosmetic arrangement.
#
# Usage: scripts/make-dmg.sh [output.dmg]
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DESKTOP="$(cd "$HERE/.." && pwd)"
BUNDLE="$DESKTOP/src-tauri/target/release/bundle"
APP="$BUNDLE/macos/Strata.app"
VERSION="$(grep -m1 '^version = ' "$DESKTOP/src-tauri/Cargo.toml" | cut -d'"' -f2)" # the one place the app version is written
OUT="${1:-$BUNDLE/dmg/Strata_${VERSION}_aarch64.dmg}"
if [ ! -d "$APP" ]; then
echo "error: $APP does not exist; run 'cargo tauri build --bundles app' first" >&2
exit 1
fi
WORK="$(mktemp -d -t strata-dmg)"
trap 'rm -rf "$WORK"' EXIT
# A staging folder is what create-dmg copies into the image: the app, and nothing else.
#
# The Applications drop link is created by `--app-drop-link` below. Adding one here as well makes
# create-dmg fail with "File exists", because it links into the volume it is assembling.
mkdir -p "$WORK/stage"
cp -R "$APP" "$WORK/stage/"
# Reuse Tauri's vendored create-dmg if the bundler left it behind, otherwise take it from the
# bundle cache. Building it yourself is not necessary and not expected.
SCRIPT="$BUNDLE/dmg/bundle_dmg.sh"
if [ ! -x "$SCRIPT" ]; then
echo "error: $SCRIPT not found; run 'cargo tauri build --bundles dmg' once to vendor it" >&2
exit 1
fi
mkdir -p "$(dirname "$OUT")"
rm -f "$OUT"
# --skip-jenkins: no Finder AppleScript. Everything else is the same code path Tauri uses.
"$SCRIPT" \
--volname "Strata $VERSION" \
--volicon "$BUNDLE/macos/Strata.app/Contents/Resources/Strata.icns" \
--app-drop-link 400 200 \
--skip-jenkins \
--no-internet-enable \
"$OUT" "$WORK/stage"
echo
echo "built: $OUT"
ls -lh "$OUT" | awk '{print "size: " $5}'
shasum -a 256 "$OUT"
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env node
/**
* The app and installer icon, refined from the existing Strata mark (apps/host/public/brand/strata-mark-*.svg and the current
* icons/icon.png): the same three rounded bars on the same dark tile, in the same colours. Nothing is redrawn. What changes is
* how it is cut for small sizes: at 16-48 px the bars are snapped to whole pixels and made a little bolder and wider so they
* stay crisp instead of blurring; from 64 px up the geometry is the mark's own proportions.
*
* node apps/desktop/scripts/make-icons.mjs [--out <folder>] writes icon-<size>.png for every size and icon.ico
*/
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => (a.startsWith('--') ? [...acc, [a.slice(2), all[i + 1]]] : acc), []));
const here = dirname(fileURLToPath(import.meta.url));
const host = resolve(here, '../../host');
const out = resolve(args.out ?? join(here, '../src-tauri/icons'));
mkdirSync(out, { recursive: true });
const { chromium } = await import(pathToFileURL(join(host, 'node_modules/playwright-core/index.mjs')).href).catch(() => import('playwright-core'));
const CHROME = process.env.CHROME ?? ['C:/Program Files/Google/Chrome/Application/chrome.exe', '/usr/bin/google-chrome'].find(existsSync);
const TILE = '#161a1d', LIGHT = '#eef1f3', GREEN = '#34d399'; // the colours of the current icon.png
/** Integer geometry for the small sizes: bars on whole pixels, centred. Returns [{x,y,w,h,fill}]. */
function hinted(size) {
const bar = Math.max(2, Math.round(size * 0.14)), gap = Math.max(2, Math.round(size * 0.1)), mid = bar + 1;
const total = 2 * bar + mid + 2 * gap, top = Math.floor((size - total) / 2);
const even = w => (w % 2 === size % 2 ? w : w + 1);
const wide = even(Math.round(size * 0.5)), long = even(Math.round(size * 0.76));
return [
{ x: (size - wide) / 2, y: top, w: wide, h: bar, fill: LIGHT },
{ x: (size - long) / 2, y: top + bar + gap, w: long, h: mid, fill: GREEN },
{ x: (size - wide) / 2, y: top + bar + gap + mid + gap, w: wide, h: bar, fill: LIGHT },
];
}
/** The mark's own proportions (from icon.png at 512): bars 34% / 50% / 34% wide, 9% high, on a tile with 21.5% corners. */
function proportional(size) {
const k = size / 512;
return [
{ x: 170 * k, y: 156 * k, w: 173 * k, h: 46 * k, fill: LIGHT },
{ x: 128 * k, y: 232 * k, w: 256 * k, h: 48 * k, fill: GREEN },
{ x: 170 * k, y: 309 * k, w: 173 * k, h: 46 * k, fill: LIGHT },
];
}
function svg(size) {
const small = size <= 48;
const bars = (small ? hinted(size) : proportional(size));
const radius = small ? Math.round(size * 0.22) : size * 0.215;
const rects = bars.map(b => `<rect x="${b.x}" y="${b.y}" width="${b.w}" height="${b.h}" rx="${small ? Math.min(b.h / 2, 1.5 + size / 32) : b.h / 2}" fill="${b.fill}"${small ? ' shape-rendering="crispEdges"' : ''}/>`).join('');
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}"><rect width="${size}" height="${size}" rx="${radius}" fill="${TILE}"/>${rects}</svg>`;
}
const SIZES = [16, 20, 24, 32, 40, 48, 64, 96, 128, 256, 512, 1024];
const browser = await chromium.launch({ executablePath: CHROME, headless: true });
const png = {};
for (const size of SIZES) {
const page = await browser.newPage({ viewport: { width: size, height: size }, deviceScaleFactor: 1 });
await page.setContent(`<!doctype html><body style="margin:0;background:transparent">${svg(size)}</body>`);
png[size] = await page.screenshot({ omitBackground: true, clip: { x: 0, y: 0, width: size, height: size } });
await page.close();
writeFileSync(join(out, `icon-${size}.png`), png[size]);
}
await browser.close();
// The .ico: one PNG image per size (16-256), the sizes Windows asks for at 100-250% scaling.
const ICO = [16, 20, 24, 32, 40, 48, 64, 96, 128, 256];
const head = Buffer.alloc(6); head.writeUInt16LE(0, 0); head.writeUInt16LE(1, 2); head.writeUInt16LE(ICO.length, 4);
let offset = 6 + 16 * ICO.length;
const entries = [], blobs = [];
for (const size of ICO) {
const data = png[size], e = Buffer.alloc(16);
e.writeUInt8(size === 256 ? 0 : size, 0); e.writeUInt8(size === 256 ? 0 : size, 1); e.writeUInt8(0, 2); e.writeUInt8(0, 3);
e.writeUInt16LE(1, 4); e.writeUInt16LE(32, 6); e.writeUInt32LE(data.length, 8); e.writeUInt32LE(offset, 12);
offset += data.length; entries.push(e); blobs.push(data);
}
writeFileSync(join(out, 'icon.ico'), Buffer.concat([head, ...entries, ...blobs]));
writeFileSync(join(out, 'icon.png'), png[512]);
console.log('icons in', out, ICO.join(','));
@@ -0,0 +1,66 @@
#!/usr/bin/env node
/**
* The installer's pictures, drawn from the app's own design: the light surface and ink colours of the app (src/styles tokens), the
* brand lockup and mark files, GT Walsheim Bold for the words (baked into the pictures, because NSIS cannot use a web font).
*
* node apps/desktop/scripts/make-installer-images.mjs [--out <folder>] [--preview <folder>]
*
* Writes sidebar.bmp (164x314, the welcome and finish pages) and header.bmp (150x57, the other pages) as 24-bit BMPs, and with
* --preview the same as PNGs at 3x.
*/
import { spawnSync } from 'node:child_process';
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { tmpdir } from 'node:os';
const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => (a.startsWith('--') ? [...acc, [a.slice(2), all[i + 1]]] : acc), []));
const here = dirname(fileURLToPath(import.meta.url));
const host = resolve(here, '../../host');
const out = resolve(args.out ?? join(here, '../src-tauri/nsis'));
mkdirSync(out, { recursive: true });
const { chromium } = await import(pathToFileURL(join(host, 'node_modules/playwright-core/index.mjs')).href).catch(() => import('playwright-core'));
const CHROME = process.env.CHROME ?? ['C:/Program Files/Google/Chrome/Application/chrome.exe', '/usr/bin/google-chrome'].find(existsSync);
const font = pathToFileURL(join(host, 'public/fonts/GTWalsheimPro-Bold.woff2')).href;
const fontMedium = pathToFileURL(join(host, 'public/fonts/GTWalsheimPro-Medium.woff2')).href;
const lockup = readFileSync(join(host, 'public/brand/strata-lockup-light.svg'), 'utf8');
const mark = readFileSync(join(host, 'public/brand/strata-mark-light.svg'), 'utf8');
// the app's light tokens
const SURFACE = '#f9fafb', INK = '#171b1e', MUTED = '#5b646b', ACCENT = '#10b981', LINE = '#e3e8ea';
const css = `@font-face{font-family:GT;font-weight:700;src:url(${font})}@font-face{font-family:GT;font-weight:500;src:url(${fontMedium})}
*{box-sizing:border-box}body{margin:0;font-family:GT,'Segoe UI',sans-serif;color:${INK};background:${SURFACE}}`;
const pages = {
sidebar: { w: 164, h: 314, html: `<div style="width:164px;height:314px;padding:22px 18px;display:flex;flex-direction:column;background:linear-gradient(180deg,#f9fafb 0%,#eef6f2 100%);border-right:1px solid ${LINE}">
<div style="height:26px">${lockup.replace('<svg', '<svg style="height:26px;width:auto;display:block"')}</div>
<div style="margin-top:auto">
<div style="font-weight:700;font-size:22px;line-height:1.12;letter-spacing:-.01em">State of<br>the art.<br>At home.</div>
<div style="margin-top:10px;font-weight:500;font-size:11px;line-height:1.5;color:${MUTED}">AI on your own computer.<br>Private. Fast. Easy.</div>
<div style="margin-top:16px;height:3px;width:34px;border-radius:2px;background:${ACCENT}"></div>
</div></div>` },
header: { w: 150, h: 57, html: `<div style="width:150px;height:57px;display:flex;align-items:center;justify-content:flex-end;padding-right:12px;background:${SURFACE}">${lockup.replace('<svg', '<svg style="height:22px;width:auto;display:block"')}</div>` },
};
const browser = await chromium.launch({ executablePath: CHROME, headless: true });
const work = join(tmpdir(), 'strata-installer-images'); mkdirSync(work, { recursive: true });
for (const [name, page] of Object.entries(pages)) {
for (const scale of [1, 3]) {
const file = join(work, `${name}.html`);
writeFileSync(file, `<!doctype html><meta charset="utf-8"><style>${css}</style><body>${page.html}</body>`);
const tab = await browser.newPage({ viewport: { width: page.w, height: page.h }, deviceScaleFactor: scale });
await tab.goto(pathToFileURL(file).href);
await tab.evaluate(() => document.fonts.ready);
await tab.waitForTimeout(200);
const png = join(work, `${name}@${scale}.png`);
await tab.screenshot({ path: png, clip: { x: 0, y: 0, width: page.w, height: page.h } });
await tab.close();
if (scale === 3 && args.preview) { mkdirSync(resolve(args.preview), { recursive: true }); writeFileSync(join(resolve(args.preview), `${name}.png`), readFileSync(png)); }
}
}
await browser.close();
// 24-bit BMP, the format NSIS wants: render at 3x, shrink with a good filter
const py = `import sys\nfrom PIL import Image\nfor name in ('sidebar','header'):\n im = Image.open(sys.argv[1] + '/' + name + '@3.png').convert('RGB')\n w, h = im.width // 3, im.height // 3\n im.resize((w, h), Image.LANCZOS).save(sys.argv[2] + '/' + name + '.bmp', 'BMP')\n`;
const done = spawnSync('python', ['-c', py, work, out], { encoding: 'utf8' });
if (done.status !== 0) { console.error(done.stderr); process.exit(1); }
console.log('wrote', join(out, 'sidebar.bmp'), join(out, 'header.bmp'));
@@ -0,0 +1,85 @@
#!/usr/bin/env bash
# Write a tester's README with the checksums and sizes of the files it names, computed from the files themselves.
#
# apps/desktop/scripts/make-tester-readme.sh <template> <out.txt> --appimage <file name> --files-dir <dir> [--files-dir <dir> ...]
#
# Why: a README that carries a hand-pasted sha256 goes stale the moment the file is rebuilt (the Ubuntu 26.04 tester compared the file
# with the old build's hash). The template holds placeholders instead, and this script fills them from the real files:
# @APPIMAGE@ the AppImage's file name
# @APP_VERSION@ the app version, read from apps/desktop/src-tauri/Cargo.toml
# @SHA256:<name>@ sha256 of the file <name> found in one of the --files-dir folders
# @SIZE_GB:<name>@ its size in GB with one decimal
# @INCLUDE:<file>@ (alone on a line) the contents of <file> from the template's folder, with the same placeholders
# It refuses to write the README when a named file is missing or a placeholder is left over, so a stale or empty hash cannot ship.
# The same check is done by `--verify <readme> --files-dir <dir>`: every "<64 hex> <name>" or "sha256 <64 hex>" pair that names a file
# present in the folders must match that file.
set -euo pipefail
usage() { echo "usage: $0 <template> <out.txt> --appimage <name> --files-dir <dir> [--files-dir <dir> ...]" >&2; echo " $0 --verify <readme> --files-dir <dir> [...]" >&2; exit 2; }
VERIFY=""; TEMPLATE=""; OUT=""; APPIMAGE=""; DIRS=()
if [ "${1:-}" = "--verify" ]; then VERIFY="${2:?readme}"; shift 2; else TEMPLATE="${1:?template}"; OUT="${2:?out}"; shift 2 || usage; fi
while [ $# -gt 0 ]; do
case "$1" in
--appimage) APPIMAGE="$2"; shift 2 ;;
--files-dir) DIRS+=("$2"); shift 2 ;;
*) usage ;;
esac
done
[ ${#DIRS[@]} -gt 0 ] || usage
find_file() { local d; for d in "${DIRS[@]}"; do [ -f "$d/$1" ] && { echo "$d/$1"; return 0; }; done; return 1; }
sha_of() { sha256sum "$1" | awk '{print $1}'; }
size_gb() { awk -v b="$(stat -c %s "$1" 2>/dev/null || stat -f %z "$1")" 'BEGIN{printf "%.1f", b/1000000000}'; }
if [ -n "$VERIFY" ]; then
# every checksum in the README must be the checksum of a file that is here, and a line that names a file and carries a checksum must carry that file's checksum
hashes=""; bad=0; seen=0
for d in "${DIRS[@]}"; do for f in "$d"/*; do [ -f "$f" ] && grep -qF "$(basename "$f")" "$VERIFY" && hashes="$hashes $(sha_of "$f")"; done; done # only files the README names are hashed (a folder may hold big unrelated files)
readme_hashes="$(grep -Eo '[0-9a-f]{64}' "$VERIFY" | sort -u || true)"
for h in $readme_hashes; do
seen=$((seen + 1))
case " $hashes " in *" $h "*) ;; *) echo "STALE: the README lists $h, which is the checksum of no file in: ${DIRS[*]}" >&2; bad=$((bad + 1)) ;; esac
done
for d in "${DIRS[@]}"; do for f in "$d"/*; do
[ -f "$f" ] || continue
grep -qF "$(basename "$f")" "$VERIFY" || continue
# a line that names this file AND carries a checksum must carry this file's checksum (a sentence that merely mentions the file is fine)
line_hash="$(grep -F "$(basename "$f")" "$VERIFY" | grep -Eo '[0-9a-f]{64}' | head -1 || true)"
if [ -n "$line_hash" ] && [ "$line_hash" != "$(sha_of "$f")" ]; then
echo "STALE: $(basename "$f") is $(sha_of "$f"), the README says $line_hash" >&2; bad=$((bad + 1))
fi
done; done
echo "$seen checksum(s) in the README checked, $bad stale"
[ "$bad" = 0 ] && [ "$seen" -gt 0 ]
exit
fi
[ -n "$APPIMAGE" ] || usage
TDIR="$(cd "$(dirname "$TEMPLATE")" && pwd)"
# the app version has one source (the same file Tauri and the shell use)
APP_VERSION="$(grep -m1 '^version = ' "$TDIR/../src-tauri/Cargo.toml" | cut -d'"' -f2)"
[ -n "$APP_VERSION" ] || { echo "make-tester-readme: no version in $TDIR/../src-tauri/Cargo.toml" >&2; exit 1; }
TMP="$(mktemp)"; trap 'rm -f "$TMP" "$TMP.2"' EXIT
# includes first (one level), then the placeholders
while IFS= read -r line || [ -n "$line" ]; do
if [[ "$line" =~ ^@INCLUDE:([^@]+)@$ ]]; then tr -d '\r' < "$TDIR/${BASH_REMATCH[1]}"; else printf '%s\n' "$line"; fi
done < <(tr -d '\r' < "$TEMPLATE") | sed "s|@APPIMAGE@|$APPIMAGE|g; s|@APP_VERSION@|$APP_VERSION|g" > "$TMP"
# @SHA256:name@ and @SIZE_GB:name@
while IFS= read -r token; do
kind="${token#@}"; kind="${kind%%:*}"; name="${token#@*:}"; name="${name%@}"
f="$(find_file "$name")" || { echo "make-tester-readme: no file named $name in: ${DIRS[*]} (a README must not carry a hash it did not compute)" >&2; exit 1; }
case "$kind" in
SHA256) value="$(sha_of "$f")" ;;
SIZE_GB) value="$(size_gb "$f")" ;;
*) echo "make-tester-readme: unknown placeholder $token" >&2; exit 1 ;;
esac
# replace every occurrence of this exact token (plain text, no pattern characters)
text="$(cat "$TMP"; printf x)"; text="${text%x}"
printf '%s' "${text//"$token"/$value}" > "$TMP"
done < <(grep -Eo '@(SHA256|SIZE_GB):[^@ ]+@' "$TMP" | sort -u)
if grep -Eq '@[A-Z0-9_]+[:@]' "$TMP"; then echo "make-tester-readme: placeholders left over:" >&2; grep -En '@[A-Z0-9_]+[:@]' "$TMP" >&2; exit 1; fi
cp "$TMP" "$OUT"
echo "wrote $OUT"
+200
View File
@@ -0,0 +1,200 @@
#!/usr/bin/env node
/**
* Attached pictures reaching a model, end to end on a test box, through the installed app layout (packaged host + bundled Control + an engine
* component that carries strata-vision):
* 1. the model catalog marks the picture-capable models (images: true) when the picture reader is installed;
* 2. API models: a llama-server with a small picture model (--mmproj) is saved as a Custom model API; the host works out that it reads pictures,
* and a real picture goes out as image_url and is answered. A server without a picture reader, a model set "not to read pictures", and an API
* of another style each get a plain refusal (409) that says how to fix it; ticking "This model reads pictures" lets the picture through;
* 3. a local model (unless --skip-local): Download and prepare with Read pictures ON, load, and a picture through the app's own chat.
*
* node pics-e2e.mjs --install <node/ host/ control/> --components <engine components> --work <scratch> --image shapes.png \
* Also: --silent <url> (a server whose model list says nothing about pictures: ON by default), --agent (the same picture through the agent; needs a Harness)
* --vlm http://127.0.0.1:18090/v1 --vlm-model smolvlm --text-only http://127.0.0.1:18091/v1 [--skip-local --family qwen --model IQ3_XXS --data <folder> --device cpu]
*/
import { spawn } from 'node:child_process';
import { existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
import { createServer } from 'node:net';
import { join, resolve } from 'node:path';
const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => (a.startsWith('--') ? [...acc, [a.slice(2), all[i + 1]?.startsWith('--') || all[i + 1] === undefined ? true : all[i + 1]]] : acc), []));
for (const need of ['install', 'components', 'work', 'image']) if (!args[need]) { console.error('missing --' + need); process.exit(2); }
const install = resolve(args.install), work = resolve(args.work), components = resolve(args.components);
const want = String(args.match ?? args.model ?? 'IQ3_XXS').toLowerCase(), contextSize = Number(args.context ?? 32768), skipLocal = args['skip-local'] === true, device = String(args.device ?? 'cpu'), family = String(args.family ?? 'qwen'), modelId = String(args.model ?? 'IQ3_XXS');
const node = join(install, 'node', 'node'), hostEntry = join(install, 'host', 'server', 'index.mjs');
const dirs = { models: join(work, 'models'), profiles: join(work, 'profiles'), runtime: join(work, 'runtime'), home: join(work, 'home'), xdg: join(work, 'xdg'), appdata: join(work, 'appdata') };
for (const d of [dirs.models, dirs.home, dirs.xdg, dirs.appdata]) mkdirSync(d, { recursive: true });
const delay = ms => new Promise(r => setTimeout(r, ms));
let failed = false;
const record = (name, ok, detail = '') => { if (!ok) failed = true; console.log(`${ok ? 'ok ' : 'FAIL'} ${name}${detail ? ' ' + String(detail).slice(0, 600) : ''}`); };
const freePort = async () => { const s = createServer(); await new Promise(r => s.listen(0, '127.0.0.1', r)); const p = s.address().port; await new Promise(r => s.close(r)); return p; };
const env = { ...process.env, STRATA_COMPONENTS_DIR: components, STRATA_PROFILE_DIR: dirs.profiles, STRATA_RUNTIME_DIR: dirs.runtime, STRATA_INSTALL_DIR: dirs.models,
STRATA_HOME: dirs.home, STRATA_MODELS_DIR: join(work, 'models-unused'), APPDATA: dirs.appdata, XDG_CONFIG_HOME: dirs.xdg, PYTHONDONTWRITEBYTECODE: '1' };
delete env.STRATA_API_KEY;
const port = await freePort();
const base = `http://127.0.0.1:${port}`;
const host = spawn(node, [hostEntry], { cwd: join(install, 'host'), stdio: ['ignore', 'pipe', 'pipe'],
env: { ...env, STRATA_PORT: String(port), STRATA_DIST: join(install, 'host', 'dist'), NODE_ENV: 'production', STRATA_PACKAGED: '1', STRATA_REAL_ENGINE: '1', STRATA_NO_ACQUIRE: '1',
STRATA_SHARE_PORT: String(await freePort()), STRATA_TUNNEL_PORT: String(await freePort()) } });
let hostLog = '';
host.stdout.on('data', c => { hostLog += c; }); host.stderr.on('data', c => { hostLog += c; });
const call = async (path, method = 'GET', body) => {
const response = await fetch(base + path, { method, headers: { 'content-type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body) });
return { status: response.status, body: await response.json().catch(() => null) };
};
const png = 'data:image/png;base64,' + readFileSync(resolve(args.image)).toString('base64');
const QUESTION = 'Name each shape in this picture with its colour, and read out any text.';
/** One chat turn through the app's own endpoint: the plain-text answer, or the refusal (status + message). */
async function chat(extra, message = QUESTION) {
const response = await fetch(base + '/api/chat/stream', { method: 'POST', headers: { 'content-type': 'application/json' },
body: JSON.stringify({ message, agent: false, images: [{ url: png, name: 'shapes.png' }], ...extra }) });
if (response.status !== 200) { const b = await response.json().catch(() => ({})); return { status: response.status, message: b.error ?? b.message ?? '' }; }
let text = '', error = '';
for (const line of (await response.text()).split('\n')) {
if (!line.trim()) continue;
let f; try { f = JSON.parse(line); } catch { continue; }
if (f.type === 'error') error = f.message ?? 'error';
else if (typeof f.text === 'string' && f.type === 'delta') text += f.text;
}
return { status: 200, text, error };
}
const saveApi = async (name, baseUrl, models, protocol = 'openai-chat') => {
const existing = (await call('/api/model-providers')).body.connections.find(c => c.name === name);
return (await call('/api/model-providers', 'POST', { ...(existing ? { id: existing.id } : {}), kind: 'custom', provider: null, name, baseUrl, protocol, apiKey: '', models })).body;
};
async function readsAfter(connectionName, id, want, seconds = 20) {
for (let i = 0; i < seconds * 2; i++) {
const c = (await call('/api/model-providers')).body.connections.find(x => x.name === connectionName);
const m = c?.models.find(x => x.id === id);
if (m && m.reads === want) return { c, m };
await delay(500);
}
const c = (await call('/api/model-providers')).body.connections.find(x => x.name === connectionName);
return { c, m: c?.models.find(x => x.id === id) };
}
async function waitJob(id, seconds) {
let last = '';
for (let i = 0; i < seconds * 2; i++) {
const state = (await call('/api/native')).body;
const job = state.jobs.find(j => j.id === id);
if (job && job.summary !== last) { last = job.summary; console.log(' ... ' + String(last).slice(0, 120)); }
if (job && job.state !== 'running') return { job, state };
await delay(500);
}
throw new Error('operation did not finish');
}
const names = low => /(red|blue|green|circle|square|triangle|strata|42)/.test(low);
async function main() {
let up = false;
for (let i = 0; i < 200 && !up; i++) { try { up = (await fetch(base + '/api/health')).ok; } catch { /* not yet */ } if (!up) await delay(150); }
record('host healthy', up, up ? base : hostLog.slice(-500));
if (!up) return;
// ---- 1. the catalog
const packages = existsSync(join(components, 'engine')) ? readdirSync(join(components, 'engine')) : [];
const reader = packages.some(p => existsSync(join(components, 'engine', p, 'strata-vision')));
const native = (await call('/api/native?refresh=1')).body;
record('the engine component carries the picture reader and the host knows it (visionReader)', reader && native.visionReader === true, `component=${reader} host=${native.visionReader}`);
const rows = (native.catalog?.families ?? []).flatMap(f => (f.sizes ?? []).map(s => ({ family: f.title ?? f.family, size: s.id, images: s.images, capable: s.imagesCapable })));
console.log(' catalog (family / size / images / imagesCapable):');
for (const r of rows) console.log(` ${String(r.family).padEnd(26)} ${String(r.size).padEnd(30)} images=${r.images} capable=${r.capable}`);
record('some models in the catalog read pictures', rows.some(r => r.images === true), `${rows.filter(r => r.images).length} of ${rows.length}`);
record('every picture-capable model reads pictures when the reader is installed', rows.every(r => r.capable === r.images), '');
const flash = rows.filter(r => /flash-next/i.test(`${r.family} ${r.size}`) && !/gyro|qad/i.test(`${r.family} ${r.size}`));
record('the Qwen3.8-Flash-Next models are marked images: true', flash.length > 0 && flash.every(r => r.images === true), flash.map(r => r.size + '=' + r.images).join(' '));
// ---- 2. API models
if (args.vlm) {
const smol = args['vlm-model'] ?? 'smolvlm';
await saveApi('Small picture model', args.vlm, [{ id: smol, name: 'SmolVLM' }]);
const found = await readsAfter('Small picture model', smol, true);
record('the host works out that the llama-server reads pictures (no tick needed)', found.m?.reads === true, JSON.stringify(found.m));
const conn = found.c;
const sent = await chat({ remoteModel: { connectionId: conn?.id, modelId: smol } });
console.log(` answer from the API model: ${JSON.stringify((sent.text ?? sent.message ?? '').slice(0, 300))}`);
record('the picture goes to the API model and is answered', sent.status === 200 && !sent.error && (sent.text ?? '').trim().length > 0 && names((sent.text ?? '').toLowerCase()), `status=${sent.status} err=${sent.error ?? ''}`);
if (args['text-only']) {
await saveApi('Text only server', args['text-only'], [{ id: 'plain', name: 'Plain model' }]);
const t = await readsAfter('Text only server', 'plain', false);
record('a server without a picture reader is known not to read pictures', t.m?.reads === false, JSON.stringify(t.m));
const refused = await chat({ remoteModel: { connectionId: t.c.id, modelId: 'plain' } });
console.log(` refusal: ${JSON.stringify(refused.message)}`);
record('a picture for it is refused in plain words that say how to fix it', refused.status === 409 && /Read pictures with this model/.test(refused.message ?? '') && /Settings/.test(refused.message ?? ''), `${refused.status}`);
const ticked = await saveApi('Text only server', args['text-only'], [{ id: 'plain', name: 'Plain model', vision: true }]);
const after = (await call('/api/model-providers')).body.connections.find(x => x.name === 'Text only server');
record('ticking "Read pictures with this model" is saved and makes the model read pictures', after?.models[0]?.vision === true && after?.models[0]?.reads === true, JSON.stringify(after?.models[0]));
const through = await chat({ remoteModel: { connectionId: after.id, modelId: 'plain' } });
record('with the tick the picture is sent on (the server itself decides what to do with it)', through.status === 200 && (through.error ?? '') !== '' ? /turn off/.test(through.error) : through.status === 200, `${through.status} ${through.message ?? through.error ?? ''}`);
}
await saveApi('Small picture model', args.vlm, [{ id: smol, name: 'SmolVLM', vision: false }]);
const off = await readsAfter('Small picture model', smol, false);
const notRead = await chat({ remoteModel: { connectionId: off.c.id, modelId: smol } });
record('a model switched OFF refuses pictures and says where to turn them on again', notRead.status === 409 && /not to read pictures/.test(notRead.message ?? '') && /Settings/.test(notRead.message ?? ''), `${notRead.status} ${notRead.message ?? ''}`);
await saveApi('Small picture model', args.vlm, [{ id: smol, name: 'SmolVLM', vision: null }]);
// unknown is ON by default: a server whose list and /props say nothing is sent the picture
if (args.silent) {
await saveApi('Silent server', args.silent, [{ id: 'quiet', name: 'Quiet model' }]);
const q = await readsAfter('Silent server', 'quiet', true, 8);
record('a server that says nothing about pictures is ON by default', q.m?.reads === true && q.m?.vision === null, JSON.stringify(q.m));
const sentQuiet = await chat({ remoteModel: { connectionId: q.c.id, modelId: 'quiet' } });
record('and the picture is sent to it (not refused)', sentQuiet.status === 200, `${sentQuiet.status} ${sentQuiet.message ?? sentQuiet.error ?? ''}`);
}
await saveApi('Other style', 'https://api.anthropic.com/v1', [{ id: 'claude-x', name: 'Claude' }], 'anthropic');
const other = (await call('/api/model-providers')).body.connections.find(x => x.name === 'Other style');
const style = await chat({ remoteModel: { connectionId: other.id, modelId: 'claude-x' } });
console.log(` other style: ${JSON.stringify(style.message)}`);
record('an API of another style says plainly why pictures stay in the preview', style.status === 409 && /OpenAI/.test(style.message ?? ''), `${style.status}`);
}
if (skipLocal) return;
// ---- 3. a local model
const values = { family, model: modelId, context: contextSize, vision: 'yes', loadVision: device, ...(args.data ? { data_dir: resolve(args.data) } : {}) };
const started = await call('/api/native/operations', 'POST', { kind: 'install', args: values });
record('the install operation starts', started.status === 200 || started.status === 202, JSON.stringify(started.body).slice(0, 200));
const done = await waitJob(started.body.id, 3600);
record('Download and prepare finishes', done.job.state === 'done', `${done.job.state} ${done.job.error ?? done.job.summary}`);
const refreshed = (await call('/api/native?refresh=1')).body;
const model = refreshed.installed?.models?.find(m => [m.id, m.model, m.tag].some(x => String(x ?? '').toLowerCase() === want));
record('the model is prepared with its picture reader', !!model?.visionPrepared, JSON.stringify({ ready: model?.ready, visionPrepared: model?.visionPrepared }));
const load = await call('/api/native/operations', 'POST', { kind: 'load', args: { model: model?.model ?? want } });
const loaded = await waitJob(load.body.id, 900);
const instance = loaded.state.instances[0];
record('the model loads with the picture reader', loaded.job.state === 'done' && instance?.status === 'ready' && instance?.vision === true, `${loaded.job.state} ${loaded.job.error ?? ''} vision=${instance?.vision}`);
record('the model loaded with pictures ON without being asked per load (default)', instance?.vision === true, '');
const t0 = Date.now();
let local = { status: 0 };
for (let i = 0; i < 2 && !(local.text ?? '').trim(); i++) local = await chat({ nativeModelId: instance?.id, maxTokens: 4096 });
console.log(` answer from the local model (${Math.round((Date.now() - t0) / 1000)} s): ${JSON.stringify((local.text ?? local.message ?? '').slice(0, 300))}`);
const low = (local.text ?? '').toLowerCase();
record('the local model answers about the picture (names the shapes)', local.status === 200 && /red|circle/.test(low) && /blue|square/.test(low) && /green|triangle/.test(low), `status=${local.status} err=${local.error ?? ''}`);
record('the local model reads STRATA 42', low.includes('42'));
// the same picture through the agent (the Harness route must declare image input: the "does not declare image input" error of the tester)
if (args.agent) { // needs a Harness beside the host (the installed layout has one; a bare test box does not)
const t1 = Date.now(); let a = { status: 0 };
for (let i = 0; i < 2 && !/red|circle|square|triangle/.test((a.text ?? '').toLowerCase()); i++) a = await chat({ nativeModelId: instance?.id, agent: true, sessionId: 'pics-agent-' + i, mode: 'plan', maxTokens: 4096 });
console.log(` answer through the agent (${Math.round((Date.now() - t1) / 1000)} s): ${JSON.stringify((a.text ?? a.message ?? '').slice(0, 300))}`);
record('the agent route reads the picture too', a.status === 200 && /red|circle|square|triangle/.test((a.text ?? '').toLowerCase()) && !/does not declare image input/.test(a.text ?? ''), `status=${a.status} err=${a.error ?? ''}`);
}
// OFF: loading with Read pictures off refuses a picture in plain words and says what to do
await call(`/api/native/instances/${instance?.id}/unload`, 'POST', {});
const loadOff = await call('/api/native/operations', 'POST', { kind: 'load', args: { model: model?.model ?? want, vision: 'no' } });
const loadedOff = await waitJob(loadOff.body.id, 900);
const off2 = loadedOff.state.instances[0];
record('loaded with Read pictures OFF', loadedOff.job.state === 'done' && off2?.vision === false, `${loadedOff.job.state} vision=${off2?.vision}`);
const refusedLocal = await chat({ nativeModelId: off2?.id, maxTokens: 64 });
console.log(` refusal: ${JSON.stringify(refusedLocal.message)}`);
record('a picture for the OFF model is refused with a next step', refusedLocal.status === 409 && /Read pictures/.test(refusedLocal.message ?? '') && /Models/.test(refusedLocal.message ?? ''), `${refusedLocal.status}`);
const unloaded = await call(`/api/native/instances/${off2?.id}/unload`, 'POST', {});
record('unload', unloaded.status === 200, unloaded.status);
}
try { await main(); } catch (e) { record('run', false, e.stack ?? e); }
host.kill();
await delay(1500);
console.log(failed ? 'PICS-E2E FAIL' : 'PICS-E2E PASS');
process.exit(failed ? 1 : 0);
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# Put the bundled payloads back to the bytes they were staged with, inside a finished AppImage.
#
# apps/desktop/scripts/repack-appimage.sh <in.AppImage> <out.AppImage> [--resources <src-tauri/resources>] [--check]
#
# Why: Tauri hands the AppDir to linuxdeploy, which treats every ELF file under usr/lib as a library to fix up: it rewrites the RPATH of
# the engine (strata, libstrata_kq.so, libcudart), of the bundled Python and Node and of the Harness's native modules. The files then no
# longer match the SHA-256 and size their manifests list (engine-component.json), so the host refuses the bundled engine, and the
# 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.
set -euo pipefail
IN="$(realpath "${1:?in.AppImage}")"; OUT="${2:-}"
RES="$(cd "$(dirname "${BASH_SOURCE[0]}")/../src-tauri/resources" && pwd)"
CHECK=0
shift 2 || true
while [ $# -gt 0 ]; do
case "$1" in
--resources) RES="$(cd "$2" && pwd)"; shift 2 ;;
--check) CHECK=1; shift ;;
*) echo "unknown option $1" >&2; exit 2 ;;
esac
done
OFFSET="$("$IN" --appimage-offset)"
WORK="$(mktemp -d -t strata-repack.XXXXXX)"
trap 'rm -rf "$WORK"' EXIT
unsquashfs -no-progress -o "$OFFSET" -d "$WORK/AppDir" "$IN" >/dev/null
# the product folder under usr/lib (Tauri names it after productName)
LIB="$(find "$WORK/AppDir/usr/lib" -maxdepth 2 -type d -name engine-bundle -printf '%h\n' | head -1)"
[ -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; }
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)
echo "$bad file(s) differ from what was staged"
[ "$bad" = 0 ]
exit
fi
[ -n "$OUT" ] || { echo "give out.AppImage" >&2; exit 2; }
for d in control harness node engine-bundle; do
[ -d "$RES/$d" ] || continue
rsync -aL --checksum --delete --exclude .gitignore "$RES/$d/" "$LIB/$d/"
done
# 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
# and dies ("OPENSSL_3.2.0 not found"). The line goes right after `this_dir=...`, ahead of every hook.
if ! grep -q STRATA_ORIGINAL_ENV "$WORK/AppDir/AppRun"; then
sed -i '/^this_dir=/a # Strata: the environment this AppImage was started from (given back to programs that are not Strata'"'"'s own)\nexport STRATA_ORIGINAL_ENV="$(env -0 | base64 -w0)"' "$WORK/AppDir/AppRun"
grep -q STRATA_ORIGINAL_ENV "$WORK/AppDir/AppRun" || { echo "could not add the environment record to AppRun" >&2; exit 1; }
fi
mksquashfs "$WORK/AppDir" "$WORK/app.squashfs" -root-owned -noappend -comp zstd -b 131072 -no-progress >/dev/null
head -c "$OFFSET" "$IN" > "$OUT"
cat "$WORK/app.squashfs" >> "$OUT"
chmod +x "$OUT"
# The checksum is computed here, from the finished file, every time: a README or a note must take it from this file
# (apps/desktop/scripts/make-tester-readme.sh) and never from an older build.
( cd "$(dirname "$OUT")" && sha256sum "$(basename "$OUT")" > "$(basename "$OUT").sha256" )
echo "repacked: $OUT ($(stat -c %s "$OUT") bytes)"
cat "$OUT.sha256"
+202
View File
@@ -0,0 +1,202 @@
#!/usr/bin/env python3
"""Stage the Control component the installer ships: src-tauri/resources/control.
python apps/desktop/scripts/stage-control.py # build the terminal bundle, stage it
python apps/desktop/scripts/stage-control.py --archive X.zip # stage an archive you already built
python apps/desktop/scripts/stage-control.py --wheelhouse DIR # build with no network (wheels from DIR)
python apps/desktop/scripts/stage-control.py --check # verify what is staged
What lands in `control/`: the pinned CPython 3.12.15 (python-build-standalone, digest-checked), the hash-locked wheels
(`packaging/locks/runtime-<target>-cp312.txt`, installed with --require-hashes at build time), strata_control,
strata_cli, serve, tools/strata_tokenizer.py + gguf_reader.py, and BUNDLE.json (version, interpreter and lock digests,
the digest of the archive it came from). It is the same bundle as the Terminal archive (packaging/package_terminal.py):
one Python runtime-core for both products. The app then never runs pip, npm, git or a compiler to start or serve.
This script is a BUILD step. It may download the interpreter and the wheels (verified); the app never does.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import shutil
import subprocess
import sys
import tempfile
import zipfile
import tarfile
from pathlib import Path
HERE = Path(__file__).resolve().parent
ROOT = HERE.parents[2]
OUT = HERE.parent / "src-tauri" / "resources" / "control"
sys.path.insert(0, str(ROOT / "packaging"))
import package_terminal as pt # noqa: E402
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(1 << 20), b""):
digest.update(block)
return digest.hexdigest()
def build_archive(work: Path, wheelhouse: str | None) -> Path:
command = [sys.executable, str(ROOT / "packaging" / "package_terminal.py"), "--out", str(work)]
if wheelhouse:
command += ["--wheelhouse", wheelhouse]
subprocess.run(command, check=True, cwd=str(ROOT))
archives = sorted(work.glob("strata-terminal-*.zip")) + sorted(work.glob("strata-terminal-*.tar.gz"))
if len(archives) != 1:
raise SystemExit(f"expected one archive in {work}, found {archives}")
return archives[0]
def extract(archive: Path, destination: Path) -> Path:
"""Unpack the archive; returns the folder that holds python/ and strata.py."""
scratch = destination.parent / (destination.name + ".incoming")
shutil.rmtree(scratch, ignore_errors=True)
scratch.mkdir(parents=True)
if archive.name.endswith(".zip"):
with zipfile.ZipFile(archive) as zf:
for member in zf.namelist():
target = (scratch / member).resolve()
if scratch.resolve() not in target.parents and target != scratch.resolve():
raise SystemExit(f"unsafe member in {archive.name}: {member}")
zf.extractall(scratch)
else:
with tarfile.open(archive) as tf:
tf.extractall(scratch, filter="data")
(inner,) = [p for p in scratch.iterdir() if p.is_dir()]
return inner
#: What the app's installer needs on top of the Terminal bundle: the model catalog, GPU probe and install job. The list and the copy
#: live in packaging/package_terminal.py (the Terminal archive carries the same files, for `strata launch`).
INSTALLER_FILES, INSTALLER_TOOLS, INSTALLER_DATA = pt.INSTALLER_FILES, pt.INSTALLER_TOOLS, pt.INSTALLER_DATA
local_imports = pt.local_imports
def add_installer(folder: Path) -> dict:
"""Copy the installer's files from the source tree into the staged Control (after the Terminal bundle is in place)."""
return pt.add_installer(folder, ROOT)
#: What the other names of the interpreter become: Tauri copies a symlink as a full copy of its target, so `python`, `python3` and
#: `python3.12` in a bundle would be three real files (68 MB each on Linux). Only `python3.12` stays the real one.
#: The launcher runs inside the AppImage's environment (LD_LIBRARY_PATH holds the bundled libraries), where an external program such as
#: `dirname` can fail to start (Ubuntu 26.04's coreutils: "libsystemd.so.0: version LIBSYSTEMD_254 not found"), so it uses shell built-ins
#: only. The first version called `dirname`; that left the folder empty and "Control stopped" on such a computer.
LAUNCHER = """#!/bin/sh
# Strata: this name only starts the real interpreter beside it (the bundle keeps one copy, not one per name). Built-ins only: no dirname.
case "$0" in */*) dir=${0%%/*} ;; *) dir=. ;; esac
here=$(cd "$dir" && pwd)
exec "$here/%s" "$@"
"""
def dedupe_python(folder: Path) -> list[str]:
"""Make every other name of the interpreter in `python/bin` a small launcher of the real binary (Linux and macOS; Windows has one
python.exe). A symlink would not survive the bundling, and a plain copy is what costs the space. Returns the names changed."""
bindir = folder / "python" / "bin"
if not bindir.is_dir():
return []
real = bindir / f"python{pt.PYTHON_VERSION.rsplit('.', 1)[0]}"
if not real.exists():
return []
if real.is_symlink(): # the real file is a link itself: make the target the real one
target = real.resolve()
real.unlink()
shutil.move(str(target), str(real))
changed = []
for name in ("python", "python3"):
path = bindir / name
if not (path.is_symlink() or (path.is_file() and path.stat().st_size == real.stat().st_size)):
continue
path.unlink()
path.write_bytes((LAUNCHER % real.name).encode("utf-8"))
path.chmod(0o755)
changed.append(name)
return changed
def write_bundle_record(folder: Path, archive: Path) -> dict:
manifest_path = archive.with_name(archive.name.rsplit(".", 2 if archive.name.endswith(".tar.gz") else 1)[0] + ".manifest.json")
manifest = json.loads(manifest_path.read_text(encoding="utf-8")) if manifest_path.exists() else {}
target = manifest.get("target") or pt.host_target()
record = {
"schema_version": 1, "component": "strata-control",
"version": (folder / "VERSION").read_text(encoding="utf-8").strip(),
"target": target,
"python": {"version": pt.PYTHON_VERSION, "build": pt.PBS_RELEASE, "sha256": pt.PYTHON_PINS[target]["sha256"]},
"lock": manifest.get("lock"),
"installed": manifest.get("installed"),
"archive": {"name": archive.name, "sha256": sha256(archive), "bytes": archive.stat().st_size},
"acquires_at_runtime": False,
"installer": add_installer(folder),
}
(folder / "BUNDLE.json").write_text(json.dumps(record, indent=1) + "\n", encoding="utf-8")
return record
def check(folder: Path) -> int:
target = pt.host_target()
python = folder / pt.PYTHON_PINS[target]["python"]
problems = []
for need in (python, folder / "strata.py", folder / "strata_control", folder / "serve" / "server.py", folder / "BUNDLE.json",
folder / "setup.py", folder / "tools" / "strata_mcp.py", folder / "tools" / "iq_pack.py", folder / "data" / "draft_vocab.bin"):
if not need.exists():
problems.append(f"missing {need}")
if problems:
print("\n".join(problems), file=sys.stderr)
return 1
env = {"PYTHONNOUSERSITE": "1", "PYTHONDONTWRITEBYTECODE": "1", "STRATA_NO_ACQUIRE": "1", "SYSTEMROOT": __import__("os").environ.get("SYSTEMROOT", "")}
done = subprocess.run([str(python), str(folder / "strata.py"), "--version"], capture_output=True, text=True, env=env,
cwd=str(folder), timeout=60)
print(done.stdout.strip() or done.stderr.strip())
if done.returncode:
return done.returncode
# the installer: its catalog, a GPU probe and its imports work on the bundled Python alone (nothing is installed or fetched)
probe = ("import sys; sys.path.insert(0, %r); import importlib.util as u; s = u.spec_from_file_location('s', %r); m = u.module_from_spec(s); "
"s.loader.exec_module(m); import iq_pack, strata_pack, pack_index, mtp_fetch; "
"print('installer ok: %%d models, %%d families' %% (len(m.MODELS), len(m.FAMILIES)))") % (str(folder / "tools"), str(folder / "setup.py"))
env["STRATA_CONTROL_INSTALL"] = "1"
again = subprocess.run([str(python), "-c", probe], capture_output=True, text=True, env=env, cwd=str(folder), timeout=120)
print(again.stdout.strip() or again.stderr.strip()[-600:])
return again.returncode
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--archive", type=Path, help="stage this terminal archive instead of building one")
ap.add_argument("--wheelhouse", help="build with the wheels in this folder (no network)")
ap.add_argument("--out", type=Path, default=OUT)
ap.add_argument("--check", action="store_true", help="verify the staged component and exit")
a = ap.parse_args()
if a.check:
return check(a.out)
with tempfile.TemporaryDirectory(prefix="strata-stage-control-") as work:
archive = a.archive.resolve() if a.archive else build_archive(Path(work), a.wheelhouse)
keep = [p for p in a.out.iterdir() if p.name == ".gitignore"] if a.out.exists() else []
if a.out.exists():
for child in a.out.iterdir():
if child.name != ".gitignore":
shutil.rmtree(child, ignore_errors=True) if child.is_dir() else child.unlink()
a.out.mkdir(parents=True, exist_ok=True)
inner = extract(archive, a.out)
record = write_bundle_record(inner, archive)
for child in inner.iterdir():
shutil.move(str(child), str(a.out / child.name))
shutil.rmtree(inner.parent, ignore_errors=True)
launched = dedupe_python(a.out)
if launched:
print(f"python: {', '.join(launched)} are launchers of the one real interpreter")
print(f"staged {record['component']} {record['version']} ({record['target']}) into {a.out}")
return check(a.out)
if __name__ == "__main__":
raise SystemExit(main())
+176
View File
@@ -0,0 +1,176 @@
#!/usr/bin/env node
/**
* Stage the DeepSeek Harness (pinned, MIT) as the installer's `harness/` folder: a pruned PRODUCTION install, built at
* build time, so nothing is fetched or compiled on the person's PC.
*
* node apps/desktop/scripts/stage-harness.mjs --src <built Harness checkout> [--out apps/desktop/src-tauri/resources/harness]
*
* <built Harness checkout> is a clone of the pinned commit after `pnpm install --frozen-lockfile` and `pnpm run build:artifacts`
* (see phaseA-tests/notes-split-s4a.md, step 1). The script never touches it: it runs `pnpm deploy --legacy --prod` into
* the output folder, removes what a running Harness does not read (sources, tests, maps, docs, other operating systems'
* prebuilt binaries, the LibreOffice document converter and the speech engine, which the app does not use), keeps the
* MIT licence and third-party notices, writes BUNDLE.json, and finally boots the staged copy once as proof.
*
* The host finds the result at `<install>/harness` (server/agent/harness-runtime.mjs), or in the component folder.
*/
import { spawnSync } from 'node:child_process';
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
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';
const args = process.argv.slice(2);
const opt = (name, fallback) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : fallback; };
const src = resolve(opt('src', process.env.STRATA_HARNESS_SRC ?? ''));
const out = resolve(opt('out', join(import.meta.dirname, '..', 'src-tauri', 'resources', 'harness')));
if (!opt('src', process.env.STRATA_HARNESS_SRC) || !existsSync(join(src, 'apps', 'cli', 'lib', 'bin.js'))) {
console.error('Give --src <a built Harness checkout> (apps/cli/lib/bin.js must exist).');
process.exit(2);
}
const platform = process.platform;
const run = (cmd, argv, cwd, extra = {}) => {
const r = spawnSync(cmd, argv, { cwd, stdio: 'inherit', shell: false, ...extra });
if (r.status !== 0) { console.error(`${cmd} ${argv.join(' ')} failed (${r.status ?? r.error})`); process.exit(1); }
};
// The output folder in the repo carries a .gitignore that keeps the staged files out of git: keep it across the clean-up.
const keepIgnore = existsSync(join(out, '.gitignore')) ? readFileSync(join(out, '.gitignore')) : null;
rmSync(out, { recursive: true, force: true });
mkdirSync(out, { recursive: true });
const pnpm = platform === 'win32' ? ['corepack.cmd', ['pnpm']] : ['corepack', ['pnpm']];
run(pnpm[0], [...pnpm[1], 'deploy', '--legacy', '--prod', '--filter', '@deepseek-ai/dsh', out], src, { shell: platform === 'win32' });
// --- peers -------------------------------------------------------------------------------------------------------------
// `pnpm deploy --prod` leaves out peer dependencies, but the Harness loads some of them by name at run time (for example the
// plugin-group helper that app-boot imports). Walk the installed packages; every dependency or peer that cannot be
// resolved from the output is copied from the source checkout's own node_modules, and its own requirements after it.
function packageJsonsUnder(dir, out2 = []) {
for (const e of readdirSync(dir, { withFileTypes: true })) {
const p = join(dir, e.name);
if (e.isSymbolicLink()) continue;
if (e.isDirectory()) packageJsonsUnder(p, out2);
else if (e.name === 'package.json' && p.includes(`${sep}node_modules${sep}`)) out2.push(p);
}
return out2;
}
function requirementsOf(file) {
try { const j = JSON.parse(readFileSync(file, 'utf8')); return [...Object.keys(j.dependencies ?? {}), ...Object.keys(j.peerDependencies ?? {}), ...Object.keys(j.optionalDependencies ?? {})]; } catch { return []; }
}
/** The checkout's own workspace packages (packages/**, vendor/*, apps/*) by name: where a peer that is one of ours comes from. */
const workspace = new Map();
function indexWorkspace(dir, depth = 0) {
if (depth > 4) return;
for (const e of readdirSync(dir, { withFileTypes: true })) {
if (!e.isDirectory() || e.isSymbolicLink() || e.name === 'node_modules' || e.name.startsWith('.')) continue;
const p = join(dir, e.name);
const manifest = join(p, 'package.json');
if (existsSync(manifest)) { try { workspace.set(JSON.parse(readFileSync(manifest, 'utf8')).name, p); } catch { /* not a package */ } }
indexWorkspace(p, depth + 1);
}
}
for (const top of ['packages', 'vendor', 'apps']) if (existsSync(join(src, top))) indexWorkspace(join(src, top));
const resolvable = name => existsSync(join(out, 'node_modules', ...name.split('/')));
const queue = [...new Set(packageJsonsUnder(join(out, 'node_modules')).flatMap(requirementsOf))].filter(n => !resolvable(n));
const added = [];
while (queue.length) {
const name = queue.shift();
if (resolvable(name)) continue;
const from = workspace.get(name) ?? join(src, 'node_modules', ...name.split('/'));
if (!existsSync(from)) continue; // an optional package for another platform, or not installed in the source: nothing to copy
const to = join(out, 'node_modules', ...name.split('/'));
mkdirSync(join(to, '..'), { recursive: true });
const realFrom = realpathSync(from);
cpSync(realFrom, to, { recursive: true, dereference: true, filter: f => !relative(realFrom, f).split(sep).includes('node_modules') });
added.push(name);
queue.push(...requirementsOf(join(to, 'package.json')).filter(n => !resolvable(n)));
}
if (added.length) console.log(`peer packages added: ${added.join(', ')}`);
// --- the browser tool server ---------------------------------------------------------------------------------------------
// Settings -> Tools can switch on a browser the assistant drives (Playwright's MCP server, Apache-2.0) on the person's own Chrome or Edge,
// so no browser is downloaded. The server and the two Playwright libraries it needs are not part of the Harness's production closure;
// they are copied from the source checkout's package store (~20 MB) and the host starts it with Node (server/agent/browser-probe.mjs).
const pnpmStore = join(src, 'node_modules', '.pnpm');
function storePackage(name, version) {
if (!existsSync(pnpmStore)) return null;
const prefix = `${name.replace('/', '+')}@`;
const hit = readdirSync(pnpmStore).filter(n => n.startsWith(prefix) && (!version || n === `${prefix}${version}` || n.startsWith(`${prefix}${version}_`))).sort().at(-1);
const dir = hit ? join(pnpmStore, hit, 'node_modules', ...name.split('/')) : null;
return dir && existsSync(dir) ? dir : null;
}
const browserDir = storePackage('@playwright/mcp');
if (browserDir) {
const wanted = JSON.parse(readFileSync(join(browserDir, 'package.json'), 'utf8')).dependencies ?? {};
const parts = [['@playwright/mcp', browserDir], ...['playwright', 'playwright-core'].map(n => [n, storePackage(n, wanted[n])])];
if (parts.every(([, dir]) => dir)) {
for (const [name, dir] of parts) {
const to = join(out, 'node_modules', ...name.split('/'));
mkdirSync(join(to, '..'), { recursive: true });
cpSync(realpathSync(dir), to, { recursive: true, dereference: true, filter: f => !relative(dir, f).split(sep).includes('node_modules') });
}
console.log(`browser tool server added: ${parts.map(([n]) => n).join(', ')}`);
} else console.log('browser tool server NOT added: Playwright libraries missing from the package store');
} else console.log('browser tool server NOT added: @playwright/mcp is not in the source checkout');
// --- one real folder per package, no links -------------------------------------------------------------------------------
// The installer copies files; pnpm's layout is links. See flatten-pnpm.mjs. The two packages the app does not use are never copied.
const flat = flattenPnpm(out, { skip: /libreoffice-kit|sherpa-onnx/i });
console.log(`flattened: ${flat.packages} placed (${flat.nested} nested for a second version)`);
// --- prune ------------------------------------------------------------------------------------------------------------
/** Whole packages the app never loads: office-document conversion (hundreds of MB) and speech input. */
const DROP_PACKAGES = [/libreoffice-kit/i, /sherpa-onnx/i];
/** Only when sitting directly in a package's root (a library may have a runtime folder called `doc`, e.g. yaml/dist/doc). */
const DROP_DIRS = new Set(['test', 'tests', '__tests__', 'docs', 'website', 'benchmarks', 'examples', '.github']);
const DROP_FILES = /\.(map|md|markdown|pdb|tsbuildinfo)$/i;
/** Licences and notices travel with the files they cover. */
const KEEP_FILES = /^(LICENSE|LICENCE|NOTICE|THIRD_PARTY)/i;
let removed = 0;
function prune(dir) {
for (const e of readdirSync(dir, { withFileTypes: true })) {
const p = join(dir, e.name);
if (e.isSymbolicLink()) continue;
if (e.isDirectory()) {
const inLib = p.split(sep).includes('lib');
if ((DROP_DIRS.has(e.name) && !inLib && existsSync(join(dir, 'package.json'))) || DROP_PACKAGES.some(r => r.test(e.name)) || isOtherOsBinaryFolder(platform, e.name, p)) {
rmSync(p, { recursive: true, force: true }); removed++; continue;
}
prune(p);
} else if (DROP_FILES.test(e.name) && !KEEP_FILES.test(e.name)) {
rmSync(p, { force: true }); removed++;
}
}
}
prune(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));
}
const cli = JSON.parse(readFileSync(join(out, 'package.json'), 'utf8'));
let commit = process.env.STRATA_HARNESS_COMMIT ?? null;
if (!commit) { const r = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: src, encoding: 'utf8' }); if (r.status === 0) commit = r.stdout.trim(); }
function size(dir) { let t = 0, n = 0; for (const e of readdirSync(dir, { withFileTypes: true })) { const p = join(dir, e.name); if (e.isDirectory()) { const s = size(p); t += s.bytes; n += s.files; } else if (e.isFile()) { t += statSync(p).size; n++; } } return { bytes: t, files: n }; }
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(),
note: 'Pruned production install (pnpm deploy --prod). No package manager runs on the user\'s PC.',
}, null, 2));
if (keepIgnore) writeFileSync(join(out, '.gitignore'), keepIgnore);
// --- proof 1: nothing the Harness imports by name was pruned ------------------------------------------------------------
// (2026-10-17: the other-OS rule once removed @deepseek-ai/dsh-win32-process, which every platform imports; Linux/Mac agent chats died.)
const lost = missingHarnessImports(out);
if (lost.length) { console.error(['staged Harness is missing imported packages:', ...lost.map(l => ` ${l.from} -> ${l.missing}`)].join(String.fromCharCode(10))); process.exit(1); }
console.log('import check: every @deepseek-ai package the Harness imports is staged');
// --- proof 2: the staged copy answers --version and boots the web profile once ----------------------------------------
const bin = join(out, 'apps', 'cli', 'lib', 'bin.js');
const entry = existsSync(bin) ? bin : join(out, 'lib', 'bin.js');
const v = spawnSync(process.execPath, [entry, '--version'], { encoding: 'utf8' });
if (v.status !== 0) { console.error('staged Harness does not start:', v.stderr); process.exit(1); }
console.log(`staged ${cli.version} (${commit?.slice(0, 8) ?? 'no commit'}) in ${out}: ${(total.bytes / 1048576).toFixed(0)} MiB, ${total.files} files, ${removed} pruned items; --version -> ${v.stdout.trim()}`);
console.log(`sha256(BUNDLE.json)=${createHash('sha256').update(readFileSync(join(out, 'BUNDLE.json'))).digest('hex')}`);
+721
View File
@@ -0,0 +1,721 @@
#!/usr/bin/env node
/**
* Headless UI flows of the installed app against a packaged host with a FAKE installer and a FAKE engine (nothing is
* downloaded, no real engine runs; the real Control starts the fake engine through the owner runtime):
*
* node apps/desktop/scripts/ui-flows.mjs [--control <staged control>] [--node <node.exe>] [--shots <dir>] [--only download,models,tune,fail,theme]
*
* download the onboarding download: ONE combined bar over both files, an ETA in minutes left of Stop, Finish later closes the
* pop-up with the download still running, the Models screen shows the same progress, Stop stops it
* models an installed model is the one Models selects and loads, without a download pop-up
* tune the tuning step offers no picture reading, tunes, writes a RAM budget for "leave 20%", opens chat; reading size
* fail an engine that stops while loading: the error is in words and the pop-up does not scroll
* theme the app follows the system light/dark setting
* Per-user folders are all under a scratch folder. Exit 1 if a check fails.
*/
import { spawn, spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import { copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, rmdirSync, symlinkSync, writeFileSync } from 'node:fs';
import { createServer } from 'node:net';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => (a.startsWith('--') ? [...acc, [a.slice(2), all[i + 1]?.startsWith('--') ? true : all[i + 1]]] : acc), []));
const here = dirname(fileURLToPath(import.meta.url));
const repoHost = resolve(here, '../../host');
const staged = resolve(here, '../src-tauri/resources');
const stagedControl = resolve(args.control ?? join(staged, 'control'));
const nodeExe = resolve(args.node ?? join(staged, 'node', process.platform === 'win32' ? 'node.exe' : 'node'));
const work = mkdtempSync(join(tmpdir(), 'strata-ui-flows-'));
const shots = resolve(args.shots ?? join(work, 'shots'));
mkdirSync(shots, { recursive: true });
const only = new Set(String(args.only ?? 'download,models,profile,ui2,ui2d,tune,fail,delete,theme,restart,perf,polish,stop,latest').split(','));
const isWin = process.platform === 'win32';
const { chromium } = await import(pathToFileURL(join(repoHost, 'node_modules/playwright-core/index.mjs')).href).catch(() => import('playwright-core'));
const CHROME = process.env.CHROME ?? ['C:/Program Files/Google/Chrome/Application/chrome.exe', '/usr/bin/google-chrome'].find(existsSync);
const delay = ms => new Promise(r => setTimeout(r, ms));
const sha = buf => createHash('sha256').update(buf).digest('hex');
const freePort = async () => { const s = createServer(); await new Promise(r => s.listen(0, '127.0.0.1', r)); const p = s.address().port; await new Promise(r => s.close(r)); return p; };
let failed = false;
const check = (ok, label, detail = '') => { console.log(`${ok ? 'ok ' : 'FAIL'} ${label}${detail ? ' ' + detail : ''}`); if (!ok) failed = true; };
// ------------------------------------------------------------------ the app folder: the real Control with a fake installer
const control = join(work, 'control');
mkdirSync(control, { recursive: true });
for (const name of ['python', 'strata_control', 'strata_cli', 'serve']) symlinkSync(join(stagedControl, name), join(control, name), 'junction');
for (const name of ['strata.py', 'VERSION', 'BUNDLE.json', 'requirements.txt']) copyFileSync(join(stagedControl, name), join(control, name));
cpSync(join(stagedControl, 'tools'), join(control, 'tools'), { recursive: true });
const python = isWin ? join(control, 'python', 'python.exe') : join(control, 'python', 'bin', 'python3');
const FAKE_INSTALLER = String.raw`
import json, os, sys, time
from pathlib import Path
ROOT = Path(sys.argv[sys.argv.index('--root') + 1]) if '--root' in sys.argv else Path('.')
STATE = Path(os.environ['FAKE_STATE']); STATE.mkdir(parents=True, exist_ok=True)
RATE = float(os.environ.get('FAKE_RATE_GB_S', '0.5'))
def size(model, gb, ram, mid):
return {'model': model, 'about': model, 'download_gb': gb, 'ram_needed_gb': ram, 'experts_gb': ram - 8, 'on_this_pc': 'fits', 'installed': False, 'images': True, 'experimental': False, 'id': mid}
CATALOG = {'summary': 'fake', 'families': [{'family': 'qwen', 'title': 'Qwen3.8-Flash-Next', 'about': 'x', 'experimental': False, 'images': True, 'sizes': [
size('Q2_0', 66.4, 48, 'q2_0'), size('IQ3_XXS', 75.8, 60, 'iq3_xxs'), size('IQ3_S', 83.6, 62, 'iq3_s')]}],
'contexts': [8192, 32768, 65536], 'recommendation': {'family': 'qwen', 'model': 'IQ3_XXS', 'title': 'Qwen3.8-Flash-Next IQ3_XXS', 'context': 32768}, 'context_note': ''}
HARDWARE = {'ram_gb': 63.1, 'cpu': {'name': 'cpu', 'cores': 12}, 'gpus': [{'index': 0, 'name': 'NVIDIA GeForce RTX 5070', 'vram_gb': 11.9, 'usable': True}]}
TOTAL = 83.6
def job():
f = STATE / 'job.json'
if not f.exists(): return None
j = json.loads(f.read_text())
if j.get('cancelled'):
return {'running': False, 'exit_code': 1, 'error': 'stopped', 'result': 'failed'}
done = min(TOTAL, RATE * (time.time() - j['start']))
return {'running': True, 'step': '5/7: downloading Qwen3.8-Flash-Next IQ3_S', 'model': 'IQ3_S',
'download': {'file': 'model files', 'done_gb': round(done, 3), 'total_gb': TOTAL, 'remaining_gb': round(TOTAL - done, 3), 'percent': int(100 * done / TOTAL)}}
def call(name, a):
if name == 'strata_models': return CATALOG
if name == 'strata_remove':
cfg = ROOT / ('strata-%s.json' % a['model'])
if a.get('confirm') and cfg.exists(): cfg.unlink()
return {'summary': 'Deleted', 'plan': {'gb': 83.6}, 'failed': []}
if name == 'strata_status': return {'summary': 'fake', 'installed': {'models': [{'model': 'iq3_s', 'disk_gb': 83.6, 'disk_bytes': 83600000000}]}, 'hardware': HARDWARE, 'recommendation': CATALOG['recommendation'], 'install': job(), 'running': []}
if name == 'strata_install':
if a.get('cancel'):
f = STATE / 'job.json'
if f.exists(): j = json.loads(f.read_text()); j['cancelled'] = True; f.write_text(json.dumps(j))
return {'summary': 'stopped', 'install': job()}
if a.get('confirm'):
(STATE / 'job.json').write_text(json.dumps({'start': time.time()}))
return {'summary': 'started', 'plan': {'download_gb': TOTAL}, 'install': job()}
if not a: return {'summary': 'progress', 'install': job()}
return {'summary': 'Plan', 'plan': {'title': 'Qwen3.8-Flash-Next IQ3_S', 'download_gb': TOTAL, 'disk_short': None}}
raise Exception('unknown tool ' + name)
for line in sys.stdin:
line = line.strip()
if not line: continue
m = json.loads(line)
if 'id' not in m: continue
method = m.get('method')
if method == 'initialize': res = {'protocolVersion': '2024-11-05'}
elif method == 'tools/list': res = {'tools': [{'name': n} for n in ('strata_models', 'strata_status', 'strata_install', 'strata_remove')]}
else:
p = m['params']
res = {'structuredContent': call(p['name'], p.get('arguments') or {})}
sys.stdout.write(json.dumps({'jsonrpc': '2.0', 'id': m['id'], 'result': res}) + '\n'); sys.stdout.flush()
`;
writeFileSync(join(control, 'tools', 'strata_mcp.py'), FAKE_INSTALLER);
const FAKE_ENGINE = `import os, sys, json, time
from pathlib import Path
if os.environ.get('FAKE_ENGINE_FAIL'):
print('memory guard: 5.1 GiB of RAM free is under the 6 GiB limit (limit 63.1 GiB)', flush=True)
sys.exit(3)
ctx = '4096'
if '--max-context' in sys.argv: ctx = sys.argv[sys.argv.index('--max-context') + 1]
print('READY %s stop' % ctx, flush=True)
for line in sys.stdin:
if line.strip() == 'QUIT': break
if line.startswith(('GEN ', 'BGEN ')):
slot = int(line.split()[1]) if line.startswith('BGEN ') else None
t0 = time.time(); n = 0
for ch in os.environ.get('FAKE_REPLY', 'a'):
print('T %d' % ord(ch), flush=True); n += 1
if os.environ.get('FAKE_TOKEN_DELAY'): time.sleep(float(os.environ['FAKE_TOKEN_DELAY']))
print('DONE %d 12 300.0 %.1f length 0 0 0' % (n, max(1.0, (time.time() - t0) * 1000)), flush=True)
if slot is not None: print(f'BADM {slot} 0', flush=True)
`;
// one scenario = one fresh host with its own per-user folders
async function scenario(name, { installed = false, env = {} } = {}, body) {
if (!only.has(name)) return;
console.log(`--- ${name}`);
const dir = join(work, name);
const dirs = { home: join(dir, 'home'), appdata: join(dir, 'appdata'), local: join(dir, 'local'), components: join(dir, 'local', 'Strata', 'components'), install: join(dir, 'local', 'Strata', 'install'), state: join(dir, 'fake-state'), profiles: join(dir, 'profiles'), runtime: join(dir, 'runtime') };
for (const d of [dirs.home, dirs.appdata, dirs.local, dirs.components, dirs.install, dirs.state, join(dir, 'models')]) mkdirSync(d, { recursive: true });
const baseEnv = { ...process.env, STRATA_COMPONENTS_DIR: dirs.components, STRATA_PROFILE_DIR: dirs.profiles, STRATA_RUNTIME_DIR: dirs.runtime, STRATA_HOME: dirs.home, STRATA_MODELS_DIR: join(dir, 'models'), APPDATA: dirs.appdata, LOCALAPPDATA: dirs.local, USERPROFILE: dirs.home, XDG_CONFIG_HOME: join(dir, 'xdg'), XDG_DATA_HOME: join(dir, 'xdgdata'), PYTHONDONTWRITEBYTECODE: '1', STRATA_ENGINE_BACKEND: 'cuda', FAKE_STATE: dirs.state, ...env };
delete baseEnv.STRATA_API_KEY; delete baseEnv.STRATA_INSTALL_DIR;
if (installed) {
const platform = isWin ? 'windows-x86_64' : 'linux-x86_64';
const folder = join(dirs.components, 'engine', `0.1.45-${platform}-cuda`);
mkdirSync(folder, { recursive: true });
writeFileSync(join(folder, 'engine_main.py'), FAKE_ENGINE);
const entry = isWin ? 'engine.cmd' : 'engine';
writeFileSync(join(folder, entry), isWin ? `@echo off\r\n"${python}" "%~dp0engine_main.py" %*\r\n` : `#!/bin/sh\nexec "${python}" "$(dirname "$0")/engine_main.py" "$@"\n`);
const files = [entry, 'engine_main.py'].map(p => { const b = readFileSync(join(folder, p)); return { path: p, sha256: sha(b), bytes: b.length }; });
writeFileSync(join(folder, 'engine-component.json'), JSON.stringify({ schema_version: 1, id: 'strata-engine', version: '0.1.45', platform, backend: 'cuda', entrypoint: entry, files }));
const tok = join(dirs.install, 'tokenizer');
mkdirSync(tok, { recursive: true });
const script = `import json, sys\nfrom tools.strata_tokenizer import BYTE_TO_UNICODE\ntok = sys.argv[1]\ntokens = [BYTE_TO_UNICODE[i] for i in range(256)] + ['ab', '<|im_start|>', '<|im_end|>', '<|endoftext|>']\njson.dump({t: i for i, t in enumerate(tokens)}, open(tok + '/vocab.json', 'w'))\nopen(tok + '/merges.txt', 'w').write('a b')\njson.dump([0] * 257 + [3, 3, 3], open(tok + '/token_type.json', 'w'))\n`;
const made = spawnSync(python, ['-c', script, tok], { cwd: control, env: { ...baseEnv, PYTHONNOUSERSITE: '1' }, encoding: 'utf8' });
if (made.status !== 0) throw new Error('tokenizer fixture: ' + made.stderr);
writeFileSync(join(dirs.install, 'strata-iq3_s.json'), JSON.stringify({ exe: 'component:strata-engine', backend: 'cuda', tokenizer: tok, args: ['--max-context', '32768', '--pool-workers', '12'], cwd: dir, model_name: 'stub', host: '127.0.0.1', port: 0, kv: 'int8' }));
}
const port = env.FIXED_PORT ? Number(env.FIXED_PORT) : await freePort(), base = `http://127.0.0.1:${port}`;
const host = spawn(nodeExe, [join(repoHost, 'server', 'index.mjs')], { cwd: repoHost, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true,
env: { ...baseEnv, STRATA_PORT: String(port), STRATA_DIST: join(repoHost, 'dist'), STRATA_PACKAGED: '1', STRATA_REAL_ENGINE: '1', STRATA_NO_ACQUIRE: '1', STRATA_CONTROL_DIR: control, STRATA_SHARE_PORT: String(await freePort()), STRATA_TUNNEL_PORT: String(await freePort()) } });
let log = ''; host.stdout.on('data', c => { log += c; }); host.stderr.on('data', c => { log += c; });
const api = async (path, init) => { const r = await fetch(base + path, init); return { status: r.status, body: await r.json().catch(() => null) }; };
const browser = await chromium.launch({ executablePath: CHROME, headless: true });
try {
let up = false;
for (let i = 0; i < 200 && !up; i++) { try { up = (await api('/api/health')).status === 200; } catch { await delay(150); } }
check(up, `${name}: the packaged host starts`);
await api('/api/native?refresh=1');
await body({ base, api, browser, dirs, shot: (page, n) => page.screenshot({ path: join(shots, `${name}-${n}.png`) }), log: () => log });
} catch (error) {
check(false, `${name}: the flow`, String(error?.stack ?? error).slice(0, 700) + '\n' + log.slice(-600));
} finally {
await browser.close().catch(() => {});
host.kill(); await new Promise(r => host.once('exit', r));
}
}
const newPage = async (browser, base, options = {}) => {
const context = await browser.newContext({ viewport: { width: 1400, height: 950 }, ...(process.env.SCHEME ? { colorScheme: process.env.SCHEME } : {}), ...options });
const page = await context.newPage();
const problems = [];
page.on('pageerror', e => problems.push(e.message));
page.problems = problems;
await page.goto(base, { waitUntil: 'domcontentloaded' });
return page;
};
const clickText = async (page, re, timeout = 30000) => { const b = page.getByRole('button', { name: re }).first(); await b.waitFor({ timeout }); await b.click(); };
const noScroll = async page => page.evaluate(() => {
const root = document.querySelector('.guided-onboarding');
const bad = [];
for (let el = document.querySelector('.onboard-tune-stage, .onboard-prepare-stage, .onboard-model-stage') ?? root; el && el !== document.body; el = el.parentElement) {
const style = getComputedStyle(el);
if (/(auto|scroll)/.test(style.overflowY) && el.scrollHeight > el.clientHeight + 1) bad.push(`${el.className || el.tagName}: ${el.scrollHeight} > ${el.clientHeight}`);
}
return bad;
});
// ------------------------------------------------------------------------------------------------ download
await scenario('download', {}, async ({ base, api, browser, shot }) => {
const page = await newPage(browser, base);
await clickText(page, /get started/i);
await page.getByText(/Choose a version/i).waitFor({ timeout: 30000 });
await page.locator('[role=radio]').filter({ hasText: 'IQ3_S' }).first().click();
await clickText(page, /^Continue/);
await clickText(page, /Download model/i, 60000);
await page.locator('.onboard-progress progress').waitFor({ timeout: 30000 });
// ONE bar: sample the percent while it runs, it only goes up
const samples = [];
let eta = '';
for (let i = 0; i < 40 && !eta; i++) {
const state = (await api('/api/native')).body;
const d = state.jobs.at(-1)?.result?.install?.download;
if (d) samples.push(d.done_gb);
eta = (await page.locator('.onboard-progress-actions span').first().textContent({ timeout: 1000 }).catch(() => '')) ?? '';
await delay(1000);
}
await shot(page, '1-wizard-progress');
check(/About \d+ (min left|h)/.test(eta), 'the wizard shows about how many minutes are left', eta.trim());
const order = await page.evaluate(() => { const a = document.querySelector('.onboard-progress-actions'); const kids = a ? [...a.children].map(k => k.tagName + ':' + Math.round(k.getBoundingClientRect().left)) : []; return kids; });
check(order.length === 2 && order[0].startsWith('SPAN') && order[1].startsWith('BUTTON') && Number(order[0].split(':')[1]) < Number(order[1].split(':')[1]), 'the time left sits left of Stop', order.join(' '));
check(samples.length >= 3 && samples.every((v, i) => i === 0 || v >= samples[i - 1]), 'one combined count that only goes up', samples.map(v => v.toFixed(1)).join(', '));
const meta = await page.locator('.onboard-progress-meta span').first().textContent();
check(/GB/.test(meta ?? '') && /83\.6/.test(meta ?? ''), 'the bar says done / total over the whole model', meta);
// close the pop-up while it downloads
await clickText(page, /Finish later/i);
await page.waitForURL(/#\/models/, { timeout: 15000 }).catch(() => {});
await page.getByText(/Preparing your model/i).waitFor({ timeout: 15000 }).catch(() => {});
await delay(1500);
await shot(page, '2-models-progress');
await page.locator('.native-progress').first().screenshot({ path: join(shots, 'download-3-models-progress-card.png') }).catch(() => {});
check(await page.locator('.guided-onboarding').count() === 0, 'the setup pop-up closed');
check(await page.getByText(/Preparing your model/i).count() > 0, 'the Models screen shows the running download');
const still = (await api('/api/native')).body.jobs.at(-1);
check(still?.state === 'running', 'the download keeps running in the background', still?.state);
const modelsEta = await page.locator('.native-progress .onboard-progress-actions span').first().textContent({ timeout: 15000 }).catch(() => '');
check(/About \d+ (min left|h)/.test(modelsEta ?? ''), 'the Models screen shows the time left too', (modelsEta ?? '').trim());
await clickText(page, /^Stop$/);
await delay(2500);
check((await api('/api/native')).body.jobs.at(-1)?.state !== 'running', 'Stop stops it');
check(page.problems.length === 0, 'no page error', page.problems.join('; '));
});
// ------------------------------------------------------------------------------------------------ models
await scenario('models', { installed: true }, async ({ base, api, browser, shot }) => {
const page = await newPage(browser, base);
await clickText(page, /Go straight to the app/i);
await page.waitForURL(/#\/models/, { timeout: 15000 }).catch(() => {});
await page.getByText(/Conversation context/i).first().waitFor({ timeout: 30000 });
await delay(1500);
await shot(page, '1-models');
await page.locator('.version-trigger').first().waitFor({ timeout: 20000 }).catch(async () => { await shot(page, '0-models-missing-summary'); console.log('info page text:', JSON.stringify((await page.locator('body').innerText()).slice(0, 500))); });
const summary = await page.locator('.version-trigger').first().textContent({ timeout: 5000 }).catch(() => '');
// the Models tab fits the first view: nothing to scroll while no list is open and Expert mode is closed
if (await page.getByRole('button', { name: /^Close expert mode$/ }).count()) { await page.getByRole('button', { name: /^Close expert mode$/ }).click(); await delay(500); }
await shot(page, '1b-models-one-view');
const overflow = await page.evaluate(() => { const el = document.querySelector('.main__body'); return el ? el.scrollHeight - el.clientHeight : 0; });
check(overflow <= 2, 'the Models tab fits the first view without scrolling', String(overflow) + ' px over');
check(await page.locator('.family-switch').count() === 1, 'the model family switch is in the navbar');
check(await page.locator('h1').filter({ hasText: /^Model$/ }).count() === 0, 'and the navbar does not say "Model"');
check(/IQ3_S|High quality/.test(summary ?? ''), 'Models selects the model that is on this PC, not the recommendation', (summary ?? '').replace(/\s+/g, ' ').trim());
check(!/IQ3_XXS|Balanced/.test(summary ?? ''), 'the recommendation (IQ3_XXS) is not what is selected');
const load = page.getByRole('button', { name: /^Load model$/ }).first();
await load.click();
await delay(1200);
check(await page.getByRole('dialog').filter({ hasText: /Download and prepare/i }).count() === 0, 'loading an installed model opens no download pop-up');
let ready = false;
for (let i = 0; i < 90 && !ready; i++) { const s = (await api('/api/native')).body; ready = s.instances.some(x => x.status === 'ready'); const err = s.instances.find(x => x.status === 'error'); if (err) throw new Error('load failed: ' + err.error); await delay(1000); }
if (!ready) { const st = (await api('/api/native')).body; console.log('info jobs', JSON.stringify(st.jobs.map(j => [j.kind, j.state, j.error ?? j.summary])), 'instances', JSON.stringify(st.instances.map(i => [i.status, i.error]))); await shot(page, '2-models-after-click'); }
check(ready, 'the model loads (a fake engine under the real Control)');
await page.reload({ waitUntil: 'domcontentloaded' });
await page.getByRole('button', { name: /^Unload$/ }).first().waitFor({ timeout: 30000 }).catch(() => {});
await shot(page, '3-models-loaded');
check(await page.getByRole('button', { name: /^Unload$/ }).count() > 0, 'after a reload the Models screen shows the model as loaded, with Unload');
check(await page.getByRole('button', { name: /^Load model$/ }).count() === 0, 'and no longer offers Load model');
await page.getByRole('button', { name: /^Unload$/ }).first().click();
let gone = false;
for (let i = 0; i < 40 && !gone; i++) { gone = (await api('/api/native')).body.instances.length === 0; await delay(500); }
check(gone, 'Unload stops the engine');
await page.getByRole('button', { name: /^Load model$/ }).first().waitFor({ timeout: 15000 }).catch(() => {});
check(await page.getByRole('button', { name: /^Load model$/ }).count() > 0, 'and Load model is back');
check(page.problems.length === 0, 'no page error', page.problems.join('; '));
});
// ------------------------------------------------------------------------------------------------ the profile used, the popover, the pop-up
await scenario('profile', { installed: true }, async ({ base, api, browser, shot }) => {
const page = await newPage(browser, base);
await clickText(page, /Go straight to the app/i);
await page.getByText(/Conversation context/i).first().waitFor({ timeout: 30000 });
await delay(1500);
const work = () => page.getByRole('button', { name: /Leave 20% for my work/ }).first();
const full = () => page.getByRole('button', { name: /Full performance/ }).first();
await work().click();
check(await work().getAttribute('aria-pressed') === 'true', 'Leave 20% is chosen before loading');
await page.getByRole('button', { name: /^Load model$/ }).first().click();
let ready = false;
for (let i = 0; i < 90 && !ready; i++) { const s = (await api('/api/native')).body; ready = s.instances.some(x => x.status === 'ready'); await delay(500); }
check(ready, 'the model loads');
const inst = (await api('/api/native')).body.instances.find(x => x.status === 'ready');
check(inst?.resourceProfile === 'work', 'the running model reports the profile it was started with', String(inst?.resourceProfile));
await delay(1500);
check(await work().getAttribute('aria-pressed') === 'true' && await full().getAttribute('aria-pressed') === 'false', 'after the load the page still shows Leave 20%, not Full performance');
await page.reload({ waitUntil: 'domcontentloaded' });
await page.getByRole('button', { name: /^Unload$/ }).first().waitFor({ timeout: 30000 });
await delay(1000);
check(await work().getAttribute('aria-pressed') === 'true', 'and after a reload it is still the profile in use');
await shot(page, '1-profile-used');
// the reasoning popover names the loaded model
await page.evaluate(() => { location.hash = '#/chat'; });
const effort = page.getByRole('button', { name: /^Reasoning effort/ }).first();
await effort.waitFor({ timeout: 30000 });
await effort.click();
const header = await page.locator('.reasoning-menu-model strong').first().textContent({ timeout: 10000 });
check(header && !/No model loaded/i.test(header), 'the Reasoning popover names the loaded model, not "No model loaded"', String(header));
await shot(page, '2-reasoning-popover');
await page.keyboard.press('Escape');
// an installed model asked for with other settings: the pop-up never speaks of downloading
await page.evaluate(() => { location.hash = '#/models'; });
await page.getByRole('button', { name: /^Unload$/ }).first().waitFor({ timeout: 30000 });
await page.getByRole('button', { name: /^Unload$/ }).first().click();
await page.getByRole('button', { name: /^Load model$/ }).first().waitFor({ timeout: 30000 });
await page.getByRole('combobox', { name: 'Conversation context' }).first().click();
const options = page.getByRole('option');
const pick = (await options.allTextContents()).findIndex(t => /^8K$/.test(t.trim()));
if (pick >= 0) await options.nth(pick).click();
await page.getByRole('button', { name: /^Load model$|^Checking/ }).first().click();
const dialog = page.getByRole('dialog').first();
await dialog.waitFor({ timeout: 15000 }).catch(() => {});
const text = (await dialog.textContent().catch(() => '')) ?? '';
await shot(page, '3-apply-dialog');
if (text) {
check(!/Download and prepare|Download . load|approximately|[0-9] GB/i.test(text), 'for a downloaded model the pop-up has no download wording or size', text.replace(/\s+/g, ' ').slice(0, 600));
check(/Apply/.test(text), 'it says what happens: apply settings and load');
} else console.log('info the settings were the saved ones, so loading needed no pop-up at all');
check(page.problems.length === 0, 'no page error', page.problems.join('; '));
});
// ------------------------------------------------------------------------------------------------ version list, This computer, Tune, a task
for (const scheme of ['light', 'dark']) await scenario(scheme === 'light' ? 'ui2' : 'ui2d', { installed: true, env: { FAKE_TOKEN_DELAY: '0.02', FAKE_REPLY: 'I am thinking about it. </think> ' + 'Here is the answer, word by word. '.repeat(12), STRATA_TUNE_NO_QUIET_CHECK: '1' } }, async ({ base, api, browser, shot }) => {
const page = await newPage(browser, base, { colorScheme: scheme });
await clickText(page, /Go straight to the app/i);
await page.getByText(/Conversation context/i).first().waitFor({ timeout: 30000 });
await delay(1500);
// the version list: Recommended (the fixed four), then folded groups, one line per model
console.log('info triggers', await page.locator('.version-trigger').count(), await page.getByRole('button', { name: 'Model version' }).count());
await page.locator('.version-trigger').first().click();
await delay(300);
console.log('info expanded after click', await page.locator('.version-trigger').first().getAttribute('aria-expanded'));
await delay(800); await shot(page, `0-after-click-${scheme}`);
console.log('info menu', await page.evaluate(() => { const m = document.querySelector('.version-menu'); const t = document.querySelector('.version-trigger'); return JSON.stringify({ menu: m ? m.getBoundingClientRect() : null, parent: m ? [m.parentElement.className, getComputedStyle(m.parentElement).display, getComputedStyle(m.parentElement).visibility, m.parentElement.style.cssText.slice(0, 120)] : null, expanded: t?.getAttribute('aria-expanded'), roles: [...document.querySelectorAll('[role=menu]')].length }); }));
await page.locator('.version-menu').waitFor({ timeout: 10000 });
const heads = await page.locator('.version-head span:first-child, .version-group__head').allTextContents();
const names = await page.locator('.version-menu > .version-row .version-row__name strong').allTextContents();
console.log('info version list', JSON.stringify(heads.map(t => t.replace(/\s+/g, ' ').trim())), JSON.stringify(names));
check(heads[0]?.trim() === 'Recommended', `${scheme}: the list starts with Recommended`);
check(names.length >= 1 && names.length <= 4, `${scheme}: Recommended shows only the picks this catalog has (at most the fixed four)`, names.join(' | '));
check(heads.some(t => /Swift models|GSQ-RCO models|Gyro models|NVFP4 models|Unsloth models/.test(t)), `${scheme}: the other models are in folded groups`);
check(heads.at(-1)?.includes('Use your own GGUF'), `${scheme}: the last row is Use your own GGUF`);
const tall = await page.locator('.version-row').evaluateAll(els => els.filter(e => e.getBoundingClientRect().height > 44).length);
check(tall === 0, `${scheme}: every model is one line`, String(tall));
await shot(page, `1-versions-${scheme}`);
await page.keyboard.press('Escape');
// This computer
const pc = page.locator('.pc-card');
await pc.waitFor({ timeout: 10000 });
const pcText = ((await pc.textContent()) ?? '').replace(/\s+/g, ' ');
check(/This computer/.test(pcText) && /(Fits fully in memory|read from the SSD)/.test(pcText), `${scheme}: This computer says in one line whether it fits`, pcText.slice(0, 200));
check(await pc.locator('.pc-meter').count() >= 1, `${scheme}: it shows the memory bars`);
check(await pc.locator('.chip, .st-pill, [class*="badge"]').count() === 0, `${scheme}: no chips or badges in it`);
await shot(page, `2-computer-${scheme}`);
// Tune: five steps, a live speed, a result card
await page.getByRole('button', { name: /^Tune model$/ }).first().click({ timeout: 10000 });
await delay(1500); await shot(page, `2b-tune-dialog-${scheme}`);
// The fake engine has no other setting worth trying, so its tune passes through "trying" too fast to be seen. The page is shown the progress a
// real tune reports (Control's status file: stage, speeds) for the running view; the start, the result and every other call are real.
let fakePhase = 'run';
await page.route('**/api/native*', async route => {
const response = await route.fetch(); let json = null;
try { json = await response.json(); } catch { return route.fulfill({ response }); }
for (const job of json?.jobs ?? []) if (job.kind === 'autotune' && fakePhase === 'done') { job.state = 'done'; job.progress = null; job.result = { outcome: 'improved', gain_pct: 18, base: { decode_tok_s: 41.2 }, chosen: { decode_tok_s: 48.6 }, preset: { id: 'fast', name: 'Fast · 64K' }, answers: { class: 'exact', fields: [] } }; } else if (job.kind === 'autotune' && job.state === 'running') job.progress = { stage: 'rounds', text: null, minutesLeft: 9, runsDone: 3, runsPlanned: 6, elapsedS: 300, step: 'Trying: more experts kept in memory (round 1 of 2)', speeds: [{ label: 'base', tok_s: 41.2 }, { label: 'Fewer helper threads', tok_s: 40.1 }, { label: 'More experts kept in memory', tok_s: 46.8 }] };
return route.fulfill({ response, json });
});
await page.getByRole('button', { name: /Start tuning|Tune again/ }).first().click({ timeout: 15000 });
await page.locator('.tune-step').first().waitFor({ timeout: 60000 });
check(await page.locator('.tune-step').count() === 5, `${scheme}: the tuner shows its five steps`);
let sawRun = false, sawSpeed = false;
for (let i = 0; i < 400; i++) {
const done = await page.locator('.tune-result, .tune-progress').count();
if (i % 4 === 0) { const jobs = (await api('/api/native')).body?.jobs ?? []; const j = jobs.find(x => x.kind === 'autotune'); if (j) console.log('info tune job', j.state, JSON.stringify(j.progress ?? null).slice(0, 300)); }
if (await page.locator('.tune-step[data-s=run]').count()) sawRun = true;
const big = await page.locator('.tune-live__big').first().textContent().catch(() => '');
if (/[0-9]/.test(big ?? '')) sawSpeed = true;
if (sawRun && sawSpeed) { await delay(700); await shot(page, `3-tune-running-${scheme}`); break; }
if (done) break;
await delay(600);
}
check(sawRun, `${scheme}: a step is shown as running`);
check(sawSpeed, `${scheme}: the live speed shows a number while it runs`);
if (sawSpeed) { fakePhase = 'done'; await page.locator('.tune-outcome').waitFor({ timeout: 15000 }).catch(() => {}); check(/18% faster/.test((await page.locator('.tune-outcome__body h3').textContent().catch(() => '')) ?? ''), `${scheme}: the result card shows the gain as its headline`); check(/Fast/.test((await page.locator('.tune-outcome__facts').textContent().catch(() => '')) ?? ''), `${scheme}: and what it was saved as`); }
await shot(page, `4-tune-result-${scheme}`);
await page.getByRole('button', { name: /^Close$/ }).first().click({ timeout: 10000 }).catch(() => {});
// a task: the opened task is skimmable, with pause / delete / Edit / Run now
const made = await api('/api/tasks', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ title: 'Open Downloads folder', prompt: 'Open my Downloads folder and tell me what is new.', frequency: 'weekdays', at: '08:00', weekday: 1, timeZone: 'Europe/Ljubljana', mode: 'manual', enabled: true }) });
const id = made.body?.id ?? made.body?.task?.id;
check(!!id, `${scheme}: a task is created`, JSON.stringify(made.body).slice(0, 120));
await page.evaluate(i => { location.hash = '#/tasks/' + i; }, id);
await page.getByRole('button', { name: /^Edit$/ }).first().waitFor({ timeout: 15000 });
console.log('info header buttons', await page.getByRole('button', { name: /^Pause / }).count(), await page.getByRole('button', { name: /^Delete / }).count(), await page.getByRole('button', { name: /Run now/ }).count(), JSON.stringify(await page.locator('.task-header-actions button').evaluateAll(b => b.map(x => (x.getAttribute('aria-label') ?? x.textContent ?? '').trim()))));
check(await page.getByRole('button', { name: /^Pause / }).count() === 1 && await page.getByRole('button', { name: /^Delete / }).count() >= 1 && await page.getByRole('button', { name: /Run now/ }).count() >= 1, `${scheme}: pause, delete, Edit and Run now are in the header`);
check(await page.locator('.main__back').count() === 0 && await page.locator('.main__title-line .task-state').count() === 0, `${scheme}: nothing sits on the left of an opened task's navbar`);
const factsText = ((await page.locator('.task-facts').textContent()) ?? '').replace(/\s+/g, ' ');
check(/Weekdays/.test(factsText) && /Schedule/i.test(factsText) && /Last result/i.test(factsText), `${scheme}: schedule, what it may do and the last result are at the top`, factsText.slice(0, 160));
await shot(page, `5-task-${scheme}`);
await page.getByRole('button', { name: /^Pause / }).first().click();
await page.getByRole('button', { name: /^Resume / }).first().waitFor({ timeout: 10000 });
check(true, `${scheme}: Pause becomes Resume`);
check(page.problems.length === 0, `${scheme}: no page error`, page.problems.join('; '));
});
// ------------------------------------------------------------------------------------------------ tune + reading size
await scenario('tune', { installed: true, env: { FAKE_TOKEN_DELAY: '0.02', FAKE_REPLY: ['I am thinking about it.', '</think>', '', 'Here is a list:', '', '- one', '- two', '', '## Heading', '', 'Some `code` and **bold** text.', '', '| a | b |', '|---|---|', '| 1 | 2 |', '', '```', 'code line', '```', ''].join(String.fromCharCode(10)) } }, async ({ base, api, browser, dirs, shot }) => {
const page = await newPage(browser, base);
await clickText(page, /get started/i);
await page.getByText(/Choose a version/i).waitFor({ timeout: 30000 });
await page.locator('[role=radio]').filter({ hasText: 'IQ3_S' }).first().click();
await clickText(page, /^Continue/);
await page.getByText(/Leave 20% for my work/i).waitFor({ timeout: 30000 });
await delay(800);
await shot(page, '1-tune-step');
check(await page.getByText(/Read pictures when this model loads/i).count() === 0, 'the tuning step offers no picture reading for an app install');
await clickText(page, /Tune & start chatting/i);
let done = false, job = null;
for (let i = 0; i < 120 && !done; i++) { job = (await api('/api/native')).body.jobs.findLast?.(j => j.kind === 'tune') ?? null; done = !!job && job.state !== 'running'; await delay(1000); }
check(job?.state === 'done', 'tuning finishes without a vision error', job?.error ?? job?.state);
const cfg = JSON.parse(readFileSync(join(dirs.install, 'strata-iq3_s.json'), 'utf8'));
check(cfg.args.includes('--resident-budget-gib'), '"leave 20%" wrote a RAM budget for the experts', cfg.args.join(' '));
const gib = Number(cfg.args[cfg.args.indexOf('--resident-budget-gib') + 1]);
check(gib + 10 <= 63.1 * 0.8, 'the budget keeps the model within 80% of the PC RAM', `${gib} GiB`);
// chat + reading size
await page.getByRole('textbox', { name: /Message Strata/i }).waitFor({ timeout: 60000 });
await page.getByRole('textbox', { name: /Message Strata/i }).fill('hello');
await page.keyboard.press('Enter');
await page.locator('.st-msg--assistant .st-bubble').first().waitFor({ timeout: 60000 });
await page.locator('.st-msg--assistant .st-bubble p').first().waitFor({ timeout: 60000 }).catch(async () => console.log('info reply so far:', JSON.stringify(await page.locator('.st-msg--assistant').first().innerText()).slice(0, 300)));
await page.waitForFunction(() => !document.querySelector('.st-msg--assistant .caret'), null, { timeout: 90000 }).catch(() => {});
// the chat's telemetry chips: speed and context
const chips = page.locator('.telemetry__chip');
const chipTexts = await chips.allInnerTexts();
console.log('info chips', JSON.stringify(chipTexts));
let speedText = '', contextText = '';
for (let i = 0; i < await chips.count(); i++) {
await chips.nth(i).click();
await delay(400);
const pop = await page.locator('.telepop').last().innerText().catch(() => '');
if (/tok\/s|speed/i.test(pop) && !speedText) speedText = pop;
if (/context/i.test(pop) && !contextText) contextText = pop;
await page.keyboard.press('Escape');
}
const tps = Number(speedText.match(/([\d.]+)\s*tok(?:ens)?\/s/)?.[1] ?? NaN);
check(tps > 15 && tps < 120, 'the speed shown is the decode speed (the engine paced at ~50 tokens a second)', speedText.replace(/\s+/g, ' ').slice(0, 160));
check(/32\.8K|32K|32,768|32768/.test(contextText) && !/128K/.test(contextText), 'the context shown is the one the model runs with (32K), not 128K', contextText.replace(/\s+/g, ' ').slice(0, 160));
const sizes = [];
for (const px of [12, 14, 16, 22]) {
await page.evaluate(v => { const k = 'strata.prefs.v2'; const cur = JSON.parse(localStorage.getItem(k) || '{}'); localStorage.setItem(k, JSON.stringify({ ...cur, contentFontSize: v })); }, px);
await page.reload({ waitUntil: 'domcontentloaded' });
await page.locator('.st-msg--assistant .st-bubble').first().waitFor({ timeout: 30000 });
await page.screenshot({ path: join(shots, `tune-3-chat-${px}px.png`) });
sizes.push(await page.evaluate(() => { const b = document.querySelector('.st-msg--assistant .st-bubble'); const t = b.querySelector('p, .md-li, span') ?? b; const u = document.querySelector('.st-msg--user .st-bubble'); const all = {}; for (const el of b.querySelectorAll('*')) { if (!el.childNodes.length || ![...el.childNodes].some(n => n.nodeType === 3 && n.textContent.trim())) continue; all[el.tagName.toLowerCase() + (el.className ? '.' + String(el.className).split(' ')[0] : '')] = parseFloat(getComputedStyle(el).fontSize); } const extra = {}; for (const [k, q] of Object.entries({ reasoning: '.st-msg--assistant .st-collapse__body', codeblock: '.st-msg--assistant .st-code pre', codehead: '.st-msg--assistant .st-code__head' })) { const e = document.querySelector(q); if (e) extra[k] = parseFloat(getComputedStyle(e).fontSize); } Object.assign(all, extra); const chain = []; for (let e = b.querySelector('p'); e && e !== b; e = e.parentElement) chain.push(e.tagName + '.' + String(e.className).split(' ')[0] + '=' + getComputedStyle(e).fontSize); all.chain = chain; globalThis.__all = all; return [parseFloat(getComputedStyle(b).fontSize), parseFloat(getComputedStyle(t).fontSize), parseFloat(getComputedStyle(u).fontSize), all]; }));
}
console.log('info chain', JSON.stringify(await page.evaluate(() => { const el = document.querySelector('.st-msg--assistant .st-bubble p'); const out = []; for (let e = el; e && !e.classList.contains('st-msg'); e = e.parentElement) out.push(e.tagName + '.' + String(e.className).split(' ')[0] + '=' + getComputedStyle(e).fontSize); return out; })));
// which visible text does NOT follow the reading size? (same elements at 12 and 22)
const survey = async px => { await page.evaluate(v => { const k = 'strata.prefs.v2'; const cur = JSON.parse(localStorage.getItem(k) || '{}'); localStorage.setItem(k, JSON.stringify({ ...cur, contentFontSize: v })); }, px); await page.reload({ waitUntil: 'domcontentloaded' }); await page.locator('.st-msg--assistant .st-bubble').first().waitFor({ timeout: 30000 }); await delay(600);
return page.evaluate(() => { const out = {}; for (const el of document.querySelectorAll('main *, .chat *, textarea')) { if (![...el.childNodes].some(n => n.nodeType === 3 && n.textContent.trim()) && el.tagName !== 'TEXTAREA') continue; const r = el.getBoundingClientRect(); if (!r.width || !r.height) continue; const key = el.tagName.toLowerCase() + '.' + String(el.className).split(' ')[0] + ' <' + (el.parentElement?.className ?? '').toString().split(' ')[0] + '>'; out[key] = parseFloat(getComputedStyle(el).fontSize); } return out; }); };
const a = await survey(12), b = await survey(22);
// a fresh app (nothing saved) uses 14 px
await page.evaluate(() => localStorage.removeItem('strata.prefs.v2'));
await page.reload({ waitUntil: 'domcontentloaded' });
await page.locator('.st-msg--assistant .st-bubble').first().waitFor({ timeout: 30000 });
check(await page.evaluate(() => getComputedStyle(document.querySelector('.st-msg--assistant .st-bubble')).fontSize) === '14px', 'the default reading size is 14 px');
const fixed = Object.keys(a).filter(k => k in b && a[k] === b[k]);
console.log('info text that does not follow the reading size:', JSON.stringify(fixed.map(k => k + '=' + a[k])));
await shot(page, '2-chat-22px');
console.log('info sizes', JSON.stringify(sizes), JSON.stringify(await page.locator('.st-msg--assistant .st-bubble').first().innerHTML()).slice(0, 400));
check(sizes[0][1] === 12 && sizes[1][1] === 14 && sizes[2][1] === 16 && sizes[3][1] === 22 && sizes[0][2] === 12 && sizes[3][2] === 22, 'the reading size changes the text size of the conversation', JSON.stringify(sizes));
// the slider itself
await page.evaluate(() => { location.hash = '#/chat?settings=1'; });
const slider = page.getByLabel('Reading text size');
if (await slider.count()) {
await slider.first().focus();
for (let i = 0; i < 4; i++) await page.keyboard.press('ArrowRight');
await delay(500);
const now = await page.evaluate(() => getComputedStyle(document.querySelector('.st-msg--assistant .st-bubble') ?? document.body).fontSize);
check(parseFloat(now) === 18, 'moving the slider changes the conversation text at once', now);
const box = await slider.first().boundingBox();
await page.mouse.click(box.x + box.width * 0.9, box.y + box.height / 2);
await delay(400);
const dragged = await page.evaluate(() => [getComputedStyle(document.querySelector('.st-msg--assistant .st-bubble') ?? document.body).fontSize, getComputedStyle(document.documentElement).getPropertyValue('--st-content-fs'), JSON.parse(localStorage.getItem('strata.prefs.v2') || '{}').contentFontSize]);
check(parseFloat(dragged[0]) >= 20 && parseFloat(dragged[2]) >= 20, 'clicking the slider with the mouse changes it too, and it is saved', dragged.join(' '));
} else console.log('info the settings slider was not reachable by hash here; the stored size was checked above');
check(page.problems.length === 0, 'no page error', page.problems.join('; '));
});
// ------------------------------------------------------------------------------------------------ delete
await scenario('delete', { installed: true }, async ({ base, api, browser, dirs, shot }) => {
const page = await newPage(browser, base);
await clickText(page, /Go straight to the app/i);
await page.waitForURL(/#\/models/, { timeout: 15000 }).catch(() => {});
await page.getByRole('button', { name: /^Load model$/ }).first().click();
let ready = false;
for (let i = 0; i < 90 && !ready; i++) { ready = (await api('/api/native')).body.instances.some(x => x.status === 'ready'); await delay(1000); }
check(ready, 'a model is loaded');
await clickText(page, /Manage models/i);
await page.getByRole('button', { name: /^Delete$/ }).first().click();
await delay(600);
await shot(page, '1-delete-confirm');
const dialog = await page.getByRole('dialog').last().innerText();
check(/Delete this version\?/.test(dialog) && /Remove 83\.6 GB of downloaded files\. You can download this version again\./.test(dialog), 'the confirm dialog uses the existing words and the real size', dialog.replace(/\s+/g, ' ').slice(0, 600));
check(/running model will be unloaded/i.test(dialog), 'and says the loaded model is unloaded first');
await page.getByRole('button', { name: /^Delete version$/ }).click();
let state = null;
for (let i = 0; i < 40; i++) { state = (await api('/api/native')).body; if (state.instances.length === 0 && !state.installed?.models?.some(m => m.model === 'iq3_s')) break; await delay(500); }
check(state.instances.length === 0, 'the loaded model was unloaded');
check(!existsSync(join(dirs.install, 'strata-iq3_s.json')), 'the installer removed the run config of the model');
check(!state.installed?.models?.some(m => m.model === 'iq3_s'), 'the model is no longer listed as downloaded');
await shot(page, '2-after-delete');
});
// ------------------------------------------------------------------------------------------------ perf
await scenario('perf', { installed: true }, async ({ base, api, browser, shot }) => {
const page = await newPage(browser, base);
await clickText(page, /Go straight to the app/i);
await page.waitForURL(/#\/models/, { timeout: 15000 }).catch(() => {});
await page.getByRole('button', { name: /^Load model$/ }).first().click();
let ready = false;
for (let i = 0; i < 90 && !ready; i++) { ready = (await api('/api/native')).body.instances.some(x => x.status === 'ready'); await delay(1000); }
check(ready, 'a model is loaded');
await delay(2500);
const requests = [];
const started = new Map();
page.on('request', r => started.set(r, Date.now()));
page.on('requestfinished', r => requests.push([r.url().replace(base, '').slice(0, 60), Date.now() - (started.get(r) ?? Date.now())]));
await page.evaluate(() => { window.__long = []; new PerformanceObserver(l => { for (const e of l.getEntries()) window.__long.push(Math.round(e.duration)); }).observe({ entryTypes: ['longtask'] }); });
const t0 = Date.now();
await page.evaluate(() => { location.hash = '#/chat'; });
await page.getByRole('textbox', { name: /Message Strata/i }).waitFor({ timeout: 30000 });
const open = Date.now() - t0;
await delay(1500);
const long = await page.evaluate(() => window.__long);
console.log('info chat opened in', open, 'ms; long tasks', JSON.stringify(long), 'requests', JSON.stringify(requests.filter(r => r[1] > 100)), 'all', requests.length);
check(open < 1500 && Math.max(0, ...long) < 300, 'Models to Chat opens at once, with no long block', open + ' ms, longest task ' + Math.max(0, ...long) + ' ms');
});
// ------------------------------------------------------------------------------------------------ polish
await scenario('polish', { installed: true }, async ({ base, api, browser, dirs, shot }) => {
const page = await newPage(browser, base);
await clickText(page, /Go straight to the app/i);
await page.waitForURL(/#\/models/, { timeout: 15000 }).catch(() => {});
await page.getByText(/Conversation context/i).first().waitFor({ timeout: 30000 });
check(await page.getByRole('button', { name: /^Tune model$/ }).count() === 1, 'the Models tab has the Tune model button (the real tuner, not the old short benchmark)');
await clickText(page, /Manage models/i);
await delay(500);
const rows = await page.locator('.native-manage-row').count();
check(rows === 1, 'Manage models lists only the downloaded model, not the catalog', rows + ' row(s)');
await shot(page, '1-manage-models');
await page.getByRole('button', { name: /^Done$/ }).click();
await page.getByRole('button', { name: /Other local models/i }).click().catch(async () => { await clickText(page, /Manage models/i); await clickText(page, /Use a local GGUF/i); });
await page.getByText(/Folder with the GGUF files/i).first().waitFor({ timeout: 15000 }).catch(() => {});
await shot(page, '2-local-gguf');
check(await page.getByText(/Folder with the GGUF files/i).count() > 0, 'the local GGUF page opens in the same design as Models');
await page.getByPlaceholder(/Full path to the folder/i).fill(join(dirs.install, 'does-not-exist'));
await page.getByRole('button', { name: /^Check files$/ }).click();
await delay(3000);
check(await page.locator('.error-notice').count() > 0, 'a folder that cannot be used says so in words');
await shot(page, '2b-local-gguf-problem');
await page.evaluate(() => { location.hash = '#/chat?settings=1'; });
await delay(1500);
const text = await page.locator('body').innerText();
check(!/Contrast/.test(text), 'Settings has no Contrast option');
check(!/View request log/.test(text), 'and no "View request log" button');
await page.getByText(/^Engine$/).first().click().catch(() => {});
await delay(1500);
await shot(page, '3-settings-engine');
const engine = await page.locator('body').innerText();
check(/Update component/.test(engine) && /Diagnostics/.test(engine), 'Settings > Engine has Update component and Diagnostics');
check(!/Performance|Model settings|Calibrate|Engine installation/.test(engine), 'and nothing else');
await page.getByRole('button', { name: /^View logs$/ }).click(); // the logs open in a pop-up that holds Save logs
await page.getByRole('button', { name: /^Save logs$/ }).click();
await page.getByText(/Saved to/).first().waitFor({ timeout: 15000 }).catch(() => {});
const savedText = await page.getByText(/Saved to/).first().innerText().catch(() => '');
const savedPath = savedText.replace(/^Saved to\s*/, '').trim();
check(!!savedPath && existsSync(savedPath), 'Save logs writes a file to Downloads and says where', savedPath);
await shot(page, '4-logs-saved');
await page.evaluate(() => { location.hash = '#/monitor'; });
await delay(6000);
await shot(page, '5-monitor');
const mon = await page.locator('body').innerText();
check(/°C/.test(mon), 'the Monitor shows GPU data with no model loaded', (mon.match(/.{0,30}°C.{0,20}/)?.[0] ?? 'no temperature text').replace(/\s+/g, ' '));
});
// ------------------------------------------------------------------------------------------------ stop
await scenario('stop', { installed: true, env: { FAKE_TOKEN_DELAY: '0.1', FAKE_REPLY: 'thinking '.repeat(30) } }, async ({ base, api, browser, shot }) => {
const page = await newPage(browser, base);
await clickText(page, /Go straight to the app/i);
await page.waitForURL(/#\/models/, { timeout: 15000 }).catch(() => {});
await page.getByRole('button', { name: /^Load model$/ }).first().click();
let ready = false;
for (let i = 0; i < 90 && !ready; i++) { ready = (await api('/api/native')).body.instances.some(x => x.status === 'ready'); await delay(1000); }
if (!ready) { const st = (await api('/api/native')).body; console.log('info jobs', JSON.stringify(st.jobs.map(j => [j.kind, j.state, j.error ?? j.summary])), JSON.stringify(st.instances.map(i => [i.status, i.error]))); await shot(page, '0-stop-notloaded'); }
check(ready, 'a model is loaded');
await page.evaluate(() => { location.hash = '#/chat'; });
const box = page.getByRole('textbox', { name: /Message Strata/i });
await box.waitFor({ timeout: 30000 });
await box.fill('hello');
await page.keyboard.press('Enter');
await page.locator('.typing').first().waitFor({ timeout: 30000 });
await delay(1500);
await page.getByRole('button', { name: /^Stop/i }).first().click().catch(async () => { await page.keyboard.press('Escape'); });
await delay(4000);
const dots = await page.locator('.typing').count();
await shot(page, '1-after-stop');
check(dots === 0, 'after Stop in the middle of thinking the three dots are gone', dots + ' left');
});
// ------------------------------------------------------------------------------------------------ latest
await scenario('latest', {}, async ({ base, browser, shot }) => {
const page = await newPage(browser, base);
const now = Date.now();
await page.evaluate(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 }]));
localStorage.setItem('strata.active-session.v1', 's_latest');
}, now);
await page.reload({ waitUntil: 'domcontentloaded' });
await delay(1500);
if (await page.getByRole('button', { name: /Go straight to the app/i }).count()) { await clickText(page, /Go straight to the app/i); await delay(1500); }
await page.goto(base + '/#/chat', { waitUntil: 'domcontentloaded' });
await page.getByRole('textbox', { name: /Message Strata/i }).waitFor({ timeout: 30000 });
await delay(800);
await page.locator('.main__body.chat').first().waitFor({ timeout: 15000 }).catch(async () => { await shot(page, '0-latest-missing'); console.log('info body', JSON.stringify((await page.locator('body').innerText()).slice(0, 300))); });
await page.locator('.main__body.chat').first().evaluate(el => { el.scrollTop = 3000; });
await delay(600);
await page.mouse.wheel(0, -400);
await delay(600);
await page.locator('.main__body.chat').first().evaluate(el => { el.scrollTop = 0; });
await delay(800);
const gaps = await page.evaluate(() => { const b = document.querySelector('.jumplatest'), c = document.querySelector('.main__body.chat'); if (!b || !c) return null; const r = b.getBoundingClientRect(), cr = c.getBoundingClientRect(); return { top: Math.round(r.top - cr.top), bottom: Math.round(cr.bottom - r.bottom) }; });
console.log('info chat dom', JSON.stringify(await page.evaluate(() => { const c = document.querySelector('.main__body.chat'); return { msgs: document.querySelectorAll('.st-msg').length, scrollH: c?.scrollHeight, clientH: c?.clientHeight, top: c?.scrollTop, jump: document.querySelectorAll('.jumplatest').length }; })));
await shot(page, '1-latest-button');
check(!!gaps && gaps.top >= 12 && gaps.bottom >= 12, 'the Latest button keeps a gap from the top navigation and from the bottom', JSON.stringify(gaps));
});
// ------------------------------------------------------------------------------------------------ fail
await scenario('fail', { installed: true, env: { FAKE_ENGINE_FAIL: '1' } }, async ({ base, api, browser, shot }) => {
const page = await newPage(browser, base);
await clickText(page, /get started/i);
await page.getByText(/Choose a version/i).waitFor({ timeout: 30000 });
await page.locator('[role=radio]').filter({ hasText: 'IQ3_S' }).first().click();
await clickText(page, /^Continue/);
await clickText(page, /Tune & start chatting/i, 30000);
await page.getByText(/ran low on memory/i).first().waitFor({ timeout: 120000 });
await delay(1000);
await shot(page, '1-tune-error');
const text = await page.locator('.guided-onboarding').innerText();
check(/ran low on memory/.test(text), 'the error says in words that the PC ran low on memory');
check(/Leave 20% for my work/.test(text) && /low-RAM/.test(text), 'and what to try');
check(!/server_exited|memory guard/.test(text), 'no code or log line in the main text');
check(!/vision files/i.test(text), 'no picture-reading error');
const bad = await noScroll(page);
check(bad.length === 0, 'the tuning pop-up does not scroll when an error shows', bad.join('; '));
await page.locator('.error-notice summary').first().click();
await delay(500);
await shot(page, '2-tune-error-details');
const details = await page.locator('.error-notice pre').first().innerText();
check(/server_exited/.test(details) && /exited before/.test(details), 'Details holds the code and the reason the server logged', details.replace(/\s+/g, ' ').slice(0, 120));
const badOpen = await noScroll(page);
check(badOpen.length === 0, 'and the pop-up still does not scroll with Details open', badOpen.join('; '));
await page.setViewportSize({ width: 1280, height: 700 });
await delay(600);
const small = await noScroll(page);
check(small.length === 0, 'nor in a small window (1280x700), Details open', small.join('; '));
await shot(page, '3-tune-error-small-window');
void api;
});
// ------------------------------------------------------------------------------------------------ restart
// The app keeps its theme, text size, language and chat list in the browser's storage, which belongs to the window's origin
// (127.0.0.1:<port>). The shell now uses the same port every launch: stop the host, start it again on that port, reopen the
// window with the same browser profile, and everything is still there.
if (only.has('restart')) {
console.log('--- restart');
const fixed = await freePort();
const profile = join(work, 'browser-profile');
const run = async (body) => scenario('restart', { env: { FIXED_PORT: String(fixed) } }, async ctx => {
const context = await chromium.launchPersistentContext(profile, { executablePath: CHROME, headless: true, viewport: { width: 1200, height: 800 } });
try { await body(context, ctx); } finally { await context.close(); }
});
only.add('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 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 }));
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);
});
}
// ------------------------------------------------------------------------------------------------ theme
await scenario('theme', {}, async ({ base, browser, shot }) => {
for (const scheme of ['dark', 'light']) {
const page = await newPage(browser, base, { colorScheme: scheme });
await delay(1500);
const theme = await page.evaluate(() => document.documentElement.dataset.theme);
check(theme === scheme, `a fresh app follows the system (${scheme})`, theme);
await shot(page, `1-${scheme}`);
await page.context().close();
}
});
// the junctions into the staged Control go first (rmdir on a junction removes only the link)
for (const name of ['python', 'strata_control', 'strata_cli', 'serve']) { try { rmdirSync(join(control, name)); } catch { /* already gone */ } }
try { rmSync(work, { recursive: true, force: true, maxRetries: 3 }); } catch (e) { console.log('info scratch folder left behind:', work); }
console.log(`shots in ${shots}`);
process.exit(failed ? 1 : 0);
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env node
/**
* Picture reading in the APP, end to end on a test box, through the installed app layout (packaged host + bundled Control + a real engine component
* that carries strata-vision):
* Download and prepare with "Read pictures" ON (the picture-reader file is fetched, the vision settings are written), load the model with the picture
* reader, send a real picture through the host's own /v1 endpoint and check that the answer names what is in it, unload, shut down.
*
* node vision-e2e.mjs --install <folder with node/ host/ control/> --components <folder holding engine/<package with strata-vision>> \
* --work <scratch> --family qwen --model IQ3_XXS --data <data folder that has (or will get) the model files> --image shapes.png [--device cpu|gpu]
*
* The picture has a red circle, a blue square, a green triangle and the text "STRATA 42". The run exits 1 if any step fails.
*/
import { spawn } from 'node:child_process';
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync } from 'node:fs';
import { createServer } from 'node:net';
import { join, resolve } from 'node:path';
const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => (a.startsWith('--') ? [...acc, [a.slice(2), all[i + 1]?.startsWith('--') || all[i + 1] === undefined ? true : all[i + 1]]] : acc), []));
for (const need of ['install', 'components', 'work', 'model', 'image']) if (!args[need]) { console.error('missing --' + need); process.exit(2); }
const install = resolve(args.install), work = resolve(args.work), components = resolve(args.components), family = String(args.family ?? 'qwen'), modelId = String(args.model);
const device = String(args.device ?? 'cpu');
const node = join(install, 'node', 'node'), hostEntry = join(install, 'host', 'server', 'index.mjs'), controlDir = join(install, 'control');
const dirs = { models: join(work, 'models'), profiles: join(work, 'profiles'), runtime: join(work, 'runtime'), home: join(work, 'home'), xdg: join(work, 'xdg'), appdata: join(work, 'appdata') };
for (const d of [dirs.models, dirs.home, dirs.xdg, dirs.appdata]) mkdirSync(d, { recursive: true });
const delay = ms => new Promise(r => setTimeout(r, ms));
let failed = false;
const record = (name, ok, detail = '') => { if (!ok) failed = true; console.log(`${ok ? 'ok ' : 'FAIL'} ${name}${detail ? ' ' + String(detail).slice(0, 600) : ''}`); };
const freePort = async () => { const s = createServer(); await new Promise(r => s.listen(0, '127.0.0.1', r)); const p = s.address().port; await new Promise(r => s.close(r)); return p; };
const env = { ...process.env, STRATA_COMPONENTS_DIR: components, STRATA_PROFILE_DIR: dirs.profiles, STRATA_RUNTIME_DIR: dirs.runtime, STRATA_INSTALL_DIR: dirs.models,
STRATA_HOME: dirs.home, STRATA_MODELS_DIR: join(work, 'models-unused'), APPDATA: dirs.appdata, XDG_CONFIG_HOME: dirs.xdg, PYTHONDONTWRITEBYTECODE: '1' };
delete env.STRATA_API_KEY;
const port = await freePort();
const base = `http://127.0.0.1:${port}`;
const host = spawn(node, [hostEntry], { cwd: join(install, 'host'), stdio: ['ignore', 'pipe', 'pipe'],
env: { ...env, STRATA_PORT: String(port), STRATA_DIST: join(install, 'host', 'dist'), NODE_ENV: 'production', STRATA_PACKAGED: '1', STRATA_REAL_ENGINE: '1', STRATA_NO_ACQUIRE: '1',
STRATA_SHARE_PORT: String(await freePort()), STRATA_TUNNEL_PORT: String(await freePort()) } });
let hostLog = '';
host.stdout.on('data', c => { hostLog += c; }); host.stderr.on('data', c => { hostLog += c; });
const call = async (path, method = 'GET', body) => {
const response = await fetch(base + path, { method, headers: { 'content-type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body) });
return { status: response.status, body: await response.json().catch(() => null) };
};
async function waitJob(id, seconds) {
let last = '';
for (let i = 0; i < seconds * 2; i++) {
const state = (await call('/api/native')).body;
const job = state.jobs.find(j => j.id === id);
if (job && job.summary !== last) { last = job.summary; console.log(' ... ' + String(last).slice(0, 120)); }
if (job && job.state !== 'running') return { job, state };
await delay(500);
}
throw new Error('operation did not finish');
}
async function main() {
let up = false;
for (let i = 0; i < 200 && !up; i++) { try { up = (await fetch(base + '/api/health')).ok; } catch { /* not yet */ } if (!up) await delay(150); }
record('host healthy', up, up ? base : hostLog.slice(-500));
if (!up) return;
const packages = existsSync(join(components, 'engine')) ? readdirSync(join(components, 'engine')) : [];
const reader = packages.some(p => existsSync(join(components, 'engine', p, 'strata-vision')));
record('the engine component carries the picture reader (strata-vision)', reader, packages.join(', '));
const status = (await call('/api/control/status')).body;
record('Control: engine usable', status.engine?.usable === true, `engine=${status.engine?.engine?.version}`);
// ---- Download and prepare, "Read pictures" on (the app sends vision "yes" for the download)
const values = { family, model: modelId, context: 32768, vision: 'yes', loadVision: device, ...(args.data ? { data_dir: resolve(args.data) } : {}) };
const plan = await call('/api/native/plan', 'POST', values);
record('the plan for the download is made', plan.status === 200, JSON.stringify(plan.body).slice(0, 300));
const started = await call('/api/native/operations', 'POST', { kind: 'install', args: values });
record('the install operation starts', started.status === 200 || started.status === 202, JSON.stringify(started.body).slice(0, 200));
const done = await waitJob(started.body.id, 3600);
record('Download and prepare finishes', done.job.state === 'done', `${done.job.state} ${done.job.error ?? done.job.summary}`);
const native = (await call('/api/native?refresh=1')).body;
console.log(' installed models: ' + JSON.stringify((native.installed?.models ?? []).map(m => ({ id: m.id, model: m.model, ready: m.ready, visionPrepared: m.visionPrepared, images: m.images, config: m.config }))));
const want = modelId.toLowerCase();
const model = native.installed?.models?.find(m => [m.id, m.model, m.tag].some(x => String(x ?? '').toLowerCase() === want));
record('the model is installed with the picture reader prepared (visionPrepared)', !!model?.visionPrepared, JSON.stringify({ ready: model?.ready, visionPrepared: model?.visionPrepared, images: model?.images }));
const configFile = model?.configPath ?? (model?.config ? join(dirs.models, model.config) : null);
let cfg = {};
try { cfg = JSON.parse(readFileSync(configFile, 'utf8')); } catch { /* reported below */ }
const mm = cfg.vision?.mmproj;
record('the run settings carry the vision entry and --vision, and the mmproj file is on disk',
!!cfg.vision && (cfg.args ?? []).includes('--vision') && !!mm && existsSync(mm) && statSync(mm).size > 800e6,
`vision=${cfg.vision ? 'yes' : 'no'} --vision=${(cfg.args ?? []).includes('--vision')} mmproj=${mm ? Math.round(statSync(mm).size / 1048576) + ' MB' : 'none'}`);
// ---- load with the picture reader
const load = await call('/api/native/operations', 'POST', { kind: 'load', args: { model: model?.model ?? modelId.toLowerCase(), vision: device } });
const loaded = await waitJob(load.body.id, 900);
const instance = loaded.state.instances[0];
record('load starts the engine with the picture reader', loaded.job.state === 'done' && instance?.status === 'ready', `${loaded.job.state} ${loaded.job.error ?? ''}; instance=${instance?.status}; vision=${instance?.vision}`);
record('the host says the loaded model reads pictures', !!instance?.vision, `vision=${instance?.vision}`);
// ---- a real picture through the host's own /v1 endpoint
const png = readFileSync(resolve(args.image)).toString('base64');
const t = Date.now();
let answer = '', finish = '';
for (const budget of [1536, 4096]) {
const chat = await call('/v1/chat/completions', 'POST', { model: instance?.apiModelId ?? modelId, max_tokens: budget, stream: false, messages: [{ role: 'user', content: [
{ type: 'text', text: 'Describe this picture: name each shape with its colour and read out any text.' },
{ type: 'image_url', image_url: { url: 'data:image/png;base64,' + png } }] }] });
const choice = chat.body?.choices?.[0] ?? {};
answer = choice.message?.content ?? '';
finish = choice.finish_reason ?? String(chat.status);
if (answer.trim() || finish !== 'length') break;
}
console.log(` answer (${Math.round((Date.now() - t) / 1000)} s, finish=${finish}): ${JSON.stringify(answer.slice(0, 300))}`);
const low = answer.toLowerCase();
record('the answer names the red circle', /red|circle/.test(low));
record('the answer names the blue square', /blue|square/.test(low));
record('the answer names the green triangle', /green|triangle/.test(low));
record('the answer reads STRATA 42', low.includes('42'));
// ---- unload
const unloaded = await call(`/api/native/instances/${instance?.id}/unload`, 'POST', {});
record('unload', unloaded.status === 200, unloaded.status);
}
try { await main(); } catch (e) { record('run', false, e.stack ?? e); }
host.kill();
await delay(1500);
console.log(failed ? 'VISION-E2E FAIL' : 'VISION-E2E PASS');
process.exit(failed ? 1 : 0);
+2
View File
@@ -0,0 +1,2 @@
# Rust build output. Regenerated by `cargo tauri build`; never committed.
/target/
+5458
View File
File diff suppressed because it is too large. Load diff
+30
View File
@@ -0,0 +1,30 @@
[package]
name = "strata-desktop"
version = "0.2.1"
description = "Strata Desktop — local AI, tuned to one model."
edition = "2021"
rust-version = "1.77"
license = "MIT"
# Linux: the drop of a file on the window is read straight from the GTK drag signals (see hook_linux_drops in main.rs); the versions are the ones Tauri itself uses.
[target.'cfg(target_os = "linux")'.dependencies]
gtk = "0.18"
webkit2gtk = "2.0"
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-shell = "2"
tauri-plugin-updater = "2"
tauri-plugin-single-instance = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[profile.release]
codegen-units = 1
lto = true
opt-level = "s"
panic = "abort"
strip = true
+3
View File
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Tauri uses the system WKWebView, so this bundle needs no JIT entitlement of its own;
the web content process gets what it needs from the OS. The App Host runs as an ordinary
child process and needs no special rights. Kept minimal on purpose. -->
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>
@@ -0,0 +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
@@ -0,0 +1 @@
{}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1004 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 771 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 949 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1019 B

Loaded 100 of 1422 files, more files were not shown because too many files have changed in this diff. Show more