Skip to main content

eredu_runtime/execution_control/
sampling.rs

1//! Prospective sampling policy validated before native RNG preparation.
2use eredu_core::{
3    ModelRuntime, TextContinuationBoundary, TextGenerationBackend, TokenFilterController,
4};
5use serde::{Deserialize, Serialize};
6
7/// Supported changes to future sampling only. Omitting `reseed` retains the exact
8/// inherited RNG stream. Sampler strategy, adaptive counters, penalties and token
9/// history remain unchanged; incompatible strategy transitions are not exposed.
10#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
11pub struct SamplingOverride {
12    /// New temperature; zero selects greedy standard sampling.
13    pub temperature: Option<f32>,
14    /// Explicit new native RNG seed. Does not reset adaptive or penalty history.
15    pub reseed: Option<u64>,
16}
17
18/// Native facts consumed by shared validation; contains no mutable native handle.
19#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
20pub struct SamplingStateFacts {
21    /// Current effective temperature.
22    pub temperature: f32,
23    /// The retained sampler requires strictly positive temperature (Mirostat).
24    pub requires_positive_temperature: bool,
25    /// An exact resumable RNG stream exists, including while temporarily greedy.
26    pub has_rng: bool,
27}
28
29/// Validated native action. Only the shared policy constructs this value.
30#[derive(Debug, Clone, Copy)]
31pub struct ValidatedSamplingOverride {
32    temperature: f32,
33    reseed: Option<u64>,
34}
35impl ValidatedSamplingOverride {
36    /// Effective future temperature.
37    pub fn temperature(self) -> f32 {
38        self.temperature
39    }
40    /// Explicit new seed, absent when the existing RNG must remain unchanged.
41    pub fn reseed(self) -> Option<u64> {
42        self.reseed
43    }
44}
45
46/// Minimal native mechanism; ordinary sampling and snapshot state keep their
47/// existing owners. Errors must preserve the previous logical sampling state.
48pub trait TextSamplingControlBackend: TextGenerationBackend {
49    /// Side-effect-free facts for the installed sampling state.
50    fn sampling_control_facts(state: &Self::TextGenerationState) -> SamplingStateFacts;
51    /// Prepares any replacement key and proves completion through the existing
52    /// recovery owner before installing temperature/key together. Consumes no RNG
53    /// draw, submits no model prediction and leaves all sampler history intact.
54    fn install_sampling_override(
55        runtime: &mut ModelRuntime<Self>,
56        state: &mut Self::TextGenerationState,
57        request: ValidatedSamplingOverride,
58    ) -> Result<(), Self::Error>;
59}
60
61/// Invalid policy is rejected before any native operation or state change.
62#[derive(Debug, thiserror::Error)]
63pub enum SamplingOverrideError<E: std::error::Error + 'static> {
64    /// Invalid or incompatible temperature/RNG policy.
65    #[error("invalid sampling override: {0}")]
66    Invalid(&'static str),
67    /// Native replacement preparation failed, preserving the prior logical state.
68    #[error("native sampling override failed: {0}")]
69    Backend(#[source] E),
70}
71
72fn validate<E: std::error::Error + 'static>(
73    facts: SamplingStateFacts,
74    request: SamplingOverride,
75) -> Result<ValidatedSamplingOverride, SamplingOverrideError<E>> {
76    let temperature = request.temperature.unwrap_or(facts.temperature);
77    if !temperature.is_finite() || temperature < 0.0 {
78        return Err(SamplingOverrideError::Invalid(
79            "temperature must be finite and nonnegative",
80        ));
81    }
82    if facts.requires_positive_temperature && temperature == 0.0 {
83        return Err(SamplingOverrideError::Invalid(
84            "adaptive sampling requires positive temperature",
85        ));
86    }
87    if temperature > 0.0 && !facts.has_rng && request.reseed.is_none() {
88        return Err(SamplingOverrideError::Invalid(
89            "no inherited RNG exists; an explicit seed is required",
90        ));
91    }
92    Ok(ValidatedSamplingOverride {
93        temperature,
94        reseed: request.reseed,
95    })
96}
97
98/// Validates and applies one future sampling change at a proven native and record
99/// boundary. Stochastic-to-greedy retains RNG; switching back resumes that stream.
100/// A run created without an RNG needs an explicit seed to become stochastic.
101pub fn apply_sampling_override<B: TextSamplingControlBackend, C: TokenFilterController>(
102    boundary: &mut TextContinuationBoundary<'_, '_, B, C>,
103    request: SamplingOverride,
104) -> Result<SamplingStateFacts, SamplingOverrideError<B::Error>> {
105    let (runtime, state, _) = boundary.mechanism_parts();
106    apply_prepared_sampling_override(runtime, state, request)
107}
108
109/// Validates a prospective change on detached, independently prepared generation
110/// state. Used during branch construction under its copy reservation, while the
111/// loaded runtime is quiescent. Native installation must preserve the installed
112/// model state and cooperate with its existing submission/completion owner.
113pub fn apply_prepared_sampling_override<B: TextSamplingControlBackend>(
114    runtime: &mut ModelRuntime<B>,
115    state: &mut B::TextGenerationState,
116    request: SamplingOverride,
117) -> Result<SamplingStateFacts, SamplingOverrideError<B::Error>> {
118    if let eredu_core::execution_control::ControlSupport::Unsupported { .. } =
119        B::text_sampling_control_support(runtime)
120    {
121        return Err(SamplingOverrideError::Invalid(
122            "loaded execution does not support sampling overrides",
123        ));
124    }
125    let action = validate(B::sampling_control_facts(state), request)?;
126    B::install_sampling_override(runtime, state, action).map_err(SamplingOverrideError::Backend)?;
127    Ok(B::sampling_control_facts(state))
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    fn check(
134        facts: SamplingStateFacts,
135        request: SamplingOverride,
136    ) -> Result<ValidatedSamplingOverride, SamplingOverrideError<std::io::Error>> {
137        validate(facts, request)
138    }
139    #[test]
140    fn temperature_changes_preserve_rng_and_adaptation_unless_reseeding_is_explicit() {
141        let initial = SamplingStateFacts {
142            temperature: 0.0,
143            requires_positive_temperature: false,
144            has_rng: false,
145        };
146        let stochastic = SamplingOverride {
147            temperature: Some(0.7),
148            reseed: None,
149        };
150        assert!(check(initial, stochastic).is_err());
151        let seeded = check(
152            initial,
153            SamplingOverride {
154                reseed: Some(42),
155                ..stochastic
156            },
157        )
158        .unwrap();
159        assert_eq!(seeded.reseed(), Some(42));
160        assert_eq!(seeded.temperature(), 0.7);
161        let retained = SamplingStateFacts {
162            has_rng: true,
163            ..initial
164        };
165        assert_eq!(check(retained, stochastic).unwrap().reseed(), None);
166        for invalid in [f32::NAN, f32::INFINITY, -1.0] {
167            assert!(check(
168                retained,
169                SamplingOverride {
170                    temperature: Some(invalid),
171                    reseed: None
172                }
173            )
174            .is_err());
175        }
176        let adaptive = SamplingStateFacts {
177            temperature: 0.8,
178            requires_positive_temperature: true,
179            has_rng: true,
180        };
181        assert!(check(
182            adaptive,
183            SamplingOverride {
184                temperature: Some(0.0),
185                reseed: None
186            }
187        )
188        .is_err());
189        assert_eq!(
190            check(adaptive, SamplingOverride::default())
191                .unwrap()
192                .temperature(),
193            0.8
194        );
195    }
196}