met 1.0.0-beta-preview

Metrale Engine: pure Rust LLM inference server (HTTP API, scheduler, TUI, CLI).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
// SPDX-License-Identifier: MIT OR Apache-2.0

//! 2026-09-26: Arguments of `met serve` (`ServeArgs`, re-exported as `cli::ServeArgs`) and the engine fallbacks of three of its flags.
//!
//! Owner: server CLI.
//! Invariants: none beyond the types. The `///` text on `ServeArgs` and its fields is
//! the `--help` output and carries no date.
use clap::Parser;
use metrale_model_layers::layers::ExpertQuantization;
use std::path::PathBuf;

use super::flag_values::{ExpertQuantizationArg, WeightQuantizationArg};

mod scheduling;
mod service;
#[cfg(test)]
mod stageable_spec_tests;

/// 2026-09-26: Default of `--request-timeout`, in seconds. `AppState::request_deadline`
/// applies the flag's value to every request that sets no `timeout` of its own.
pub const DEFAULT_REQUEST_TIMEOUT_SECS: u32 = 300;

/// 2026-09-26: Engine fallback for `--num-drafts` when neither the flag nor MODEL.toml
/// `[behavior].default_num_drafts` gives a value; applied in
/// `serve_phases::config::resolve_num_drafts`.
pub const DEFAULT_NUM_DRAFTS: usize = 1;

/// 2026-09-26: Engine fallback for `--kv-cache-dtype` when neither the flag nor
/// MODEL.toml `[behavior].default_kv_dtype` gives a value; applied in
/// `serve_phases::kv_cache::resolve_kv_dtype_str`.
pub const DEFAULT_KV_CACHE_DTYPE: &str = "fp8";

/// Arguments for the `serve` subcommand.
#[derive(Parser, Debug, Clone, PartialEq)]
pub struct ServeArgs {
    /// HuggingFace model ID (e.g. "nvidia/Qwen3-Next-80B-A3B-Instruct-NVFP4")
    /// or a local model directory.
    /// Optional: with neither this nor `--model-from-path`, the server boots into
    /// the dashboard's Library to pick a model there. Without a dashboard (plain
    /// mode) startup is refused, since there is nothing to pick a model with.
    #[arg(value_name = "MODEL")]
    pub model: Option<String>,

    /// Load a different model when a request names one.
    ///
    /// Off by default. Only a request whose `model` exactly matches the model id of
    /// a known recipe other than the live model triggers a load; an absent, unknown
    /// or already-live name is served by the current model.
    ///
    /// It covers request-triggered swaps only: the dashboard can change the model
    /// without it.
    #[arg(long)]
    pub auto_swap: bool,

    /// Serve even when kernel lookups this model's dispatch issued did not
    /// resolve.
    ///
    /// By default startup is refused and the unresolved lookups are listed, each
    /// with its dispatch site; kernels the target declares absent are not counted.
    /// With this flag the same report is logged at `warn` and the server starts.
    #[arg(long, default_value_t = false)]
    pub dangerously_allow_unresolved_kernel_lookups: bool,

    /// Resolve all kernels for the model and exit, reporting any that did not
    /// resolve. Does not start the server. The exit code is the number of
    /// unresolved kernels: 0 means every lookup resolved.
    ///
    /// Config, GPU init, weight load and model construction all run first, so the
    /// lookups are the ones a real serve makes; the scheduler never starts and no
    /// port is bound.
    ///
    /// A POSIX status is 8 bits, so the code is CLAMPED at 255 (256 would read as
    /// 0); when the clamp applies, the true count is printed beside it and carried
    /// in the JSON.
    ///
    /// The exit code ignores `--dangerously-allow-unresolved-kernel-lookups`:
    /// passing both still prints the full list and exits with the count.
    ///
    /// A one-line JSON object (`{"metrale_kernel_check": …}`) is printed on stdout
    /// after the human report.
    #[arg(long, default_value_t = false)]
    pub check_kernels: bool,

