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;