swadhinbiswas WIP save-point: production hardening, H3 removal, fmt gate, and accumulated phase work · 7/15/2026
2080d40
181 lines · 6.4 KB
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
//! Model trait, sampling parameters, generated-token stream items, and the
//! CPU-runnable [`MockModel`] used for tests and offline demos.

/// Stopping / sampling configuration for a generation request.
///
/// Mirrors the knobs vLLM/SGLang expose; the real forward pass (Phase 42) will
/// honor `temperature`/`top_p`/`top_k` when sampling logits.
#[derive(Debug, Clone)]
pub struct SamplingParams {
    /// Maximum number of *new* tokens to generate.
    pub max_tokens: usize,
    /// Sampling temperature (0.0 = greedy). Real models only; MockModel ignores it.
    pub temperature: f32,
    /// Nucleus sampling cutoff.
    pub top_p: f32,
    /// Top-k cutoff (0 = disabled).
    pub top_k: usize,
    /// Token ids that, when produced, end generation with `FinishReason::Stop`.
    pub stop_tokens: Vec<u32>,
    /// Substrings that, when produced, end generation (real tokenizers only).
    pub stop_sequences: Vec<String>,
}

impl Default for SamplingParams {
    fn default() -> Self {
        Self {
            max_tokens: 256,
            temperature: 1.0,
            top_p: 1.0,
            top_k: 0,
            stop_tokens: Vec::new(),
            stop_sequences: Vec::new(),
        }
    }
}

/// A single streamed token produced during generation.
#[derive(Debug, Clone)]
pub struct GeneratedToken {
    /// Vocabulary id of the token.
    pub id: u32,
    /// Decoded text for the token (may be empty/partial for sub-word pieces).
    pub text: String,
    /// Log-probability of the token (0.0 for [`MockModel`]).
    pub logprob: f32,
    /// Set on the final token of the stream.
    pub finish_reason: Option<FinishReason>,
}

/// Why a generation stream ended.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FinishReason {
    /// Hit `max_tokens`.
    Length,
    /// Produced a stop token / sequence.
    Stop,
    /// Downstream consumer stopped reading (e.g. client disconnected).
    Cancelled,
}

/// Errors produced by the inference engine and models.
#[derive(Debug, thiserror::Error)]
pub enum ModelError {
    #[error("model '{0}' not found in registry")]
    NotFound(String),
    #[error("feature '{0}' is required for this operation (enable it in the crate features)")]
    FeatureRequired(&'static str),
    #[error("real model loading is not implemented yet (lands with the KV-cache in Phase 42)")]
    NotImplemented,
    #[error("generation failed: {0}")]
    Generation(String),
    #[error(transparent)]
    Io(#[from] std::io::Error),
}

/// A loadable / runnable model behind the engine.
///
/// Implementors must be `Send + Sync` so they can be shared across the registry
/// and driven from a generation thread.
pub trait Model: Send + Sync {
    /// Size of the model vocabulary.
    fn vocab_size(&self) -> usize;

    /// Run generation for `prompt` (token ids), streaming each produced token to
    /// `sink`. Return `false` from `sink` to abort early (e.g. client gone).
    /// Returns the reason the stream ended.
    fn generate(
        &self,
        prompt: &[u32],
        params: &SamplingParams,
        sink: &dyn Fn(GeneratedToken) -> bool,
    ) -> Result<FinishReason, ModelError>;

    /// Return the unnormalized next-token logits for `context` (the raw score
    /// vector over the vocabulary at the position after `context`).
    ///
    /// This is the single-step primitive required for speculative decoding: a
    /// draft model proposes tokens and the target model scores them to accept
    /// or reject. The default implementation derives a degenerate distribution
    /// from [`Model::generate`]'s behavior, but real models should compute true
    /// logits from a single forward pass.
    fn forward_logits(&self, context: &[u32]) -> Result<Vec<f32>, ModelError>;
}

/// Deterministic, weight-free model for tests, demos, and CI on CPU-only boxes.
///
/// It echoes the last prompt token and then walks the vocabulary one id at a
/// time (`(prev + 1) % vocab`), decoding each id to a lowercase letter so the
/// stream is human-readable. It is NOT a real LLM — it exists to exercise the
/// full engine/streaming/registry path end-to-end.
pub struct MockModel {
    vocab_size: usize,
}

impl MockModel {
    /// Create a mock model with the given vocabulary size (clamped to >= 1).
    pub fn new(vocab_size: usize) -> Self {
        Self { vocab_size: vocab_size.max(1) }
    }

    /// Trivial tokenization: each byte of `text` becomes a token id.
    pub fn tokenize(text: &str) -> Vec<u32> {
        text.bytes().map(|b| b as u32).collect()
    }

    /// Trivial detokenization: low byte of each id -> char (lossy for >255).
    pub fn detokenize(ids: &[u32]) -> String {
        ids.iter().map(|&id| (id as u8) as char).collect()
    }
}

impl Model for MockModel {
    fn vocab_size(&self) -> usize {
        self.vocab_size
    }

    fn forward_logits(&self, context: &[u32]) -> Result<Vec<f32>, ModelError> {
        let vocab = self.vocab_size;
        let prev = context.last().copied().unwrap_or(0) % vocab as u32;
        let next = (prev.wrapping_add(1)) % vocab as u32;
        // Degenerate-but-smooth distribution: a sharp peak at the next token so
        // greedy decoding reproduces `generate`'s `(prev + 1) % vocab` rule,
        // with a small uniform tail so temperature/top_p sampling still works.
        let mut logits = vec![-5.0f32; vocab];
        logits[next as usize] = 5.0;
        let r = (prev.wrapping_add(3)) % vocab as u32;
        logits[r as usize] = 0.0;
        Ok(logits)
    }

    fn generate(
        &self,
        prompt: &[u32],
        params: &SamplingParams,
        sink: &dyn Fn(GeneratedToken) -> bool,
    ) -> Result<FinishReason, ModelError> {
        let mut prev = prompt.last().copied().unwrap_or(0);
        for _ in 0..params.max_tokens {
            let next = (prev.wrapping_add(1)) % self.vocab_size as u32;
            let text = decode_mock(next);
            let stop = params.stop_tokens.contains(&next);
            let finish_reason = if stop { Some(FinishReason::Stop) } else { None };
            let token = GeneratedToken { id: next, text, logprob: 0.0, finish_reason };
            if !sink(token) {
                return Ok(FinishReason::Cancelled);
            }
            if stop {
                return Ok(FinishReason::Stop);
            }
            prev = next;
        }
        Ok(FinishReason::Length)
    }
}

/// Map a token id to a single lowercase letter so mock streams are readable.
fn decode_mock(id: u32) -> String {
    let c = (b'a' + (id % 26) as u8) as char;
    c.to_string()
}