    /// Load the model from this path instead of MODEL. It is resolved the same
    /// way: a local directory with `config.json` or a `.gguf`, otherwise a
    /// HuggingFace cache id.
    #[arg(long, value_name = "PATH")]
    pub model_from_path: Option<PathBuf>,

    /// Override model name shown in /v1/models and API responses.
    /// Defaults to the positional MODEL argument, then config.json _name_or_path,
    /// then the model directory's name.
    #[arg(long, alias = "served-model-name", value_name = "NAME")]
    pub model_name: Option<String>,

    /// Pin kernel-target resolution to this compiled target directory name
    /// (e.g. "qwen3.8-27b").
    ///
    /// Normally the target is selected by the checkpoint's `(model_type,
    /// hidden_size)`. When several compiled targets declare that pair, each
    /// target's `match_names` are matched against the model id/path, and startup
    /// refuses when that does not single out one target; this flag is the explicit
    /// answer. The pinned target must still declare the checkpoint's
    /// `(model_type, hidden_size)` or a wildcard for its `model_type`: a pin can
    /// choose between compatible targets, never force an incompatible one.
    #[arg(long, value_name = "TARGET")]
    pub kernel_target: Option<String>,

    /// Override HuggingFace cache directory
    /// (default: $HF_HUB_CACHE, $HF_HOME/hub, or ~/.cache/huggingface/hub).
    #[arg(long, value_name = "DIR")]
    pub cache_dir: Option<PathBuf>,

    /// HTTP port.
    #[arg(long, default_value_t = 8888)]
    pub port: u16,

    /// GPU ordinal.
    #[arg(long, default_value_t = 0)]
    pub gpu_ordinal: usize,

    /// Maximum sequence length.
    #[arg(long, default_value_t = 32768)]
    pub max_seq_len: usize,

    /// KV cache block size (tokens per block).
    #[arg(long, default_value_t = 16)]
    pub block_size: usize,

    /// KV cache dtype (fp8, also spelled fp8_e4m3; bf16; or nvfp4).
    /// Precedence (highest wins): this flag → MODEL.toml
    /// `[behavior].default_kv_dtype` → fp8 (`DEFAULT_KV_CACHE_DTYPE`). An
    /// explicitly passed value always wins, including `fp8` itself.
    ///
    /// The `turbo2`/`turbo3`/`turbo4`/`turbo8` variants (and the asymmetric
    /// `*k_*v` pairs built from them) are experimental: they are not built for
    /// every kernel target, and a target that lacks them fails the kv-cache kernel
    /// preflight at startup.
    #[arg(long, value_parser = super::flag_values::canonical_kv_cache_dtype)]
    pub kv_cache_dtype: Option<String>,

    /// Storage dtype for the GDN decode h-state: `f32` (default), `f16`, or
    /// `f16-pool`.
    ///
    /// `f16` changes the h-state numerics. It requires `--gdn-fused-norm`, because
    /// the FP16 h-state kernels exist only on the fused-norm decode arm. It is
    /// refused together with `--exact-verify`, and together with `--dflash` when
    /// `--dflash-gamma` is above 15.
    ///
    /// `f16-pool` is `f16` plus h pools sized at 2 bytes per element, which halves
    /// their memory. Prefill keeps its FP32 kernels: they run over an FP32 staging
    /// arena of one h blob per slot, widened before a layer's prefill and narrowed
    /// after it.
    ///
    /// Environment fallback: `METRALE_SSM_H_FP16` (any value) selects f16, never
    /// f16-pool, when no GDN flag is given (`--ssm-h-dtype`, `--gdn-fused-norm`,
    /// `--ssm-batched-recurrent on|off`, `--exact-verify`). Any GDN flag hands the
    /// GDN selection to the command line, except that an absent
    /// `--ssm-batched-recurrent` still takes the target default, which
    /// `METRALE_SSM_BATCHED_RECURRENT` can override; a warning names the GDN
    /// variables that are set.
    #[arg(long)]
    pub ssm_h_dtype: Option<String>,

