nx_speculative_verify.nx source
↩ module page · 231 lines · 9538 B
1// nx_speculative_verify.nx -- H3 speculative decoding bits-up.
2//
3// Per NISHI_ELDER_AI_OFF_DOCKER_2026_05_20.md ยง2.1 H3: small fast
4// draft model proposes K tokens; large target model verifies them
5// in one parallel forward pass; substrate keeps the longest
6// accept-prefix + 1 bonus token (the target's own next-token
7// prediction at the rejection boundary is free). Leviathan 2023
8// + Chen 2023 "Accelerating Large Language Model Decoding with
9// Speculative Sampling." Practical wins: 2-3x throughput at
10// equivalent quality when draft + target agree on common tokens.
11//
12// V1 mechanics: the substrate primitive does NOT run a model.
13// It accepts the draft tokens (what the small model proposed)
14// and a parallel accept_flag array (1 == target verified, 0 ==
15// target rejected) computed upstream by the verification kernel,
16// PLUS the target's own next-token at the rejection boundary
17// (the "free" bonus token). It returns the longest accept-prefix
18// length and the count of tokens actually committed to the
19// caller's output buffer.
20//
21// Composition path:
22// - nx_speculative_session_new(K, request_id) on admit
23// - For each verification round: nx_speculative_verify(session,
24// draft_tokens, accept_flags, K_actual, target_next_token,
25// out_committed) returns count_committed
26// - Per-session stats (drafted / accepted / rounds) accumulate
27// so the consumer can monitor the acceptance rate and back
28// off speculative depth when the draft model is mispredicting
29// - Acceptance rate exposed as Q16 fixed-point (rate * 65536)
30// to keep the substrate primitive integer-only
31//
32// Pure substrate logic. No Linux features. Composes with shipped
33// nx_batch_scheduler (each batch slot can carry a NxSpeculativeSession
34// pointer for its decoding state).
35//
36// V1 honest scope:
37// - Fixed max-K (NX_SPEC_MAX_K = 16) per round
38// - Caller provides verification flags (substrate doesn't run the
39// target forward pass; that's a tensor kernel one layer up)
40// - Greedy verification only (no sampled-rejection-resampling;
41// queued for V2 when probabilistic-sampling lands)
42// - Stats are per-session only (no global aggregation; that's
43// a consumer concern)
44//
45// genealogy_id: leviathan_speculative_sampling_2023 +
46// chen_accelerating_llm_decoding_2023 +
47// cardinal_2026-05-20_elder_ai_off_docker +
48// cardinal_2026-05-20_bits_up_nishi_not_linux
49// lineage_id: substrate_speculative_verify_v1
50//
51// nx_capability_manifest:
52// variant_class: speculative_decoding
53// variant_id: speculative_verify_v1_greedy
54// requires_isa: [rv32i, rv64imac, x86_64, cortex_m, armv7a, aarch64, wasm32]
55// requires_syscalls: [mmap]
56// requires_ram_min_b: 2048
57// tier_floor: NX_TIER_INF_MOBILE
58// tier_ceiling: NX_TIER_INF_HPC
59// cost_model:
60// flops_per_n: 1.0 // O(K) verify per round
61// bytes_per_n: 16.0 // session bookkeeping
62// syscalls_per_n: 0.0
63// adversary_class: THREAT_AI_ADVERSARY
64//
65// nx_safety_envelope:
66// intended_use: "Speculative-decoding accept-prefix calculator;
67// bits-up; composes with nx_batch_scheduler"
68// sil_target: SIL2
69// evidence: [canary_bracketed, greedy_prefix_invariant,
70// stats_monotonic, bonus_token_at_boundary]
71// verdict: NOT_YET_EVALUATED
72
73import "nx_syscalls.nx"
74const NX_MAGIC_65536: i64 = 65536
75
76// ===== Constants =================================================
77const NX_SPEC_MAX_K: i64 = 16
78
79// Verdicts.
80const NX_SPEC_OK: i64 = 0
81const NX_SPEC_BAD_INPUT: i64 = 1
82const NX_SPEC_BAD_K: i64 = 2
83const NX_SPEC_TAMPER: i64 = 3
84const NX_SPEC_N_VERDICTS: i64 = 4
85
86func nx_spec_verdict_is_valid(v: i64) -> i64 {
87 if v < 0 { return 0 }
88 if v >= NX_SPEC_N_VERDICTS { return 0 }
89 return 1
90}
91
92// Canary magic.
93const NX_SPEC_SESSION_CANARY_PRE: i64 = 0x537065635365737A // "SpecSesz"
94const NX_SPEC_SESSION_CANARY_POST: i64 = 0x53657373456E6464 // "SessEndd"
95
96// ===== Session struct =================================================
97struct NxSpeculativeSession {
98 canary_pre: i64,
99 request_id: i64,
100 max_k: i64, // upper bound on per-round drafts; <= NX_SPEC_MAX_K
101 total_drafted: i64, // running total of draft tokens seen
102 total_accepted: i64, // running total accepted (excludes bonus)
103 total_committed: i64, // running total committed (accepted + bonus)
104 total_rounds: i64, // number of verify rounds processed
105 canary_post: i64,
106}
107
108func nx_spec_session_is_valid(s: *NxSpeculativeSession) -> i64 {
109 if (s as i64) == 0 { return 0 }
110 if s.canary_pre != NX_SPEC_SESSION_CANARY_PRE { return 0 }
111 if s.canary_post != NX_SPEC_SESSION_CANARY_POST { return 0 }
112 if s.max_k <= 0 { return 0 }
113 if s.max_k > NX_SPEC_MAX_K { return 0 }
114 if s.total_drafted < 0 { return 0 }
115 if s.total_accepted < 0 { return 0 }
116 if s.total_committed < 0 { return 0 }
117 if s.total_rounds < 0 { return 0 }
118 // Monotonic invariant: accepted <= drafted; committed = accepted + rounds (bonus per round)
119 if s.total_accepted > s.total_drafted { return 0 }
120 return 1
121}
122
123// ===== Constructor =================================================
124func nx_speculative_session_new(request_id: i64, max_k: i64) -> *NxSpeculativeSession {
125 if max_k <= 0 { return (0 as i64) as *NxSpeculativeSession }
126 if max_k > NX_SPEC_MAX_K { return (0 as i64) as *NxSpeculativeSession }
127
128 let s: *NxSpeculativeSession = (sys_mmap(64)) as *NxSpeculativeSession
129 s.canary_pre = NX_SPEC_SESSION_CANARY_PRE
130 s.request_id = request_id
131 s.max_k = max_k
132 s.total_drafted = 0
133 s.total_accepted = 0
134 s.total_committed = 0
135 s.total_rounds = 0
136 s.canary_post = NX_SPEC_SESSION_CANARY_POST
137 return s
138}
139
140// ===== Verify =================================================
141// Inputs:
142// s -- session
143// draft_tokens -- *i64 array of K_actual token ids the draft model produced
144// accept_flags -- *i64 array of K_actual; 1 == target verified, 0 == rejected
145// k_actual -- count of draft tokens this round (1 <= k <= session.max_k)
146// target_next -- target model's predicted next-token AT the rejection
147// boundary (or after the last-accepted draft, if all K
148// accepted). This is the "free" bonus token.
149// out_committed -- *i64 array, capacity at least max_k+1; receives the
150// committed token sequence.
151//
152// Returns: count of tokens written to out_committed. On success this is
153// (accept_prefix_len + 1). On error returns 0 - <verdict>.
154func nx_speculative_verify(s: *NxSpeculativeSession, draft_tokens: *i64, accept_flags: *i64, k_actual: i64, target_next: i64, out_committed: *i64) -> i64 {
155 if nx_spec_session_is_valid(s) != 1 { return 0 - NX_SPEC_TAMPER }
156 if k_actual <= 0 { return 0 - NX_SPEC_BAD_K }
157 if k_actual > s.max_k { return 0 - NX_SPEC_BAD_K }
158 if (draft_tokens as i64) == 0 { return 0 - NX_SPEC_BAD_INPUT }
159 if (accept_flags as i64) == 0 { return 0 - NX_SPEC_BAD_INPUT }
160 if (out_committed as i64) == 0 { return 0 - NX_SPEC_BAD_INPUT }
161
162 // Longest-accept-prefix: count consecutive 1s until first non-1.
163 var real_prefix: i64 = 0
164 var i: i64 = 0
165 var done: i64 = 0
166 while i < k_actual {
167 if done == 0 {
168 if accept_flags[i] == 1 {
169 real_prefix = real_prefix + 1
170 } else {
171 done = 1
172 }
173 }
174 i = i + 1
175 }
176
177 // Emit committed sequence: prefix tokens from draft + bonus target_next.
178 var j: i64 = 0
179 while j < real_prefix {
180 out_committed[j] = draft_tokens[j]
181 j = j + 1
182 }
183 out_committed[real_prefix] = target_next
184
185 s.total_drafted = s.total_drafted + k_actual
186 s.total_accepted = s.total_accepted + real_prefix
187 s.total_committed = s.total_committed + real_prefix + 1
188 s.total_rounds = s.total_rounds + 1
189
190 return real_prefix + 1
191}
192
193// ===== Stats accessors =================================================
194func nx_spec_total_drafted(s: *NxSpeculativeSession) -> i64 {
195 if nx_spec_session_is_valid(s) != 1 { return 0 - 1 }
196 return s.total_drafted
197}
198
199func nx_spec_total_accepted(s: *NxSpeculativeSession) -> i64 {
200 if nx_spec_session_is_valid(s) != 1 { return 0 - 1 }
201 return s.total_accepted
202}
203
204func nx_spec_total_committed(s: *NxSpeculativeSession) -> i64 {
205 if nx_spec_session_is_valid(s) != 1 { return 0 - 1 }
206 return s.total_committed
207}
208
209func nx_spec_total_rounds(s: *NxSpeculativeSession) -> i64 {
210 if nx_spec_session_is_valid(s) != 1 { return 0 - 1 }
211 return s.total_rounds
212}
213
214// Acceptance rate as Q16 fixed-point: (total_accepted * 65536) / total_drafted.
215// Returns 0 when total_drafted == 0 (no rounds yet -> rate undefined, report 0).
216func nx_spec_acceptance_rate_q16(s: *NxSpeculativeSession) -> i64 {
217 if nx_spec_session_is_valid(s) != 1 { return 0 - 1 }
218 if s.total_drafted == 0 { return 0 }
219 let num: i64 = s.total_accepted * NX_MAGIC_65536
220 return num / s.total_drafted
221}
222
223func nx_spec_session_request_id(s: *NxSpeculativeSession) -> i64 {
224 if nx_spec_session_is_valid(s) != 1 { return 0 - 1 }
225 return s.request_id
226}
227
228func nx_spec_session_max_k(s: *NxSpeculativeSession) -> i64 {
229 if nx_spec_session_is_valid(s) != 1 { return 0 - 1 }
230 return s.max_k
231}