Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<name>-<version>/<name>-<version>.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. |
28 changes: 26 additions & 2 deletions llmproto/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,13 @@ pub fn user_turn(request: &str) -> String {
pub fn request(request: &str, repair: Option<Repair<'_>>) -> 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);
Expand Down Expand Up @@ -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!(
Expand All @@ -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()
Expand Down
19 changes: 14 additions & 5 deletions llmproto/src/pack/capp1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
Loading