    /// Experimental SSM verify-rollback mode: `snapshot` (default) or `replay`.
    ///
    /// `snapshot`: every verify writes per-token h/conv state snapshots and a
    /// partial accept restores from them. `replay` allocates no per-token h
    /// snapshots and reserves a verify-window input ring instead: a verify
    /// checkpoints the state and records each row's recurrent inputs, and the
    /// commit replays the accepted rows. Implemented for GLM-5.3's KDA layers;
    /// on any other model with SSM layers a replay serve boots, and every
    /// speculative verify step returns an error.
    /// The default is explicit and published on every serve.
    #[arg(long, default_value = "snapshot")]
    pub ssm_rollback_mode: String,

    /// SSM decode-rollback ring depth: `auto` (default) or an explicit slot count
    /// in `0..=8`.
    ///
    /// The ring keeps SSM-state snapshots at boundaries so a watchdog re-steer can
    /// rewind the recurrent state; it reserves `depth x --max-batch-size x the
    /// per-sequence SSM state blob`.
    ///
    /// `auto` starts at depth 8, or 0 under `--speculative`/`--dflash` or with
    /// watchdogs disabled (`METRALE_DISABLE_WATCHDOGS`), and lets preflight shrink
    /// it down `8, 4, 2, 1, 0` until the reserve fits, logging a warning when it
    /// shrinks. Fewer snapshots means fewer re-steer anchors: a rollback that finds
    /// none is declined, and the content-loop and inter-tool prose watchdogs
    /// then end the response instead.
    ///
    /// An explicit `N` pins the depth and skips the fit. `0` disables the ring.
    ///
    /// Environment fallback: `METRALE_SSM_DECODE_RING=1|0` means depth 8 or 0. It
    /// is only consulted when this flag is `auto`.
    #[arg(long, default_value = "auto", value_name = "AUTO_OR_N")]
    pub ssm_decode_ring_slots: String,

    /// Fused GDN output-norm kernel on the decode path (default: off).
    ///
    /// Required by `--ssm-h-dtype f16`: the FP16 h-state kernels live on the
    /// fused-norm arm, and the unfused arm is FP32-only.
    ///
    /// Environment fallback: `METRALE_GDN_FUSED_NORM=1`, on the same terms as
    /// `--ssm-h-dtype`: with no GDN flag on the command line the variable decides;
    /// with any of them an absent `--gdn-fused-norm` means off.
    #[arg(long)]
    pub gdn_fused_norm: bool,

    /// Batched multi-sequence GDN recurrent decode kernel: `auto` (default),
    /// `on` or `off`.
    ///
    /// One strided launch across the batch instead of one per sequence.
    ///
    /// An enum, not a presence flag, because its default is not one value: the
    /// compiled target declares it (`kernels/<hw>/HARDWARE.toml` `[defaults]
    /// ssm_batched_recurrent`: on for `hopper` and `gb10`, off for `b200` and
    /// `b300`), and `METRALE_SSM_BATCHED_RECURRENT` overrides that. `auto` defers
    /// to them; `on`/`off` pin it either way and hand the whole GDN selection to
    /// the command line.
    #[arg(long, default_value = "auto", value_name = "auto|on|off")]
    pub ssm_batched_recurrent: String,

    /// Varlen (ragged) batched prefill, opt-in (default: off).
    ///
    /// Concurrently queued prompts of different lengths are prefilled together,
    /// one forward per wave, so each projection GEMM launches once over the wave's
    /// summed tokens instead of once per request. The scheduler defers chunk 0 of
    /// new requests so they can join a wave, and a wave holds at most
    /// min(`--max-prefill-tokens`, the max batch tokens) tokens. With
    /// `--prefill-codispatch` also set, this path is used.
    ///
    /// Batching changes GEMM row counts, and kernels are selected on row count, so
    /// per-request outputs can differ from the serial path.
    ///
    /// Environment fallback: `METRALE_PREFILL_VARLEN=1` (or `true`) turns it on
    /// when this flag is absent; a given flag wins over it.
    #[arg(long)]
    pub prefill_varlen_batch: bool,

