diff --git a/docs/decisions.md b/docs/decisions.md index 879c61db..455f6d8c 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -415,6 +415,6 @@ Sources: the five merged memos in `docs/research/`. Milestones are defined in `d | P5, decided 2026-11-04 (lane N-t; the fix P5-S1's row of 2026-10-28 left to task 4; the manager agreed; K reviews) | The ring-3 float target names an os, `kai`, so cpufeatures detects CPU features at run time | `user/x86_64-kai-user.json` named no `os`, so it was `none`, and cpufeatures (0.2 and 0.3) trusts only compile-time features on `target_os = "none"`. The target enables nothing above SSE2, so every RustCrypto crate ran its portable code in ring 3: P5-S1 measured AES-GCM at 118 MB/s through rustls against its 200 MB/s bar. With `"os": "kai"`, cpufeatures asks CPUID, and XGETBV for AVX, at run time as on any OS, and keeps the portable code on a CPU without the feature. Nothing else changes. Across the 143 crates the float programs link, every other `target_os` or `target_family` test is for another architecture (ahash and portable-atomic: arm, riscv and others), another OS (linux, windows, espidf, emscripten and others) or wasm, or is in a host-only proc-macro (getrandom 0.2, under const-random-macro); our own crates test `target_os` only in xtask. On main, the one user of cpufeatures in those programs is sha2 0.11 (through ccserve, fsserve, kaipkg, kaituf, livecore, modelchan, shellcore and p256), whose SHA-256 may now take its SHA-NI or SIMD path, with the same output; the kernel already saves x87, SSE and AVX state per thread (`kernel/src/fpu.rs`). The soft-float target, `x86_64-unknown-none` (the kernel, shell, pkg, updated and others), keeps `none` and the portable backends. The TLS figures with this in ring 3 are P5-S1's second row. [Plan](plans/p5-networking.md) task 4 and section 5. | | P5, measured 2026-11-04 (lane N-t; P5-S1 again, on `kainet` itself in ring 3: spike #654, which never merges; KVM runs [41878520110](https://github.com/dane1599d/kernel-ai/actions/runs/41878520110) and [41885684768](https://github.com/dane1599d/kernel-ai/actions/runs/41885684768), real hosts [41878520332](https://github.com/dane1599d/kernel-ai/actions/runs/41878520332)) | `kainet`'s TLS passes P5-S1's ring-3 bars under KVM once the float target names an os | `kainet` (#656-#662: rustls 0.23.46 with no default features on our own RustCrypto provider) builds with no std and no C for `x86_64-unknown-none` and the hard-float user target; the client adds 574 KiB of `.text` at opt-level `s` (773 KiB at 3), within the 600 KiB bar. A ring-3 `tlsbench` boot case (`test=tls-bench`) runs a client and a server in the guest, built as every boot case is, in the dev profile. **First run** (c480b4fb): the fastest of 10 full handshakes took 4,556 us, within the 300 ms bar, but AES-128-GCM sealed 3 MB/s (AES-256-GCM 2, ChaCha20-Poly1305 7; through rustls 1, 1 and 3 MB/s), for two reasons. Our crates are unoptimised in dev builds and `kainet` instantiates the ciphers' generics, so they ran unoptimised too: #656 builds `kainet` at opt-level 2 in dev builds, as `ccserve` and `wasmhost`. And the float target named no os, so cpufeatures took only compile-time features and the ciphers ran their portable code, as P5-S1's first row (2026-10-28) found: #691 names it `kai`, and waits on the founder. **With both** (f6b88a1b): the handshake 2,892 us; AES-128-GCM seals 462 MB/s, AES-256-GCM 441, ChaCha20-Poly1305 909; through rustls, TLS13_AES_128_GCM_SHA256 229 MB/s, TLS13_AES_256_GCM_SHA384 189 and TLS13_CHACHA20_POLY1305_SHA256 348. **AES-GCM at least 200 MB/s: pass** for AES-128-GCM, the suite all four real hosts choose, bare and through rustls; AES-256-GCM through rustls is 189 MB/s, in a dev build. All three KVM shards passed every case, so every float program boots with the new os. On the runner's host, optimised (`cargo run --release --example tlsbench`): the handshake 1,528 us (fastest of 50), AES-128-GCM 2,570 MB/s, AES-256-GCM 2,429, ChaCha20-Poly1305 1,264; through rustls 814, 793 and 491. **The real hosts**, from a GitHub runner only: github.com, dane1599d.github.io, release-assets.githubusercontent.com and huggingface.co all handshake on TLS 1.3, each choosing TLS13_AES_128_GCM_SHA256 and X25519, in 11-32 ms; github.com's chain is ECDSA (a P-256 leaf, a P-384 root), the other three's RSA with SHA-256. The pinned Qwen3-4B resolve URL redirects once, to `us.aws.cdn.hf.co`, which serves it (task 9 pins the `.cdn.hf.co` suffix, #672). Against `openssl s_server` (#660): all three suites with X25519 and P-256, RSA-2048 and P-384 certificates; TLS 1.2 is refused with `ProtocolVersion` and an RSA-1024 key with `BadSignature`. The fail rule's embedded-tls is not needed. [Plan](plans/p5-networking.md) section 5. | | ADR 0013, measured 2026-11-04 (lane E, spike S13-1's spill miss, draft #682; the manager asked for the row; the founder's question is below) | A spill round trip of about 1,000 positions takes 1.30-1.81 s with today's 64 KiB disk calls and 0.91-1.30 s with 512 KiB calls; the 1.5 s bar then holds, but only while spills and the boot KV files stay on a raw cache region | S13-1's guest run (#690's row) measured the spill round trip at 1.84 s against the 1.5 s bar. Two more runs of the real Qwen3-4B in a KVM guest (runs 41892439564 and 41896735128, head 2dd530b6) show the bar depends on the runner: today's code took 1.35 s at 1,021 positions on a fast-disk runner and 1.90 s on a slow one. Within each run, a spill of 993 positions (139 MiB) measured four ways, save plus restore: 64 KiB a call flushed (today) 1,302 / 1,811 ms; 64 KiB unflushed 1,265 / 1,743 ms; **512 KiB a call flushed 912 / 1,300 ms**; 512 KiB unflushed 872 / 1,193 ms (fast / slow runner; restored equals fresh in all). Where the time goes (143 MiB): about 45% is compute, since FNV-1a 64 runs at 692-844 MB/s (byte-serial by definition, 178-207 ms a pass) and the f16-to-bytes conversion adds 135-280 ms, on save and again on restore; about 30% is the per-call round trip to the polled virtio-blk driver, which does one request at a time and costs 100-130 us a call, so 64 KiB calls spend two thirds of their I/O on it (raw region I/O, 128 MiB: write 317 / 432 ms at 64 KiB, 129 / 199 ms at 512 KiB; read 298 / 396 ms and 133 / 172 ms); the flush on the last write is 3-4%. The fix is the interim syscalls' limit, 64 KiB to 512 KiB (the disk's staging buffer): a constant in `kabi::model`, which the kernel's one check in `kv_io` uses, and nothing else; no format change, no scheduler wiring (its own PR). It also speeds the boot KV restore. A word-wise hash would save another 0.2-0.4 s but is a `kai-kv 2` format change, left to the model lane after this window. A full slot (1,945 positions) stays about 1.8 s fast / 2.8 s slow even with the fix; the bar is for 1,024 positions. **Question for the founder:** all of this is the interim raw `kai-cache` region (MODEL_KV_LOAD and MODEL_KV_SAVE, removed at P2 task 10). ADR 0013 puts spill files in kaifs, `/system/models/cache/agents/`, and files through kaifs measured 25 MB/s write and 26 MB/s read end to end (S04-2, the miss P2 closed with), so 143 MiB would take about 5.7 s each way and the boot KV restore (637-831 ms today) would slow the same way. The 1.5 s bar holds only while spills and the boot KV files stay on a raw cache region, unless kaifs gets about ten times faster. Default if unanswered: keep them on a raw region. | -| P5, built 2026-11-04 (K; the founder's "Keep pushing" on P5-S2, the manager's go; draft, merges with the founder's OK) | Our own virtio-net driver: a 256-deep receive queue refilled in batches with one kick a poll; P5-S2 passes under KVM | **The fault.** virtio-drivers' `VirtIONet` kicks the device on each receive buffer it gives back. With the ring nearly empty, QEMU asks every time, and each kick is a VM exit in which QEMU delivers the next frames itself: 50-62 µs of a frame's 52-98 µs under KVM (P5-S2's profile, run 41889491534). **The driver** (`kernel/src/net/virtio_net.rs`, about 160 lines) sits on virtio-drivers' public `VirtQueue` with our own feature bits (MAC, STATUS, VERSION_1, ring indirect and event index), no fork. Receive buffers read in a poll go into a stash, and after the poll they all go back with **one kick**; frames sent in a poll are queued and kicked once with them. The queue is 256 deep, QEMU's own maximum. **Each batch's kick is unconditional:** `VirtQueue::should_notify` compares the 16-bit ring indexes without their wrap (`avail_idx >= avail_event + 1`). Once a batch crossed 65,536 adds it said no, and receiving stopped (run 41896431110). A kick a poll is the cost the batch was for. One-at-a-time adds, as virtio-drivers' own driver does, never meet that case, so release/v0.1 needs no fix. **Measured** (run 41899342750, KVM on a TAP device, the TSC, the same runner for each pair, two CPU types): at a 20 ms round trip with 1 MiB, polled busily, batched gives 44.5 and 44.7 MB/s where a kick per buffer gives 20.9 and 15.7. With 4 MiB it gives 70.6 and 102.0 (23.8 and 18.8). With 4 MiB at the shipped 2 ms poll gap it gives 64.4 and 31.3. At 0 ms it gives 208-310 MB/s, against 21-40. **The bar (≥ 30 MB/s at 20 ms) is met on both CPU types** (AMD 2596 MHz with AVX-512, AMD 2444 MHz without). The 20 ms figures remain lower bounds (the spike's delay queue). Per frame, the refill now costs 0.2-4 µs where it cost 34-60. **What's left:** at a 2 ms gap the wait between polls dominates (up to 94% of a frame's time), which lane D's interrupts remove. [P5 plan](plans/p5-networking.md) section 5. | +| P5, built 2026-11-04 (K; the founder's "Keep pushing" on P5-S2, the manager's go; draft, merges with the founder's OK) | Our own virtio-net driver: a 256-deep receive queue refilled in batches with one kick a poll; P5-S2 passes under KVM | **The fault.** virtio-drivers' `VirtIONet` kicks the device on each receive buffer it gives back. With the ring nearly empty, QEMU asks every time, and each kick is a VM exit in which QEMU delivers the next frames itself: 50-62 µs of a frame's 52-98 µs under KVM (P5-S2's profile, run 41889491534). **The driver** (`kernel/src/net/virtio_net.rs`, about 160 lines) sits on virtio-drivers' public `VirtQueue` with our own feature bits (MAC, STATUS, VERSION_1, ring indirect and event index), no fork. Receive buffers read in a poll go into a stash, and after the poll they all go back with **one kick**; frames sent in a poll are queued and kicked once with them. The queue is 256 deep, QEMU's own maximum. **Each batch's kick is unconditional:** `VirtQueue::should_notify` compares the 16-bit ring indexes without their wrap (`avail_idx >= avail_event + 1`). Once a batch crossed 65,536 adds it said no, and receiving stopped (run 41896431110). A kick a poll is the cost the batch was for. One-at-a-time adds, as virtio-drivers' own driver does, never meet that case, so release/v0.1 needs no fix. **Measured** (run 41899342750, KVM on a TAP device, the TSC, the same runner for each pair, two CPU types): at a 20 ms round trip with 1 MiB, polled busily, batched gives 44.5 and 44.7 MB/s where a kick per buffer gives 20.9 and 15.7. With 4 MiB it gives 70.6 and 102.0 (23.8 and 18.8). With 4 MiB at the shipped 2 ms poll gap it gives 64.4 and 31.3. At 0 ms it gives 208-310 MB/s, against 21-40. **The bar (≥ 30 MB/s at 20 ms) is met on both CPU types** (AMD 2596 MHz with AVX-512, AMD 2444 MHz without). The 20 ms figures remain lower bounds (the spike's delay queue). Per frame, the refill now costs 0.2-4 µs where it cost 34-60. **On main** (run 41962939272, 2026-11-05, after #704 and #683, main's driver and clock, KVM on a TAP device, three runners): at 20 ms with 1 MiB, polled busily, 46.1, 46.2 and 47.3 MB/s; at the shipped 2 ms poll gap, 33.8, 34.1 and 34.3. With 4 MiB, 87.9-128.3 busily and 65.2-79.8 at 2 ms. At 0 ms, 235-375 MB/s busily and 97-126 at 2 ms. **The bar holds on main on all three runners, at the shipped poll gap too.** **What's left:** at a 2 ms gap the wait between polls dominates (up to 94% of a frame's time), which lane D's interrupts remove. [P5 plan](plans/p5-networking.md) section 5. | | P5, built 2026-11-04 (lane N-t, task 8's host half; C reviewed #675) | `pkg`'s fixed destination for the online repository, until its `NetDest` | `kaipkg::online::Hosts::github()` pins the three hosts `pkg` may reach for the online repository, each on port 443 only: `dane1599d.github.io` for TUF metadata (`/kernel-ai-updates/metadata/`), `github.com` for release downloads (`/dane1599d/kernel-ai-updates/releases/download/pkg--/-.kpkg`), and `release-assets.githubusercontent.com`, where GitHub's release download redirects (a HEAD for a public release asset from the dev PC got a 302 there on 2026-10-21, ADR 0011 §8). Every request and every redirect is checked against them before it is made, so a redirect anywhere else is refused, never followed. The set is fixed in the image and changes only with a signed OS update. Task 1 replaces it with a `NetDest` for the same three hosts, which the kernel then enforces. [Plan](plans/p5-networking.md) task 8; ADR 0011 §4. | | P4, measured 2026-11-05 (model lane; the manager's order: report only, the gate stays) | actbench gains a report-only `shipped` arm, and the mock pack overstates the product | The gated `act` arm answers its 190 tasks on actbench's own 40-row mock pack (`xtask/src/actbench/render.rs`: its own examples and rules); only its M8 50 go through the pack that ships. So a change to `llmproto::pack` cannot move the exact-call or ambiguous bars (dane/act-margin's shipped-pack examples left them unchanged and M8 went 47 to 46; dropped). Arm `shipped` answers the same tasks through llm's own `act` path on the shipped pack (`llmproto::local::act::routed`), skips and counts tasks for rows the OS lacks (such as `component_apply`), scores a `script` call on its routing, and gates nothing; the workflow runs one `shipped` job beside the `act` seeds. Run 41909205696 (seed 42): **111/142** on the shipped pack, 48 skipped: single 28/32 (mock 57/60), multi 13/16, ambiguous **10/20** (mock 16/20), irrelevant 20/20, bait 9/17 (8 answered `clarify` where `say` is expected: none ran anything), negation 8/9, memory 13/15, state 4/6, hostile 6/7. The product's misses the mock hides: vague requests taken as `undo` or a read (`go back`, `undo some of it`, `switch models` to `model_list`), a placeholder argument (`pkg_remove("package_name")`), scripts that print a refusal or a claim (`print("Change applied.")`), and questions about the system answered with `clarify`. Whether the gate should move to the shipped arm, re-baselined, is the founder's call. | diff --git a/llmproto/src/app.rs b/llmproto/src/app.rs index dba11403..e852b356 100644 --- a/llmproto/src/app.rs +++ b/llmproto/src/app.rs @@ -31,7 +31,13 @@ pub fn user_turn(request: &str) -> String { pub fn request(request: &str, repair: Option>) -> chat::Request { let tag = "\n/no_think"; let user = format!("{}{tag}", user_turn(request)); - let mut asked = chat::Request::new(Some(&system_prompt()), &user, script::APP_MAX_TOKENS); + // A repair's card leaves out the example, so the refused source and a + // whole corrected main.c fit in the context with it. + let system = match repair { + Some(_) => capp1::render_repair(), + None => system_prompt(), + }; + let mut asked = chat::Request::new(Some(&system), &user, script::APP_MAX_TOKENS); if let Some(repair) = repair { let previous = serde_json::json!({ "script": repair.script, "summary": "" }).to_string(); let feedback = format!("{}\nWrite the corrected main.c.{tag}", repair.error); @@ -75,6 +81,24 @@ mod tests { ))); } + /// A repair keeps the card's rules, calls and template but not its + /// example, which frees the room a whole corrected main.c needs. + #[test] + fn a_repair_card_leaves_out_the_example() { + let first = request("guess\na guessing game", None); + let repair = Repair { + script: "int main;", + error: "cc build refused it", + }; + let again = request("guess\na guessing game", Some(repair)); + let (card, short) = (&first.messages[0].content, &again.messages[0].content); + assert!(card.contains("Example:") && !short.contains("Example:")); + assert!(card.starts_with(short.as_str())); + assert!(short.contains("Template:")); + assert!(capp1::estimate_tokens(card) - capp1::estimate_tokens(short) > 250); + assert_eq!(again.messages.len(), 4); + } + #[test] fn the_name_and_the_words_are_their_own_lines() { assert_eq!( @@ -94,7 +118,7 @@ mod tests { serde_json::from_slice(&chat_request("m", "timer\na timer", Some(repair))).unwrap(); let messages = body["messages"].as_array().unwrap(); assert_eq!(messages.len(), 4); - assert_eq!(messages[0]["content"], system_prompt()); + assert_eq!(messages[0]["content"], capp1::render_repair()); assert!( messages[1]["content"] .as_str() diff --git a/llmproto/src/pack/capp1.rs b/llmproto/src/pack/capp1.rs index 3a99dc0b..0da4babb 100644 --- a/llmproto/src/pack/capp1.rs +++ b/llmproto/src/pack/capp1.rs @@ -130,6 +130,20 @@ pub fn estimate_tokens(card: &str) -> usize { /// The card. pub fn render() -> String { + let mut card = render_repair(); + card.push_str("Example:\n"); + for (request, program, summary) in EXAMPLES { + let envelope = serde_json::json!({ "script": program, "summary": summary }); + let _ = writeln!(card, "Request: {request}\n{envelope}"); + } + card +} + +/// The card without its worked example, for a repair round: the model's own +/// last answer shows the shape, and the example's tokens (about a third of +/// the card) are the room a whole repaired main.c needs at the OS's 2,048 +/// positions (spike S-W1's probe: guess's repair filled the context). +pub fn render_repair() -> String { let mut card = String::from(RULES); for (call, doc) in CALLS { let _ = match *doc { @@ -138,10 +152,5 @@ pub fn render() -> String { }; } let _ = write!(card, "Template:\n{TEMPLATE}"); - card.push_str("Example:\n"); - for (request, program, summary) in EXAMPLES { - let envelope = serde_json::json!({ "script": program, "summary": summary }); - let _ = writeln!(card, "Request: {request}\n{envelope}"); - } card }