atlas_kernels/lib.rs
1// SPDX-License-Identifier: AGPL-3.0-only
2
3#![deny(warnings)]
4#![deny(clippy::all)]
5
6//! Atlas CUDA kernel PTX modules.
7//!
8//! Single source of truth for embedded PTX. The `spark-runtime`
9//! (pure Rust engine) and benchmarks consume these.
10//!
11//! PTX modules are grouped by [`KernelTarget`] — each `(H, M_q)`
12//! tuple maps to a distinct set of hyperoptimized kernels.
13//!
14//! Constants, `ptx_modules()`, and `all_ptx_sets()` are auto-generated
15//! by `build.rs` from the `kernels/{hw}/{model}/{quant}/` directories.
16//! When `ATLAS_TARGET_MODEL=*` or `ATLAS_TARGET_QUANT=*`, multiple
17//! targets are compiled and available at runtime.
18
19// Used by the `include!`d `target_ptx.rs` below, which constructs
20// `KernelTarget { .. }` literals in THIS module's scope. The
21// `ATLAS_SKIP_BUILD=1` stub emits an empty `all_ptx_sets()` and names no
22// target, so the import is genuinely unused in that one build mode --
23// hence the allow, which `#![deny(warnings)]` would otherwise turn into a
24// host-only build failure.
25#[allow(unused_imports)]
26use atlas_core::target::KernelTarget;
27
28pub mod resolve;
29pub use resolve::{ResolveCandidate, TargetResolveError, ptx_for_config, ptx_for_exact_target};
30
31// Build-time/run-time shared `[behavior]` defaults — also `include!`d by
32// `build_parse_behavior.rs` so the build script's parse defaults cannot
33// drift from `ModelBehavior::default()` (the #328 failure mode).
34mod behavior_defaults;
35pub use behavior_defaults::{
36 DEFAULT_EFFORT_CAPPED_AT_CEILING, DEFAULT_MAX_INTER_TOOL_PROSE, DEFAULT_MAX_THINKING_BUDGET,
37};
38
39// The compiled target's SERVING defaults, baked from
40// `kernels/<hw>/HARDWARE.toml` `[defaults]`. The TYPE is hand-written here;
41// the `TARGET_DEFAULTS` and `TARGET_SM_COUNT` consts are generated into the
42// same `target_ptx.rs` the kernel registry lives in — one generated file, one
43// `include!`, one content hash, so there is exactly one thing that can go
44// stale and `ATLAS_KERNEL_SET_HASH` below already covers it.
45mod target_defaults;
46pub use target_defaults::TargetDefaults;
47
48// The paged-decode attention split-K policy (#928). Lives HERE, below
49// spark-model, because two crates need the same answer: the dispatch that
50// picks `num_splits` and the buffer arena that sizes the split-K workspace
51// (`spark-runtime`'s `sizes.rs`). One pure rule, two call sites — a second
52// copy is how the grid comes to index past the allocation.
53pub mod attn_splitk;
54pub use attn_splitk::{MAX_DECODE_SPLITS, SplitkPolicy};
55
56// Auto-generated: per-target PTX constants, ptx_modules() function,
57// all_ptx_sets() for multi-target builds, and `TARGET_DEFAULTS` /
58// `TARGET_SM_COUNT`.
59// NOTE: cargo does NOT track this build-script-generated include! as a
60// recompile trigger, so when build.rs regenerates target_ptx.rs (e.g. the
61// module set changes) this lib can keep a STALE embedded set. Any edit to
62// this file (or `cargo clean -p atlas-kernels`) forces a fresh recompile
63// against the current OUT_DIR/target_ptx.rs.
64include!(concat!(env!("OUT_DIR"), "/target_ptx.rs"));
65
66/// Content fingerprint of the generated kernel set, emitted by `build.rs` as a
67/// `rustc-env`. Referencing it here makes cargo recompile this crate whenever
68/// the kernel set changes — closing the `include!`-not-tracked staleness hole
69/// that silently embedded a stale module list (the 98-vs-99 regression).
70pub const KERNEL_SET_HASH: &str = env!("ATLAS_KERNEL_SET_HASH");
71
72/// What each kernel target in THIS binary was compiled from, as JSON.
73///
74/// A map of `"hardware/model/quant"` to `{hash, arch, compiler, flags}`, baked
75/// by `build.rs` at the moment the kernels were compiled. The benchmark gate
76/// copies it into a record so the record attests to the binary's sources rather
77/// than to whatever the working tree held when the record was written — a
78/// commit sha does not describe a stale `target/`, a dirty tree, or an image
79/// carried between boxes.
80///
81/// `{}` when the build compiled nothing (`ATLAS_SKIP_BUILD=1`) or could not
82/// identify its compiler. That is not an error: an empty attestation excuses no
83/// future diff, so such a binary's records behave exactly as they did before
84/// attestations existed.
85///
86/// `option_env!` rather than `env!` so the crate still builds against an
87/// `OUT_DIR` produced by an older build script.
88pub const TARGET_CLOSURES: &str = match option_env!("ATLAS_TARGET_CLOSURES") {
89 Some(json) => json,
90 None => "{}",
91};
92
93// ═══════════════════════════════════════════════════════════════════
94// Target-aware PTX grouping
95// ═══════════════════════════════════════════════════════════════════
96
97/// Per-category sampling defaults from MODEL.toml.
98#[derive(Debug, Clone, Copy)]
99pub struct SamplingCategory {
100 pub temperature: f32,
101 pub top_p: f32,
102 pub top_k: u32,
103 pub presence_penalty: f32,
104 pub frequency_penalty: f32,
105 /// Multiplicative penalty on already-seen tokens (1.0 = disabled).
106 /// Populated from MODEL.toml `[sampling.*].repetition_penalty` via build.rs.
107 pub repetition_penalty: f32,
108 /// DRY (Don't-Repeat-Yourself) sampler parameters. Penalises tokens
109 /// that extend repeated n-grams past `dry_allowed_length` with an
110 /// exponential `dry_multiplier * dry_base^(match_len - allowed)` —
111 /// the targeted fix for phrase-level attractors (e.g. the
112 /// ```` ```bash cd … cargo test ``` ```` fence-narration loop
113 /// observed in Qwen3.5-35B-A3B-FP8 opencode sessions at turn ≥ 8).
114 ///
115 /// `presence_penalty` on its own is a FLAT per-unique-token hit
116 /// (does not scale with repetition count), so it can't break a
117 /// phrase attractor where individual tokens already paid their
118 /// penalty once. DRY scales with the repeat-length and is the
119 /// published remedy (oobabooga/text-generation-webui#5677, used in
120 /// llama.cpp / Aphrodite / TabbyAPI).
121 ///
122 /// `dry_multiplier = 0.0` disables DRY for this category (default
123 /// for every preset unless MODEL.toml sets it explicitly).
124 pub dry_multiplier: f32,
125 pub dry_base: f32,
126 pub dry_allowed_length: u32,
127 /// LZ penalty (arXiv:2504.20131). Per-extension n-gram penalty
128 /// over a 256-token rolling window. Frequency-weighted and length-
129 /// scaled, so it correctly distinguishes "phrase loop" from
130 /// "legitimate vocabulary reuse" without the flat-per-token
131 /// `presence_penalty` regression. 0.0 = disabled. SGLang reference
132 /// strength = 0.2 (lossless on AIME/GPQA).
133 pub lz_penalty: f32,
134 /// Model-declared min-p, or `None` when MODEL.toml is silent.
135 ///
136 /// `Option`, not `f32`, because absence and `0.0` mean opposite things
137 /// here. The server ships `--default-min-p 0.08` and every request that
138 /// does not name min_p takes it, so a model whose card specifies
139 /// `min_p = 0` had no way to say so: `[behavior].min_p_floor` only ever
140 /// RAISES min_p (`min_p.max(floor)`), and the preset did not carry the
141 /// value at all. A plain `f32` defaulting to 0.0 would silently strip the
142 /// 0.08 floor from every model that has a `[sampling.*]` table, which is
143 /// the opposite regression.
144 ///
145 /// `Some(x)` outranks the CLI default and is outranked by
146 /// `generation_config.json` — the same precedence temperature/top_k/top_p
147 /// already follow. `None` preserves the CLI-owned behaviour exactly.
148 pub min_p: Option<f32>,
149 /// Model-declared top-n-sigma, or `None` when MODEL.toml is silent.
150 ///
151 /// Same absence-vs-zero problem as `min_p`: the server ships
152 /// `--default-top-n-sigma 1.0`, so a model whose card asks for NO sigma
153 /// filter had no way to say so. `Some(0.0)` disables it; `None` leaves the
154 /// CLI default owning the field.
155 pub top_n_sigma: Option<f32>,
156}
157
158/// Model-specific sampling presets loaded from MODEL.toml `[sampling.*]`.
159#[derive(Debug, Clone, Copy)]
160pub struct SamplingPresets {
161 pub thinking_text: SamplingCategory,
162 pub thinking_coding: SamplingCategory,
163 pub non_thinking: SamplingCategory,
164 /// Tool-calling preset: model-recommended sampling for agentic tasks.
165 /// Qwen3.5 recommends temperature=0.6 (NOT greedy) to avoid repetition loops.
166 pub tools: SamplingCategory,
167}
168
169impl Default for SamplingPresets {
170 fn default() -> Self {
171 let default_cat = SamplingCategory {
172 temperature: 0.7,
173 top_p: 0.95,
174 top_k: 20,
175 presence_penalty: 0.0,
176 frequency_penalty: 0.0,
177 repetition_penalty: 1.0,
178 // DRY defaults = disabled (multiplier 0.0). Per-MODEL.toml
179 // tools presets opt in when the model needs it.
180 dry_multiplier: 0.0,
181 dry_base: 1.75,
182 dry_allowed_length: 2,
183 lz_penalty: 0.0,
184 min_p: None,
185 top_n_sigma: None,
186 };
187 let tools_cat = SamplingCategory {
188 temperature: 0.6,
189 top_p: 0.95,
190 top_k: 20,
191 presence_penalty: 0.0,
192 frequency_penalty: 0.0,
193 repetition_penalty: 1.0,
194 dry_multiplier: 0.0,
195 dry_base: 1.75,
196 dry_allowed_length: 2,
197 lz_penalty: 0.0,
198 min_p: None,
199 top_n_sigma: None,
200 };
201 Self {
202 thinking_text: default_cat,
203 thinking_coding: default_cat,
204 non_thinking: default_cat,
205 tools: tools_cat,
206 }
207 }
208}
209
210/// Model-specific behavior flags from MODEL.toml `[behavior]`.
211#[derive(Debug, Clone)]
212pub struct ModelBehavior {
213 /// Allow thinking when tools are active. Default: true.
214 pub thinking_in_tools: bool,
215 /// Maximum thinking budget (tokens). Default:
216 /// [`DEFAULT_MAX_THINKING_BUDGET`].
217 pub max_thinking_budget: u32,
218 /// Clamp qualitative `reasoning_effort` levels at the model's effective
219 /// ceiling (high/xhigh resolve to `max_thinking_budget` instead of
220 /// 2x/4x it). Default [`DEFAULT_EFFORT_CAPPED_AT_CEILING`] = `false`
221 /// (historical ladder shape). See `behavior_defaults.rs` for when a
222 /// model should set `true` (measured budget non-monotonicity).
223 pub effort_capped_at_ceiling: bool,
224 /// Default thinking state for this model when the client request does not
225 /// specify a reasoning_effort / thinking parameter. Typical values:
226 /// - thinking-first models (Mistral Small 4, Qwen3.5, …): `true`
227 /// - instruct-only models with no `<think>` tokens: `false`
228 ///
229 /// Overridden per-request by `reasoning_effort`, and globally by the
230 /// `--disable-thinking` CLI flag.
231 pub thinking_default: bool,
232 /// Default FP8 KV calibration tokens (0 = disabled).
233 pub fp8_kv_calibration_tokens: usize,
234 /// Default KV cache dtype from MODEL.toml (e.g., "bf16", "fp8").
235 /// When non-empty, overrides the CLI default for models that need
236 /// higher precision. User can still override with explicit --kv-cache-dtype.
237 pub default_kv_dtype: &'static str,
238 /// Default num_drafts for speculative decoding (0 = use CLI default).
239 /// K = num_drafts + 1 (num_drafts=1 → K=2 verifies 2 tokens per step).
240 /// Optimal K varies per model; benchmarks sometimes show K=2 beats K=3.
241 /// User override with --num-drafts still wins.
242 pub default_num_drafts: u32,
243 /// Skip the `<tool_call>\n` steering prefix in the chat template's
244 /// generation prompt. Some Nemotron variants (Super 120B) weren't
245 /// trained on qwen3_coder XML and emit a `<tool_call>` token loop
246 /// when the prefix forces them into that structure. Default: false
247 /// (keep the existing Nemotron-Nano-correct behavior).
248 pub disable_tool_steering: bool,
249 /// Do not append Atlas's derived `<environment>working_directory` block to
250 /// a client system prompt. Native agent clients may already provide the
251 /// cwd; duplicating it can become a tool-selection attractor.
252 pub disable_cwd_hint_injection: bool,
253 /// Use the selected MODEL.toml sampling category for default temperature,
254 /// top-k, and top-p instead of generation_config.json. Explicit request
255 /// values still take precedence.
256 pub use_sampling_presets_for_core: bool,
257 /// Per-model tool-call parser override. Empty string = use the
258 /// `tool_defaults.toml` mapping for this `model_type`. Set in MODEL.toml
259 /// `[behavior].tool_call_parser` when one variant of a model_type needs
260 /// a different parser than its siblings (e.g. Nemotron-Super-120B uses
261 /// `bare_json` while Nemotron-Nano-30B stays on `qwen3_coder`).
262 pub tool_call_parser: &'static str,
263 /// Enable the content-loop watchdog (period-N token-repetition detector
264 /// at `decode_logits_step.rs:230`). Default: `false` — most models
265 /// terminate cleanly via EOS / `max_tokens` without it. Models with a
266 /// known prose-attractor failure mode (Qwen3.5-35B-A3B's "Running:```bash
267 /// cmd```Executing:" loop, observed during agentic Claude Code sessions)
268 /// should set this `true` in MODEL.toml `[behavior]`.
269 ///
270 /// The watchdog has false-positives on legitimate structured output
271 /// (chess board JS init `{color:BLACK,type:'P'},` × 8, HTML tables,
272 /// JSON arrays of similar objects, multiplication tables). Enable only
273 /// when the model has been observed to need it.
274 pub enable_loop_watchdog: bool,
275 /// See build_parse.rs: gate for the THINKING-phase loop watchdog.
276 pub enable_think_loop_watchdog: bool,
277 /// See build_parse_behavior.rs: honor a mid-`<think>` EOS by implicitly
278 /// closing the block. Defaults FALSE (pre-p350 behaviour).
279 pub honor_eos_inside_thinking: bool,
280 /// A4 floor: suppress `</think>` until this many think tokens
281 /// (16 = historical constant; 0 disables — card-native brief thinking).
282 pub min_reasoning_floor_tokens: u32,
283 /// Cap the thinking budget at 90% of the request's `max_tokens` (true), or
284 /// let `max_thinking_budget` be the sole cap (false = vLLM single-budget:
285 /// reasoning may use the full generation budget). See thinking.rs::resolve.
286 pub cap_thinking_at_max_tokens: bool,
287 /// Server-side min-p FLOOR (0.0 = disabled). Applied as `min_p.max(floor)`
288 /// AFTER request/preset resolution, so it binds even when a client sends
289 /// `min_p = 0` (or omits it on a server without `--default-min-p`). On
290 /// drift-prone quantized models (FP8 / NVFP4 lm-head) an unfloored tail
291 /// lets the degenerate low-probability tail be sampled into repetition
292 /// loops + argmax-flip garbling on long generation — the Claude-Code
293 /// failure mode. MEASURED 2026-06-07 (nvfp4-head@64k): 0.05 turned 4 loop-
294 /// watchdog fires → 0. Set in MODEL.toml `[behavior]`.
295 pub min_p_floor: f32,
296 /// Server-side temperature CEILING (0.0 = disabled). `temperature.min(max)`
297 /// AFTER resolution — defense-in-depth net against a client sending a high
298 /// temperature; min_p_floor is the dominant lever. Set in MODEL.toml.
299 pub temperature_max: f32,
300 /// Thinking-loop watchdog: substring-occurrence count that trips a
301 /// forced `</think>`. Default 3 (historical `THINK_LOOP_MIN_REPEATS`).
302 pub think_loop_min_repeats: u32,
303 /// Thinking-loop watchdog: trailing-token scan window. Default 160.
304 pub think_loop_scan_window: u32,
305 /// F2 confidence-run early-stop enabled. Default `true`. Set false
306 /// for models whose deterministic code drafting trips the heuristic.
307 pub confidence_early_stop: bool,
308 /// F2 confidence run length before arming forced `</think>`.
309 /// Default 30.
310 pub confidence_run_length: u32,
311 /// Fuzzy-repetition detector Hamming tolerance divisor: a
312 /// `pattern_len`-token window tolerates `pattern_len / div`
313 /// mismatches. Default 12 (~8%).
314 pub fuzzy_repeat_tolerance_div: u32,
315 /// Cap on free-text tokens between successive `<tool_call>` opens in
316 /// `tool_choice=auto`. Default [`DEFAULT_MAX_INTER_TOOL_PROSE`]
317 /// (see `behavior_defaults.rs` for the tuning history — #328).
318 pub max_inter_tool_prose: u32,
319 /// Unconditional per-generation cap on post-`</think>` content tokens
320 /// for tool-active requests (grammar attached). Bounds a runaway where
321 /// a grammar-legal-but-never-closing tool value burns to `max_tokens`
322 /// (the dominant opencode `webserver_ok` 360s-timeout cause). Default
323 /// 100_000 — effectively unbounded, the historical no-op — so a model
324 /// that sets nothing is byte-identical to before. Set a small value
325 /// (e.g. 1536) per-model to backstop the runaway. Never caps plain
326 /// chat: the runtime gate also requires `grammar_state.is_some()`.
327 pub max_post_think_content_tokens: u32,
328 /// TSCG (Tool-Schema Compilation) enabled — compile tool JSON
329 /// schemas to compact function signatures before prompting.
330 /// Default `false`; the TAS operator is tokenizer-specific so
331 /// enable + verify per model. arXiv:2605.04107.
332 pub tscg: bool,
333 /// Disable XGrammar tool-call constrained decoding for this model.
334 /// Default `false`. Escape hatch for the "structure snowballing"
335 /// alignment tax (arXiv:2604.06066) — a few models tool-call more
336 /// reliably unconstrained. When `true`, tool calls are parsed but
337 /// not grammar-enforced.
338 pub disable_tool_grammar: bool,
339 /// Phase-C: when a decode-time watchdog (content-loop, fuzzy-repeat,
340 /// inter-tool prose) detects degeneration, roll the sequence back to
341 /// the last well-formed boundary and let generation re-steer, instead
342 /// of hard-stopping the response. Default `true` (recovers responses,
343 /// especially mid-tool-call — arXiv:2603.27905 ATLAS-RTC). Set `false`
344 /// to keep the legacy hard-stop behavior. Capped at
345 /// [`crate::ROLLBACK_RESTEER_CAP`] rollbacks per sequence, after which
346 /// the hard-stop fires regardless.
347 pub rollback_resteer: bool,
348 /// Phase-C ROM (arXiv:2603.22016) scaffold. Path to a trained
349 /// repetition-onset detection head artifact. Empty string = no ROM
350 /// head; the F2 confidence heuristic stays as the fallback. A trained
351 /// artifact can be dropped in later via MODEL.toml
352 /// `[behavior].rom_head` without further code changes — the runtime
353 /// loads it through the `RomHead` trait seam. The detector
354 /// itself is intentionally NOT implemented (no per-model trained head
355 /// is available); only the optional hook is wired.
356 pub rom_head: &'static str,
357 /// Tier 5c (2026-05-26): one-shot tool-call re-roll on hard
358 /// validation failure. When `true`, `validate_tool_calls` errors on
359 /// the chat path fire a single retry inference with the same
360 /// grammar spec + a correction nudge appended to the prompt. If the
361 /// retry produces valid tool calls, they replace the failed call
362 /// before the response leaves the server. Default `true` — the
363 /// blocking-path canonical-probe trace shows a write-→bash recovery
364 /// path that's strictly better than the previous "`[atlas]` Tool call
365 /// rejected" content fallback. Set `false` per-model when a
366 /// specific model is known to ALWAYS get tool args right on the
367 /// first attempt (extra inference round-trip cost is wasted there).
368 pub tool_retry: bool,
369 /// Jinja `preserve_thinking` chat-template flag (Qwen3.6+ dense family):
370 /// keep historical `<think>` blocks in re-rendered assistant turns
371 /// instead of stripping them before the last user query.
372 ///
373 /// Tri-state on purpose (SSOT): `None` = do NOT inject the variable —
374 /// the model's own template default applies (Qwen3.6 strips unless
375 /// `preserve_thinking` is true; Qwen3.8 KEEPS unless it is explicitly
376 /// false). `Some(_)` pins the value for this target, changing
377 /// multi-turn prompt bytes and therefore prefix-cache hit rate.
378 /// Per-request `chat_template_kwargs.preserve_thinking` still wins.
379 pub preserve_thinking: Option<bool>,
380}
381
382/// Phase-C: maximum number of watchdog-triggered rollbacks a single
383/// sequence may perform before the watchdog reverts to a hard stop.
384/// Bounds the worst case where re-steering re-enters the same attractor
385/// — without this a degenerate sequence could rollback indefinitely.
386pub const ROLLBACK_RESTEER_CAP: u32 = 2;
387
388/// Phase-C: number of boundary SSM-state snapshots retained per sequence in
389/// the decode-rollback ring (hybrid GDN/Mamba models). DECOUPLED from
390/// [`ROLLBACK_RESTEER_CAP`]: the cap bounds how many times we re-steer, but
391/// the ring must retain enough *boundary* snapshots that a clean PRE-loop
392/// boundary survives long enough to roll back to. Sizing it at the old
393/// `CAP + 1 = 3` meant a loop spanning ≥3 sentence/newline boundaries evicted
394/// the clean boundary before the fuzzy detector (3 repeats) fired, forcing a
395/// `NoSsmSnapshot` decline → hard-stop (observed: Claude-Code @ nvfp4-head,
396/// 2026-06-07). 8 covers the 3-repeat detector with margin at modest cost
397/// (8 × max_batch × per-layer GDN state, allocated once). Pure-attention
398/// models ignore this (their ring is 0; they roll back to any boundary).
399pub const DECODE_ROLLBACK_RING_SLOTS: usize = 8;
400
401/// Domain salt folded into every decode cold-tier key so a decode-ring blob can
402/// never collide with a Marconi prefix-hash key on a shared store/peer.
403pub const DECODE_DOMAIN: u64 = 0xD3C0_DE12_A5B6_C7D8;
404
405impl Default for ModelBehavior {
406 fn default() -> Self {
407 Self {
408 thinking_in_tools: true,
409 max_thinking_budget: DEFAULT_MAX_THINKING_BUDGET,
410 effort_capped_at_ceiling: DEFAULT_EFFORT_CAPPED_AT_CEILING,
411 thinking_default: false,
412 fp8_kv_calibration_tokens: 0,
413 default_kv_dtype: "",
414 default_num_drafts: 0,
415 disable_tool_steering: false,
416 disable_cwd_hint_injection: false,
417 use_sampling_presets_for_core: false,
418 tool_call_parser: "",
419 enable_loop_watchdog: false,
420 enable_think_loop_watchdog: true,
421 honor_eos_inside_thinking: false,
422 min_reasoning_floor_tokens: 16,
423 cap_thinking_at_max_tokens: true,
424 min_p_floor: 0.0,
425 temperature_max: 0.0,
426 think_loop_min_repeats: 3,
427 think_loop_scan_window: 160,
428 confidence_early_stop: true,
429 confidence_run_length: 30,
430 fuzzy_repeat_tolerance_div: 12,
431 max_inter_tool_prose: DEFAULT_MAX_INTER_TOOL_PROSE,
432 max_post_think_content_tokens: 100_000,
433 tscg: false,
434 disable_tool_grammar: false,
435 rollback_resteer: true,
436 rom_head: "",
437 tool_retry: true,
438 preserve_thinking: None,
439 }
440 }
441}
442
443mod ptx_set;
444pub use ptx_set::{DflashConfig, ModelTypeMatch, TargetPtxSet};
445
446mod query;
447pub use query::{available_targets, ptx_for_model};
448
449#[cfg(test)]
450#[path = "lib_tests.rs"]
451mod tests;