    /// Co-dispatch fresh prompts: when >=2 requests without images are admitted
    /// together with nothing decoding or prefilling, defer their chunk-0 prefill so
    /// they batch into one forward.
    ///
    /// Only with chunked prefill, and not on an expert-parallel model.
    ///
    /// Environment fallback: `METRALE_PREFILL_CODISPATCH=1` (or `true`) turns it on
    /// when this flag is absent.
    #[arg(long)]
    pub prefill_codispatch: bool,

    /// Precision of each linear layer (default: declared).
    ///
    /// `declared` runs each layer at the precision the checkpoint's `quantization_config`
    /// declares, for the weights and the input activations: W4A4 where it declares FP4
    /// activations (the dense Qwen3.8-27B MLP of layers 0-55, at every decode row count and
    /// in prefill), W4A16 where it declares NVFP4 weights only (no FP4 MMQ prefill there),
    /// and the checkpoint's own lm_head format under `--lm-head-dtype default`, except a
    /// declared FP8 head, which waits for the batched FP8 head kernel (the engine's default
    /// head runs until then; `--lm-head-dtype fp8` serves it now). Gaps that
    /// remain: FP8-declared layers run 16-bit activations until the W8A8 decode kernels land
    /// (above declared); the dense loader still requantizes per-channel FP8 attention, GDN and
    /// MLP projections to NVFP4 (below declared on the weights, 16-bit activations); attention
    /// and GDN projections that declare FP4 activations run W4A4 on the batched small-M arms
    /// only, and 16-bit activations at one to three rows and on the GDN tile GEMM (above
    /// declared); `METRALE_*` diagnostic levers that force FP4 activations still do.
    ///
    /// `nvfp4` is the engine before this flag: FP8-declared projections requantized to NVFP4
    /// at load, 16-bit decode activations unless `--w4a4-downcast`, FP4 MMQ prefill on every
    /// NVFP4 FFN. Every certified recipe pins it. Gate records disclose the tier. No
    /// environment fallback.
    ///
    /// `--expert-quantization` governs routed MoE experts on its own: its `fp8` default is
    /// the declared precision of an FP8 checkpoint's experts, and its NVFP4 tiers go below it
    /// under either value of this flag.
    ///
    /// Measured cost of `declared` against `nvfp4` on GB10 (dgx1, one binary). On
    /// unsloth/Qwen3.8-27B-NVFP4, `declared` runs W8A8 on the FP8-declared layers and the
    /// checkpoint's FP8 head (W8A8, batched): decode-floor recipe 24.2 tok/s at 1.52-1.69 J/tok
    /// against 26.8 tok/s at 1.93-2.15 J/tok; throughput recipe C1/C16/C128 20.4-20.7 /
    /// 192.0-192.8 / 449.2-451.0 tok/s against 24.7/212.9/447.7. On Qwen3.6-35B-A3B-FP8,
    /// `declared` runs W8A8 on the routed and shared experts; its block-scaled attention and
    /// GDN projections declare FP8 activations but decode W8A16 (above declared) until that
    /// W8A8 path is re-validated. Measured 2026-09-29 on dgx2 (MoE ladder harness, ISL 128 /
    /// OSL 1024, 2 reps): C1/16/64/128 80.5/353.2/617.5/751.3 tok/s at 0.452/0.126/0.090/
    /// 0.0738 J/tok, against vLLM+MTP 52.9/330.0/563.6/672.9 at 0.641/0.142/0.092/0.0783; with
    /// the attention/GDN W8A8 on as well it read 83.0/360.1/630.4/747.2 at 0.439/0.119/0.084/
    /// 0.0741. BFCL echolp full draw with both on 84.56/86.40 and 85.56/86.77 against nvfp4
    /// 85.16/86.15; agentic-webserver 2/2.
    #[arg(long, value_enum, default_value_t = WeightQuantizationArg(metrale_config::WeightQuantization::Declared))]
    pub weight_quantization: WeightQuantizationArg,

