From 9c4bd4ae92c1327bbdf6601c900d42b99cbaf6a8 Mon Sep 17 00:00:00 2001 From: Joel Teply Date: Mon, 27 Jul 2026 12:37:00 -0500 Subject: [PATCH 1/4] fix(chat): ChatModule executor fails loud per-request, not process panic (#201) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ChatModule::executor()` did `.cloned().expect(...)` — a hard panic if a `chat/poll`, `chat/send`, or `persist_posted` landed before `start_server` called `install_executor_on_all` (a boot race). Panicking there SIGABRTs the whole core and takes every other module down with it, for a per-request contract violation that only concerns that one request. Convert `executor()` to `Result, String>` returning the SAME loud, contract-naming message, and `?`-propagate it in the 3 callers (all already `Result<_, String>`). Faithful to [[no-fallbacks-ever]] — still loud, still names `install_executor_on_all`, no silent default — while satisfying #26 (faculties degrade, never panic): a command that races boot fails loudly to its caller instead of crashing the process. Regression test: a pre-install `poll()` returns the loud error naming the contract instead of panicking. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo --- core/continuum-core/src/modules/chat/mod.rs | 49 +++++++++++++++------ 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/core/continuum-core/src/modules/chat/mod.rs b/core/continuum-core/src/modules/chat/mod.rs index 2bf88aabf..b56e2919d 100644 --- a/core/continuum-core/src/modules/chat/mod.rs +++ b/core/continuum-core/src/modules/chat/mod.rs @@ -121,18 +121,23 @@ impl ChatModule { Self { executor_slot } } - /// Resolve the executor for the current call. Panics if the - /// executor was never installed — that's a boot ordering bug - /// (`start_server` must call `install_executor_on_all` BEFORE - /// any chat command can dispatch). Per [[no-fallbacks-ever]]: - /// the panic message names the contract so the operator sees - /// the actual problem. - fn executor(&self) -> Arc { - self.executor_slot.cloned().expect( + /// Resolve the executor for the current call. Returns a loud, + /// contract-naming error if the executor was never installed — a + /// boot-ordering bug (`start_server` must call + /// `install_executor_on_all` BEFORE any chat command dispatches). + /// Per [[no-fallbacks-ever]] the message still names the contract + /// so the operator sees the real problem — but the blast radius is + /// THIS request, not the whole core: a command that races boot + /// fails loudly to its caller instead of `.expect()`-panicking the + /// process and taking every other module down with it + /// (#201 boot-race / #26 faculties degrade, never panic). + fn executor(&self) -> Result, String> { + self.executor_slot.cloned().ok_or_else(|| { "ChatModule: CommandExecutor not installed — \ start_server must call install_executor_on_all \ - before any chat command can dispatch (task #224)", - ) + before any chat command can dispatch (task #224)" + .to_string() + }) } /// Resolve a pagination anchor to its stored `timestamp` (the field @@ -177,7 +182,7 @@ impl ChatModule { /// 5. Normalize back to chronological order for display regardless /// of query direction. pub async fn poll(&self, params: ChatPollParams) -> Result { - let executor = self.executor(); + let executor = self.executor()?; let limit = params.limit.unwrap_or(DEFAULT_POLL_LIMIT); // The two anchors are opposite scroll directions — both at once @@ -298,7 +303,7 @@ impl ChatModule { /// could be the dedup id) but the design conversation is its /// own scope. pub async fn send(&self, params: ChatSendParams) -> Result { - let executor = self.executor(); + let executor = self.executor()?; let message_id = Uuid::new_v4(); let now_ms = now_ms(); let now_iso = now_iso(now_ms); @@ -430,7 +435,7 @@ impl ChatModule { /// EXPECTED duplicate, never an error. A persona line's id is airc's /// `event_id` (stable across replay), so restarts can't double-store either. pub async fn persist_posted(&self, payload: Value) -> Result<(), String> { - let executor = self.executor(); + let executor = self.executor()?; let field = |k: &str| -> Result { payload .get(k) @@ -1101,6 +1106,24 @@ mod tests { assert!(result.after_message_id.is_none()); } + // what this catches: #201 boot-race — a chat command that arrives BEFORE + // install_executor_on_all runs fails LOUD to its caller (naming the contract), + // instead of .expect()-panicking the whole core and taking every other module + // down with it. Regression for the .expect() → Result conversion in executor(). + #[tokio::test] + async fn command_before_executor_installed_fails_loud_not_panics() { + let chat = ChatModule::new(); // executor slot deliberately NOT installed + let err = chat + .poll(ChatPollParams::default()) + .await + .expect_err("poll before install_executor_on_all must fail, not panic"); + assert!(err.contains("not installed"), "loud contract error, got: {err}"); + assert!( + err.contains("install_executor_on_all"), + "error names the contract so the operator sees the real problem: {err}" + ); + } + // ── chat/poll: latest-N path (no anchor) ────────────────────────── #[tokio::test] From 0402e5ebc185cec9af4da72315d1ccc722c9f9e5 Mon Sep 17 00:00:00 2001 From: Joel Teply Date: Mon, 27 Jul 2026 09:49:44 -0500 Subject: [PATCH 2/4] =?UTF-8?q?feat(capacity):=20expert=5Fobserve=20harnes?= =?UTF-8?q?s=20=E2=80=94=20glass-box=20LIVE=20MoE=20expert=20routing=20(#2?= =?UTF-8?q?30/#229)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Runs a GGUF MoE through the core/llama FFI with LiveExpertObserver attached (the existing cb_eval → ffn_moe_topk seam), generates N tokens to drive real routing, then dumps the model-intrinsic affinity: hot/cold expert distribution + co-occurrence + prefetch candidates. That affinity is the INPUT to expert prefetch (#227), grid placement (#180), compaction, and distillation (#233). Uses the in-process FFI (not the live llama-server lane) because affinity is model-intrinsic — valid data, zero risk to live serving. First run (Qwen3-Coder-30B-A3B, 96 tokens, Metal): 43,640 activations, 6116/6144 expert-slots fired (~99.5%), hottest expert only 0.20% (~12x uniform), 6092 colder share 95.8%. FINDING: for an 8/128 (6.25%-active) MoE, activation over a generation is BROAD, not tiny-hot — so the paging win is the tier ladder + affinity placement, not a small resident set. Prefetch predictor returned 0 candidates over 96 tokens (needs more data). K3 (1.8% active) should be far more concentrated — same harness will quantify it when weights land. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo --- core/continuum-core/src/bin/expert_observe.rs | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 core/continuum-core/src/bin/expert_observe.rs diff --git a/core/continuum-core/src/bin/expert_observe.rs b/core/continuum-core/src/bin/expert_observe.rs new file mode 100644 index 000000000..99ceec65b --- /dev/null +++ b/core/continuum-core/src/bin/expert_observe.rs @@ -0,0 +1,114 @@ +//! expert_observe — glass-box the LIVE MoE expert routing (#230 / #229). +//! +//! Runs a GGUF MoE through the core/llama FFI with a [`LiveExpertObserver`] attached (the +//! already-built `cb_eval` → `ffn_moe_topk` seam in `core/llama/src/safe.rs`), generates +//! N tokens to drive REAL routing, then dumps the affinity data — hot/cold expert +//! distribution + co-occurrence + prefetch prediction. That affinity is the INPUT to +//! expert prefetch (#227), grid placement (#180), compaction, and distillation (#233). +//! +//! WHY this and not the live llama-server path: expert affinity is MODEL-INTRINSIC (which +//! experts co-fire for given inputs), so an in-process FFI run gathers valid data without +//! touching the live-persona serving lane. (The live llama-server path would need a +//! separate fork patch to emit routing; this harness needs neither.) +//! +//! Usage: +//! cargo run -p continuum-core --features metal,accelerate --bin expert_observe -- [n_tokens] [prompt] + +use std::path::PathBuf; +use std::sync::Arc; + +use continuum_core::capacity::expert_observer::LiveExpertObserver; +use llama::{Batch, ContextParams, ExpertObserver, Model, ModelParams, Sampler}; + +fn main() { + let args: Vec = std::env::args().collect(); + let model_path = args + .get(1) + .expect("usage: expert_observe [n_tokens] [prompt]"); + let n_tokens: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(96); + let prompt = args.get(3).map(|s| s.as_str()).unwrap_or( + "Write a Rust function to reverse a string, then explain how it works step by step:\n", + ); + + // The observer is the sink: `cb_eval` calls `observe(layer, selected_experts, n_used)` + // per MoE layer per token from inside the compute thread. + let observer = LiveExpertObserver::new(); + + let model = Model::load( + PathBuf::from(model_path), + ModelParams { + n_gpu_layers: -1, + use_mmap: true, + }, + ) + .expect("load model"); + println!("Loaded {model_path} (vocab={})", model.n_vocab()); + + let mut ctx = model + .new_context(ContextParams { + n_ctx: 4096, + n_batch: 512, + n_seq_max: 1, + expert_observer: Some(observer.clone() as Arc), + ..Default::default() + }) + .expect("context"); + + // Prefill. + let prompt_tokens = model.tokenize(prompt, true, false).expect("tokenize"); + let mut batch = Batch::allocated(512, 1); + let last = (prompt_tokens.len() - 1) as i32; + for (i, tok) in prompt_tokens.iter().enumerate() { + batch.push(*tok, i as i32, &[0], i as i32 == last); + } + ctx.decode(&batch).expect("prefill decode"); + + // Generate — every decoded token drives the router → observer tallies real selections. + let mut sampler = Sampler::greedy(); + let mut n_cur = batch.n_tokens(); + let mut n_decoded = 0usize; + for _ in 0..n_tokens { + let token = sampler.sample(&ctx, -1); + if model.is_eog_token(token) { + break; + } + batch.clear(); + batch.push(token, n_cur, &[0], true); + ctx.decode(&batch).expect("gen decode"); + n_cur += 1; + n_decoded += 1; + } + + // Dump the affinity — the fuel for the whole optimization catalog. + let total = observer.total_hits(); + let hits = observer.snapshot_hits(); + let (_seen, cooccur) = observer.snapshot_cooccurrence(); + let predicted = observer.predicted(); + + println!("\n=== EXPERT AFFINITY (model-intrinsic, {n_decoded} tokens observed) ==="); + println!("total expert activations : {total}"); + println!("distinct experts fired : {}", hits.len()); + println!("co-occurring pairs seen : {}", cooccur.len()); + println!("prefetch candidates : {} experts", predicted.len()); + + let mut ranked: Vec<(String, u64)> = hits + .iter() + .map(|(k, v)| (format!("{k:?}"), *v)) + .collect(); + ranked.sort_by(|a, b| b.1.cmp(&a.1)); + let show = ranked.len().min(24); + println!("\ntop {show} hottest experts (of {} fired):", hits.len()); + for (id, h) in ranked.iter().take(show) { + let pct = if total > 0 { 100.0 * *h as f64 / total as f64 } else { 0.0 }; + println!(" {id:<28} {h:>8} ({pct:.2}%)"); + } + // The tail: how concentrated is activation? (the paging headroom) + if ranked.len() > show { + let tail: u64 = ranked.iter().skip(show).map(|(_, h)| *h).sum(); + let tail_pct = if total > 0 { 100.0 * tail as f64 / total as f64 } else { 0.0 }; + println!( + " ... {} colder experts share the remaining {tail_pct:.2}% (the page-out tail)", + ranked.len() - show + ); + } +} From d711ce50e980ee4b45fd870f088623658b195359 Mon Sep 17 00:00:00 2001 From: Joel Teply Date: Mon, 27 Jul 2026 14:08:07 -0500 Subject: [PATCH 3/4] =?UTF-8?q?feat(capacity):=20expert=5Fobserve=20?= =?UTF-8?q?=E2=80=94=20per-domain=20concentration=20+=20working-set-size?= =?UTF-8?q?=20+=20Jaccard=20(#230)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Evolves the MoE glass-box harness from the pooled first cut to the measurement that actually decides the paging architecture (BigMama's three methodological guardrails): diverse multi-domain corpus, PER-DOMAIN concentration (top-K% activation share vs the uniform null), cross-domain hot-set Jaccard, shared-base-vs-own-only activation MASS split, and the working-set-size curve (experts resident for 50/80/90/95% of a domain's mass). Prefill-dominant sampling on realistic input, not a degenerating greedy loop. Ran live on Qwen3-Coder-30B-A3B (Metal): pooled top-10% = 18.6% (mild — the smear), but per-domain = 25–38% with near-disjoint hot sets (code↔prose Jaccard 0.05) and a tiny 38-expert universal core — i.e. paging is domain-working-set SWAPPING, not frequency tiering. This is the #180 evidence; the harness is the reusable probe for any MoE (incl. K3 at weight-drop). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo --- core/continuum-core/src/bin/expert_observe.rs | 370 ++++++++++++++---- 1 file changed, 299 insertions(+), 71 deletions(-) diff --git a/core/continuum-core/src/bin/expert_observe.rs b/core/continuum-core/src/bin/expert_observe.rs index 99ceec65b..558e73122 100644 --- a/core/continuum-core/src/bin/expert_observe.rs +++ b/core/continuum-core/src/bin/expert_observe.rs @@ -1,49 +1,125 @@ //! expert_observe — glass-box the LIVE MoE expert routing (#230 / #229). //! -//! Runs a GGUF MoE through the core/llama FFI with a [`LiveExpertObserver`] attached (the -//! already-built `cb_eval` → `ffn_moe_topk` seam in `core/llama/src/safe.rs`), generates -//! N tokens to drive REAL routing, then dumps the affinity data — hot/cold expert -//! distribution + co-occurrence + prefetch prediction. That affinity is the INPUT to -//! expert prefetch (#227), grid placement (#180), compaction, and distillation (#233). +//! Runs a GGUF MoE through the core/llama FFI with a [`LiveExpertObserver`] attached, drives +//! REAL routing over a labelled multi-domain corpus, then reports the affinity that decides +//! the paging architecture (#180): PER-DOMAIN concentration + CROSS-DOMAIN hot-set overlap. //! -//! WHY this and not the live llama-server path: expert affinity is MODEL-INTRINSIC (which -//! experts co-fire for given inputs), so an in-process FFI run gathers valid data without -//! touching the live-persona serving lane. (The live llama-server path would need a -//! separate fork patch to emit routing; this harness needs neither.) +//! ## Methodology (BigMama's three guardrails, 2026-07-27) +//! 1. **Sample size.** ~7 activations/slot is far too few to tell power-law from uniform — +//! Poisson noise swamps the skew. We drive thousands of tokens so the head separates. +//! 2. **Clean samples, not a greedy loop.** A long greedy generation degenerates and +//! OVER-routes to the same experts — a false positive for locality. We PREFILL diverse +//! prompts (prefill routes every token on realistic input) with only short generation. +//! 3. **Diverse HOW — the knife-edge.** Pooling ACROSS domains measures the global +//! mixed-workload histogram; MoE specialization routes domains to different experts, so +//! pooling SMEARS toward uniform and answers the WRONG question. The paging architecture +//! serves ONE persona doing ONE coherent thing, so we measure: +//! (a) PER-DOMAIN concentration — one observer per domain = the coherent-session +//! paging headroom (top-K% activation share WITHIN a domain). +//! (b) CROSS-DOMAIN overlap — intersect each domain's hot set. Experts hot in EVERY +//! domain = the always-resident shared base (never paged); experts hot in ONE +//! domain = the pageable specialization tier. The money number: what fraction of a +//! domain's activation MASS lands on experts cold for the other domains — the +//! eviction win. //! //! Usage: -//! cargo run -p continuum-core --features metal,accelerate --bin expert_observe -- [n_tokens] [prompt] +//! cargo run --release -p continuum-core --features metal,accelerate --bin expert_observe -- [n_gen_per_prompt] +use std::collections::{HashMap, HashSet}; use std::path::PathBuf; use std::sync::Arc; use continuum_core::capacity::expert_observer::LiveExpertObserver; use llama::{Batch, ContextParams, ExpertObserver, Model, ModelParams, Sampler}; -fn main() { - let args: Vec = std::env::args().collect(); - let model_path = args - .get(1) - .expect("usage: expert_observe [n_tokens] [prompt]"); - let n_tokens: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(96); - let prompt = args.get(3).map(|s| s.as_str()).unwrap_or( - "Write a Rust function to reverse a string, then explain how it works step by step:\n", - ); - - // The observer is the sink: `cb_eval` calls `observe(layer, selected_experts, n_used)` - // per MoE layer per token from inside the compute thread. - let observer = LiveExpertObserver::new(); - - let model = Model::load( - PathBuf::from(model_path), - ModelParams { - n_gpu_layers: -1, - use_mmap: true, - }, - ) - .expect("load model"); - println!("Loaded {model_path} (vocab={})", model.n_vocab()); +/// Labelled corpus: each domain gets its OWN observer, fed several diverse prompts so the +/// per-domain sample is dense. The point is a COHERENT workload per observer (what a persona +/// actually does in a session), not a global mixture. +const DOMAINS: &[(&str, &[&str])] = &[ + ( + "code", + &[ + "Implement a lock-free single-producer single-consumer ring buffer in Rust using \ + atomics with Release/Acquire ordering; explain why a SeqCst fence is unnecessary \ + and how the head/tail indices wrap a power-of-two capacity without a modulo in the \ + hot path.\n\nuse std::sync::atomic::{AtomicUsize, Ordering};\n", + "Write a React hook useDebouncedResource taking an async fetcher and debounce \ + interval that cancels in-flight requests on key change, dedupes concurrent callers, \ + and surfaces loading/error/stale-while-revalidate state without tearing under \ + concurrent mode.\n\nimport { useEffect, useRef, useState } from 'react';\n", + "Given orders(id, customer_id, placed_at, total) and refunds(order_id, amount, \ + refunded_at), write SQL returning each customer's net revenue by month, excluding \ + months with over 40% refunded, ranking customers within each month by a window \ + function.", + ], + ), + ( + "prose", + &[ + "The lighthouse keeper had not spoken to another person in forty-one days when the \ + boat appeared on the horizon. He noticed first the wrongness in the grey, and only \ + afterward resolved the shape into a hull riding low, and set down the brass polish \ + and went to the door, the cold coming up through the stone under his socks.", + "She had rehearsed the apology on the train, each version softer than the last, until \ + the words lost their edges and became a kind of weather she carried into the room. \ + Her mother was at the sink with her back turned, and for a moment neither of them \ + moved, and the tap ran over a single white plate.", + "Write the opening of a short story about a cartographer who discovers that a river \ + on his oldest map no longer exists, and who sets out on foot to find where it went, \ + narrated in close third person with attention to the texture of the walking.", + ], + ), + ( + "math", + &[ + "Prove that every finite integral domain is a field. Fix a nonzero a and consider \ + x -> a x; show injectivity, conclude surjectivity from finiteness, hence an inverse \ + of a exists. Then exhibit an infinite integral domain that is not a field to show \ + finiteness is essential.", + "Derive the closed form for the variance of a sum of two correlated random variables \ + in terms of their individual variances and covariance, then generalize to n \ + variables and interpret the cross terms as the reason diversification reduces \ + portfolio variance.", + "State and prove the pigeonhole principle, then use it to show that among any 51 \ + integers chosen from 1 to 100 there must be two that are coprime, and separately two \ + whose difference is exactly 10.", + ], + ), + ( + "science", + &[ + "Explain how the sodium-potassium pump maintains a neuron's resting potential: three \ + sodium out for two potassium in, the ATP-driven conformational change, and how the \ + electrogenic imbalance plus leak channels and the Nernst equilibria set the roughly \ + -70 mV resting potential.", + "Describe why the sky is blue in terms of Rayleigh scattering: the inverse fourth \ + power wavelength dependence, why shorter wavelengths scatter more strongly, and why \ + sunsets are red because of the longer atmospheric path length near the horizon.", + "Explain the greenhouse effect at the level of molecular physics: which atmospheric \ + gases absorb in the infrared, why their vibrational modes couple to outgoing \ + longwave radiation while nitrogen and oxygen do not, and how re-emission warms the \ + surface.", + ], + ), + ( + "planning", + &[ + "Sketch the migration plan to decompose a Rust monolith into services over a \ + command-and-event bus: identify the seams where synchronous calls become async, how \ + you preserve transactional guarantees that relied on one process, and how you roll \ + out incrementally behind a facade without a big-bang cutover.", + "Write a JSON schema for a distributed-cache eviction policy supporting LRU, LFU, and \ + TTL tiers with per-key overrides, budgets in both bytes and percentage-of-pool, and \ + validation that every declared cache class has at least one eviction dimension so \ + none grows unbounded.", + "Draft a rollout plan for a feature flag that changes a checkout flow: staged \ + percentage ramp, the metrics that gate each stage, the automatic rollback trigger, \ + and how you keep the two code paths from diverging while the flag is live.", + ], + ), +]; +fn drive_prompt(model: &Model, observer: &Arc, prompt: &str, n_gen: usize) { let mut ctx = model .new_context(ContextParams { n_ctx: 4096, @@ -54,61 +130,213 @@ fn main() { }) .expect("context"); - // Prefill. - let prompt_tokens = model.tokenize(prompt, true, false).expect("tokenize"); - let mut batch = Batch::allocated(512, 1); - let last = (prompt_tokens.len() - 1) as i32; - for (i, tok) in prompt_tokens.iter().enumerate() { - batch.push(*tok, i as i32, &[0], i as i32 == last); + let tokens = model.tokenize(prompt, true, false).expect("tokenize"); + let mut n_cur: i32 = 0; + for chunk in tokens.chunks(512) { + let mut batch = Batch::allocated(512, 1); + let last_global = n_cur as usize + chunk.len() - 1; + for (i, tok) in chunk.iter().enumerate() { + let pos = n_cur + i as i32; + batch.push(*tok, pos, &[0], (n_cur as usize + i) == last_global); + } + ctx.decode(&batch).expect("prefill decode"); + n_cur += chunk.len() as i32; } - ctx.decode(&batch).expect("prefill decode"); - // Generate — every decoded token drives the router → observer tallies real selections. let mut sampler = Sampler::greedy(); - let mut n_cur = batch.n_tokens(); - let mut n_decoded = 0usize; - for _ in 0..n_tokens { + for _ in 0..n_gen { let token = sampler.sample(&ctx, -1); if model.is_eog_token(token) { break; } - batch.clear(); + let mut batch = Batch::allocated(1, 1); batch.push(token, n_cur, &[0], true); ctx.decode(&batch).expect("gen decode"); n_cur += 1; - n_decoded += 1; } +} + +/// Sorted-desc counts → activation share captured by the top `frac` of FIRED experts. +fn top_share(counts_desc: &[u64], total: u64, frac: f64) -> f64 { + if counts_desc.is_empty() || total == 0 { + return 0.0; + } + let k = ((counts_desc.len() as f64 * frac).ceil() as usize).max(1).min(counts_desc.len()); + let s: u64 = counts_desc.iter().take(k).sum(); + 100.0 * s as f64 / total as f64 +} + +fn main() { + let args: Vec = std::env::args().collect(); + let model_path = args + .get(1) + .expect("usage: expert_observe [n_gen_per_prompt]"); + let n_gen: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(48); + + let model = Model::load( + PathBuf::from(model_path), + ModelParams { n_gpu_layers: -1, use_mmap: true }, + ) + .expect("load model"); + println!("Loaded {model_path} (vocab={})", model.n_vocab()); + + // Drive each domain into its OWN observer → per-domain hit maps. + let mut per_domain = Vec::new(); + for (label, prompts) in DOMAINS { + let obs = LiveExpertObserver::new(); + for prompt in *prompts { + drive_prompt(&model, &obs, prompt, n_gen); + } + let hits = obs.snapshot_hits(); + println!( + " domain {label:<9} : {} activations across {} experts", + obs.total_hits(), + hits.len() + ); + per_domain.push((label.to_string(), hits)); + } + + // ---- PER-DOMAIN concentration (the coherent-session paging headroom) ---- + println!("\n=== PER-DOMAIN CONCENTRATION (top-K% share vs uniform null) ==="); + println!("{:<10} {:>8} {:>8} {:>8} {:>7} {:>7} {:>7}", "domain", "total", "fired", "mean", "top1%", "top10%", "top25%"); + for (label, hits) in &per_domain { + let total: u64 = hits.values().sum(); + let mut counts: Vec = hits.values().copied().collect(); + counts.sort_unstable_by(|a, b| b.cmp(a)); + let mean = if counts.is_empty() { 0.0 } else { total as f64 / counts.len() as f64 }; + println!( + "{label:<10} {total:>8} {:>8} {mean:>8.1} {:>6.1}% {:>6.1}% {:>6.1}%", + counts.len(), + top_share(&counts, total, 0.01), + top_share(&counts, total, 0.10), + top_share(&counts, total, 0.25), + ); + } + println!("(uniform null → top1%≈1, top10%≈10, top25%≈25; materially above = per-session paging headroom)"); - // Dump the affinity — the fuel for the whole optimization catalog. - let total = observer.total_hits(); - let hits = observer.snapshot_hits(); - let (_seen, cooccur) = observer.snapshot_cooccurrence(); - let predicted = observer.predicted(); + // ---- WORKING-SET SIZE: how many experts resident to capture X% of a domain's mass ---- + // THE engineering number for #180: the resident working set you must keep hot; the rest + // pages to CPU/disk (misses on the cold tail are low-mass = infrequent). + let experts_for_mass = |counts_desc: &[u64], total: u64, frac: f64| -> usize { + if total == 0 { return 0; } + let target = frac * total as f64; + let mut acc = 0u64; + for (i, c) in counts_desc.iter().enumerate() { + acc += c; + if acc as f64 >= target { return i + 1; } + } + counts_desc.len() + }; + println!("\n=== WORKING-SET SIZE (experts resident for X% of a domain's mass; % of fired) ==="); + println!("{:<10} {:>10} {:>12} {:>12} {:>12}", "domain", "50%mass", "80%mass", "90%mass", "95%mass"); + for (label, hits) in &per_domain { + let total: u64 = hits.values().sum(); + let mut counts: Vec = hits.values().copied().collect(); + counts.sort_unstable_by(|a, b| b.cmp(a)); + let n = counts.len().max(1); + let cell = |f: f64| { let k = experts_for_mass(&counts, total, f); format!("{k} ({:.0}%)", 100.0 * k as f64 / n as f64) }; + println!("{label:<10} {:>10} {:>12} {:>12} {:>12}", cell(0.50), cell(0.80), cell(0.90), cell(0.95)); + } + println!("(the 50%-mass column = the tight resident set; 95%-mass = keep-everything-hot floor; the GAP is the pageable tail)"); - println!("\n=== EXPERT AFFINITY (model-intrinsic, {n_decoded} tokens observed) ==="); - println!("total expert activations : {total}"); - println!("distinct experts fired : {}", hits.len()); - println!("co-occurring pairs seen : {}", cooccur.len()); - println!("prefetch candidates : {} experts", predicted.len()); + // ---- CROSS-DOMAIN overlap: shared base vs pageable specialization ---- + // A domain's HOT SET = the smallest set of experts capturing 50% of that domain's mass. + let hot_set = |hits: &HashMap<_, u64>| -> HashSet<_> { + let total: u64 = hits.values().sum(); + let mut ranked: Vec<(_, u64)> = hits.iter().map(|(k, v)| (*k, *v)).collect(); + ranked.sort_unstable_by(|a, b| b.1.cmp(&a.1)); + let mut acc = 0u64; + let mut set = HashSet::new(); + for (k, v) in ranked { + if (acc as f64) >= 0.5 * total as f64 { + break; + } + acc += v; + set.insert(k); + } + set + }; + let hot_sets: Vec<(String, HashSet<_>)> = + per_domain.iter().map(|(l, h)| (l.clone(), hot_set(h))).collect(); - let mut ranked: Vec<(String, u64)> = hits + // Jaccard overlap matrix of hot sets. + println!("\n=== CROSS-DOMAIN HOT-SET JACCARD (top-experts-to-50%-mass) ==="); + print!("{:<10}", ""); + for (l, _) in &hot_sets { + print!("{l:>9}"); + } + println!(); + for (la, sa) in &hot_sets { + print!("{la:<10}"); + for (_lb, sb) in &hot_sets { + let inter = sa.intersection(sb).count(); + let uni = sa.union(sb).count(); + let j = if uni > 0 { inter as f64 / uni as f64 } else { 0.0 }; + print!("{:>8.2} ", j); + } + println!(); + } + + // Shared base = experts in the hot set of ALL domains. Specialization = hot in exactly one. + let n_domains = hot_sets.len(); + let mut membership: HashMap<_, usize> = HashMap::new(); + for (_l, s) in &hot_sets { + for k in s { + *membership.entry(*k).or_insert(0) += 1; + } + } + let shared_base: HashSet<_> = membership .iter() - .map(|(k, v)| (format!("{k:?}"), *v)) + .filter(|(_, &c)| c == n_domains) + .map(|(k, _)| *k) .collect(); - ranked.sort_by(|a, b| b.1.cmp(&a.1)); - let show = ranked.len().min(24); - println!("\ntop {show} hottest experts (of {} fired):", hits.len()); - for (id, h) in ranked.iter().take(show) { - let pct = if total > 0 { 100.0 * *h as f64 / total as f64 } else { 0.0 }; - println!(" {id:<28} {h:>8} ({pct:.2}%)"); - } - // The tail: how concentrated is activation? (the paging headroom) - if ranked.len() > show { - let tail: u64 = ranked.iter().skip(show).map(|(_, h)| *h).sum(); - let tail_pct = if total > 0 { 100.0 * tail as f64 / total as f64 } else { 0.0 }; + let specialized: HashSet<_> = membership + .iter() + .filter(|(_, &c)| c == 1) + .map(|(k, _)| *k) + .collect(); + println!( + "\nshared base (hot in ALL {n_domains} domains): {} experts", + shared_base.len() + ); + println!( + "domain-specialized (hot in exactly 1) : {} experts", + specialized.len() + ); + + // The money number: per domain, what fraction of activation MASS lands on the shared base + // vs on experts NOT in any OTHER domain's hot set (the pageable eviction win). + println!("\n=== ACTIVATION MASS: shared-base vs pageable-specialized (the eviction win) ==="); + println!("{:<10} {:>14} {:>22}", "domain", "on shared base", "on own-only specialists"); + for (li, (label, hits)) in per_domain.iter().enumerate() { + let total: u64 = hits.values().sum(); + // experts that are in THIS domain's hot set and NO other domain's hot set + let own_only: HashSet<_> = hot_sets[li] + .1 + .iter() + .filter(|k| membership.get(*k).copied().unwrap_or(0) == 1) + .copied() + .collect(); + let mut base_mass = 0u64; + let mut own_mass = 0u64; + for (k, v) in hits { + if shared_base.contains(k) { + base_mass += v; + } + if own_only.contains(k) { + own_mass += v; + } + } + let pct = |m: u64| if total > 0 { 100.0 * m as f64 / total as f64 } else { 0.0 }; println!( - " ... {} colder experts share the remaining {tail_pct:.2}% (the page-out tail)", - ranked.len() - show + "{label:<10} {:>13.1}% {:>21.1}%", + pct(base_mass), + pct(own_mass) ); } + println!( + "\nRead: high own-only mass = a coherent session concentrates on experts the OTHER\n\ + domains never touch → evict them when the workload isn't that domain. High shared-base\n\ + mass = a resident core you never page. That split IS the #180 paging architecture." + ); } From 881c68cc1145d9138813964fb14fbe3905da3d99 Mon Sep 17 00:00:00 2001 From: Joel Teply Date: Tue, 11 Aug 2026 21:59:00 -0500 Subject: [PATCH 4/4] fix(chat): the anchor lookup missed the fallible-executor conversion Rebase fallout from the fail-loud change itself: executor() became Result, String> (that IS the 'no process panic' fix), and every call site needs `?`. The chat/poll anchor lookup at mod.rs:155 was the one that did not get converted, so it called execute_json on a Result. One character. Caught by cargo check, not by review. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc --- core/continuum-core/src/modules/chat/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/continuum-core/src/modules/chat/mod.rs b/core/continuum-core/src/modules/chat/mod.rs index b56e2919d..b76e3d962 100644 --- a/core/continuum-core/src/modules/chat/mod.rs +++ b/core/continuum-core/src/modules/chat/mod.rs @@ -153,7 +153,7 @@ impl ChatModule { }); let anchor_result = self - .executor() + .executor()? .execute_json("data/query", anchor_query) .await .map_err(|e| format!("chat/poll: anchor lookup failed: {e}"))?;