    /// Downcast activations to NVFP4 (W4A4) on the small-M projection paths
    /// (default: false). Only with `--weight-quantization nvfp4`.
    ///
    /// On, the projection sites that call `nvfp4_proj_small_m` (GDN qkvz/out_proj,
    /// attention q/k/v/o, dense-FFN gate/up/down) run up to 32 rows on the FP4
    /// block-scale tensor-core MMA (`w4a4_gemv_mx`), which reads the checkpoint's
    /// NVFP4 weights with no dequant; the activations are quantized per row to
    /// NVFP4, whatever the checkpoint declares. It is a numerics change, so it is off
    /// unless a recipe asks for it. Under `--weight-quantization declared` it is refused:
    /// the checkpoint decides each layer's activations there. No environment fallback.
    /// The lm_head is not affected.
    #[arg(long, default_value_t = false)]
    pub w4a4_downcast: bool,

    /// Widen `--w4a4-downcast` from 1..=32 to 1..=64 rows (default: false).
    ///
    /// Projections of 33 to 64 rows then take the same W4A4 FP4 block-scale GEMV
    /// (the `w4a4_gemv_mx64` entries), with the per-row math of the 1..=32-row
    /// path. A numerics change at those widths, hence opt-in. Requires
    /// `--w4a4-downcast`; without it a warning is logged and it has no effect.
    #[arg(long, default_value_t = false)]
    pub w4a4_downcast_wide: bool,

    /// Precision of a native-FP8 checkpoint's routed MoE experts in decode (default: fp8).
    ///
    /// `fp8` decodes the checkpoint's FP8 experts: most stable, still fast. The NVFP4 tiers add a
    /// 4-bit NVFP4 copy of routed-expert projections at load (E2M1 with E4M3 block scales of 16,
    /// each scale chosen by squared error) and run every MoE decode of 1 to 64 rows through
    /// grouped kernels whose output for a row does not depend on how many rows share the
    /// launch; the shared expert and prefill keep the FP8 weights under both. `nvfp4-gate-up`
    /// lowers the routed gate and up projections only (the stable speed lever); `nvfp4` lowers
    /// every routed projection (dangerous but fast). They lower expert precision from FP8 to 4
    /// bits, so the model's answers change. No environment fallback.
    ///
    /// Measured on GB10, Qwen3.6-35B-A3B-FP8, canonical tiers: BFCL echolp full draw
    /// (N=1004) overall/normalized fp8 84.96/86.03, nvfp4-gate-up 84.86/85.33, nvfp4
    /// 85.46/86.57; agentic-webserver fp8 pass (535-546 s summed wall), nvfp4-gate-up pass 3/3
    /// (504-533 s), nvfp4 fails the gate's 700 s ceiling (168 turns, 774 s); C16 tok/s and J/tok
    /// with `--mtp-gate force`: fp8 315.5/0.227, nvfp4-gate-up 359.6/0.211, nvfp4 383.1/0.201.
    /// The NVFP4 copies stay resident beside the FP8 experts, so the KV cache shrinks: weights
    /// before KV fp8 38.4 GB, nvfp4-gate-up 49.6 GB, nvfp4 55.2 GB.
    #[arg(long, value_enum, default_value_t = ExpertQuantizationArg(ExpertQuantization::Fp8))]
    pub expert_quantization: ExpertQuantizationArg,

    /// Sequential-decode-exact GDN/SSM verify chain, opt-in (default: off).
    ///
    /// With it, MTP verify runs, per token, the GDN/SSM kernel chain sequential
    /// decode runs, instead of the WY-chunkwise and fused BF16-conv arms.
    ///
    /// It makes only the GDN/SSM verify chain exact. The FFN and attention
    /// projections still pick their kernels by row count, so speculative output is
    /// not guaranteed to match non-speculative output token for token.
    ///
    /// Refused beside `--ssm-h-dtype f16`: the exact arm's kernels are FP32
    /// readers and must never read the FP16 h-state pool.
    ///
    /// No environment variable. Like the other GDN flags, giving it hands the
    /// GDN selection to the command line; an absent `--ssm-batched-recurrent`
    /// still takes the target default.
    #[arg(long)]
    pub exact_verify: bool,

    /// Turn off mid-chunk SSM tail capture on the prefill path (on by default).
    ///
    /// While it is on, the scheduler does not end a prefill chunk at the SSM tail
    /// boundary, and prefill plans an in-pass capture of the GDN recurrent and
    /// conv state there instead (`prepare_midchunk_capture`).
    ///
    /// Environment fallback: `METRALE_SSM_TAIL_MIDCHUNK=0` disables it when this
    /// flag is absent. An absent flag publishes nothing, so the variable stays
    /// reachable.
    #[arg(long)]
    pub no_ssm_tail_midchunk: bool,

    /// Keep the row-count tiers on an FP8 MoE checkpoint (canonical tiers are on by
    /// default there).
    ///
    /// By default an FP8 MoE checkpoint gives every row the same summation order at
    /// every batch width: the W8A16 projections take the tensor-core tile family,
    /// the NVFP4 LM head its tile GEMM and the FP8 MoE the grouped kernels with
    /// the per-row router, so greedy output does not depend on how many rows share
    /// a launch. With this flag those ops pick kernels by row count, as other
    /// checkpoints do. `METRALE_CANONICAL_TIERS` (presence) turns canonical tiers
    /// on for any checkpoint; this flag wins over it.
    #[arg(long)]
    pub no_canonical_tiers: bool,

    /// MTP throughput gate: `auto` (default) or `force`.
    ///
    /// `auto` arms the gate, which measures delivered throughput with and without
    /// MTP and switches to whichever is faster. `force` disarms it, so verify steps
    /// keep running even where the gate would switch them off; the server logs it
    /// as a diagnostic setting. To run without speculation at all, omit
    /// `--speculative`.
    ///
    /// Environment fallback: `METRALE_MTP_GATE_FORCE=1` selects `force` when this
    /// flag is absent. There is no clap default, so the variable stays reachable.
    #[arg(long)]
    pub mtp_gate: Option<String>,

    /// LM-head precision: `default` (the clap default: no override, the model
    /// config decides), `bf16` (final vocab projection in BF16), `nvfp4` (force the
    /// model's NVFP4-packed lm_head), or `fp8` (an FP8 E4M3 lm_head decoded with
    /// `w8a16_gemv`: the checkpoint's own FP8 head when it ships one, else the
    /// lm_head quantized with per-row scales when the model is built).
    /// Against BF16, `fp8` reads about half the lm_head bytes per token and `nvfp4`
    /// about a quarter.
    ///
    /// Below BF16 the final vocab projection can flip low-margin argmax choices.
    /// Check long generations per model before enabling `nvfp4` or `fp8`.
    #[arg(long, default_value = "default")]
    pub lm_head_dtype: String,

    /// Boundary attention layers to keep at BF16 KV cache precision (first N + last N);
    /// the other layers use --kv-cache-dtype.
    /// Accepts: number, "auto" (=2, recommended), "max"/"all" (all BF16).
    /// Default: 0, no BF16 boundary layers, except that a turbo KV dtype gets an
    /// automatic count.
    #[arg(long, default_value = "0")]
    pub kv_high_precision_layers: String,

    /// Fraction of total GPU memory this process may consume, in (0.0, 1.0].
    /// Weights, buffers, KV cache and reserves are sized against total memory
    /// times this value.
    #[arg(long, default_value_t = 0.90)]
    pub gpu_memory_utilization: f64,

    /// Capacity of the queue that carries new requests to the scheduler.
    #[arg(long, default_value_t = 128)]
    pub max_num_seqs: usize,

    // 2026-09-26: The remaining flags, in declaration order: `ServeSchedulingArgs`,
    // which flattens `ServeServiceArgs` last. `Deref`/`DerefMut` below reach their
    // fields as `args.<field>`.
    #[command(flatten)]
    pub(crate) scheduling: scheduling::ServeSchedulingArgs,
}

impl std::ops::Deref for ServeArgs {
    type Target = scheduling::ServeSchedulingArgs;

    fn deref(&self) -> &Self::Target {
        &self.scheduling
    }
}

impl std::ops::DerefMut for ServeArgs {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.scheduling
    }
}

impl ServeArgs {
    /// 2026-09-28: `--weight-quantization` with its `--w4a4-downcast` lever, as published to
    /// the kernel crates. Errors for the lever under `declared`; `validate_serve_args`
    /// refuses that command line first.
    pub fn weight_quant_tier(&self) -> anyhow::Result<metrale_config::WeightQuantTier> {
        metrale_config::WeightQuantTier::new(
            self.weight_quantization.0,
            metrale_config::W4a4Downcast::from_flags(self.w4a4_downcast, self.w4a4_downcast_wide),
        )
    }

    /// 2026-09-30: The γ this DFlash serve runs (`resolve_dflash_gamma` of the flag and the
    /// drafter), valid only after `serve_phases::apply_dflash_gamma`; panics when read
    /// earlier, as [`Self::resolved_num_drafts`] does.
    pub fn serve_dflash_gamma(&self) -> usize {
        self.dflash_gamma_resolved
            .expect("dflash gamma read before apply_dflash_gamma resolved it")
    }

    /// 2026-09-26: The draft count, valid only after
    /// `serve_phases::apply_model_default_num_drafts` has resolved the
    /// CLI → MODEL.toml → engine-default precedence into `num_drafts`; `serve_load`
    /// calls it before the preflight reserve. Panics when read earlier. Readers that
    /// run before it (argument validation, the dashboard logo) match on the `Option`.
    pub fn resolved_num_drafts(&self) -> usize {
        self.num_drafts
            .expect("num_drafts read before apply_model_default_num_drafts resolved it")
    }
}

/// 2026-09-26: Value parser for `--lora-adapter` and `--lora-stageable-disk`,
/// `NAME=PATH_OR_HF_ID`: split at the first `=`, both parts non-empty.
fn parse_lora_adapter_spec(s: &str) -> Result<(String, String), String> {
    let (name, spec) = s
        .split_once('=')
        .ok_or_else(|| format!("--lora-adapter must be NAME=PATH_OR_HF_ID, got '{s}'"))?;
    if name.is_empty() || spec.is_empty() {
        return Err(format!("--lora-adapter: empty name or path in '{s}'"));
    }
    Ok((name.to_string(), spec.to_string()))
}

/// 2026-09-26: Value parser for `--lora-stageable NAME=PEER_ID=DIR`: split at the first
/// two `=` into three parts, all non-empty. DIR keeps any further `=`. DIR is required
/// because the peft scaling of a promoted adapter is read from its
/// `adapter_config.json` at startup.
fn parse_lora_stageable_spec(s: &str) -> Result<(String, String, String), String> {
    let mut parts = s.splitn(3, '=');
    let name = parts.next().unwrap_or("");
    let peer_id = parts.next().unwrap_or("");
    let dir = parts.next().unwrap_or("");
    if name.is_empty() || peer_id.is_empty() || dir.is_empty() {
        return Err(format!(
            "--lora-stageable must be NAME=PEER_ID=DIR (all three non-empty), got '{s}'"
        ));
    }
    Ok((name.to_string(), peer_id.to_string(), dir.to_string()))
}