Skip to main content

auracle_session/
farm.rs

1//! The stateless render farm: an **indexed** pool-draw stream, and the
2//! off-engine unit of work it hands out.
3//!
4//! ## Why the draw stream is indexed
5//!
6//! The pool used to be filled from one advancing [`rand::Rng`]: draw, render,
7//! dedupe, push, repeat. That makes the *n*-th tree a function of how many
8//! draws happened before it, which is fine while one loop owns the RNG and
9//! fatal the moment renders are farmed out to workers — a lost job, a
10//! speculative draw past the stop point, or simply a different number of
11//! renders in flight all shift the stream.
12//!
13//! Here draw *i* is instead sampled from a fresh
14//! `StdRng::seed_from_u64(draw_seed(fill_seed, i))`. Three properties follow,
15//! and together they are what makes the farm's determinism structural rather
16//! than argued:
17//!
18//! 1. **Re-issue is stateless.** A worker that dies mid-job costs nothing but
19//!    the render: the job is `(fill_seed, i)`, so any other worker can redo it
20//!    with no retained state on either side.
21//! 2. **Over-issue is free.** Work dispatched past the point the pool fills is
22//!    simply discarded; it cannot desynchronize a stream it never advanced.
23//! 3. **Width is invisible.** The pool is the fold of indices `0, 1, 2, …` in
24//!    order — dedupe and vetting applied at absorption time — so the result
25//!    depends on `(fill_seed, pool_size)` alone, at any farm width including
26//!    zero.
27//!
28//! The consequence, stated plainly: **fixed-seed pools re-baselined** when this
29//! landed. That is invisible to the app (the browser seeds from
30//! `Math.random()`, and saved sessions store trees rather than seeds) but any
31//! test asserting exact pool contents from a fixed seed had to move with it.
32
33use std::sync::Arc;
34
35use auracle_features::{
36    featurize_memo, Audition, CachedFeatures, FeaturizeError, PhraseSpec, RenderMemo,
37};
38use auracle_grammar::PatchTree;
39use serde::{Deserialize, Serialize};
40
41/// splitmix64 over `(base, index)` — the pool draw stream's index function.
42///
43/// splitmix64 rather than "seed the RNG with `base ^ index`" because adjacent
44/// seeds must produce *unrelated* streams: `StdRng` is ChaCha12, which would
45/// tolerate the naive version, but the whole point of this function is that
46/// nothing downstream has to know that. splitmix64 decorrelates by
47/// construction and is the mixer `SeedableRng::seed_from_u64` itself uses.
48#[inline]
49pub fn draw_seed(base: u64, index: u64) -> u64 {
50    let mut z = base
51        .wrapping_add(index.wrapping_mul(0x9E37_79B9_7F4A_7C15))
52        .wrapping_add(0x9E37_79B9_7F4A_7C15);
53    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
54    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
55    z ^ (z >> 31)
56}
57
58/// A candidate whose render, vetting and featurization already happened —
59/// in a farm worker, or on the way back from transport.
60///
61/// It carries exactly what [`auracle_features::featurize_memo`] produces,
62/// minus the work: the term, its [`CachedFeatures`] (content key, raw φ, vet
63/// report, onsets, length), and optionally the audition buffer. The engine
64/// folds one of these in with [`crate::Engine::absorb_prior`] or
65/// [`crate::Engine::absorb_bank_entry`], which apply the *same* dedupe,
66/// standardization and admission rules the in-process path applies.
67///
68/// A **failed** vet has no `PreFeaturized`: it is `None` at the absorb site,
69/// which is how a quarantined draw stays a normal outcome rather than an
70/// error.
71#[derive(Clone, Debug)]
72pub struct PreFeaturized {
73    /// The term.
74    pub tree: PatchTree,
75    /// Everything the featurization produced except the samples.
76    pub cached: CachedFeatures,
77    /// The audition buffer, when the producer was asked for one and it
78    /// survived transport. Absent is never a failure signal — φ is the
79    /// product, audio is the option.
80    pub audition: Option<Arc<Audition>>,
81}
82
83impl PreFeaturized {
84    /// Render, vet and featurize one term **without an [`crate::Engine`]** —
85    /// the farm worker's entire job.
86    ///
87    /// Deliberately unmemoized: a farm worker is a pure function of its
88    /// arguments and holds no state between jobs, which is what lets the
89    /// engine treat any worker as interchangeable with any other (and with
90    /// itself).
91    pub fn render(
92        tree: PatchTree,
93        spec: &PhraseSpec,
94        want_audio: bool,
95    ) -> Result<Self, FeaturizeError> {
96        let (cached, audition) = featurize_memo(&tree, spec, &RenderMemo::disabled(), want_audio)?;
97        Ok(Self {
98            tree,
99            cached,
100            audition,
101        })
102    }
103}
104
105/// One unit of farm work: an index of the draw stream and the term it names.
106///
107/// `dup` is a courtesy, not a decision: the term already sits in the pool, so
108/// rendering it would be wasted. The absorbing engine re-checks against the
109/// pool as it actually stands at index `index`, so a stale or missing `dup`
110/// costs a render and changes nothing.
111#[derive(Clone, Debug, Serialize, Deserialize)]
112pub struct Draw {
113    /// Index in the pool draw stream.
114    #[serde(rename = "i")]
115    pub index: u64,
116    /// The term at that index.
117    pub tree: PatchTree,
118    /// Already in the pool when this was issued — skippable.
119    pub dup: bool,
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    /// Indexing must decorrelate: neighbouring indices are unrelated seeds,
127    /// and the same `(base, index)` always names the same one.
128    #[test]
129    fn draw_seed_is_pure_and_decorrelated() {
130        assert_eq!(draw_seed(7, 3), draw_seed(7, 3));
131        assert_ne!(draw_seed(7, 3), draw_seed(7, 4));
132        assert_ne!(draw_seed(7, 3), draw_seed(8, 3));
133        // Adjacent indices must not differ in a handful of bits.
134        let a = draw_seed(0xC0FFEE, 0);
135        let b = draw_seed(0xC0FFEE, 1);
136        assert!(
137            (a ^ b).count_ones() > 8,
138            "adjacent draw seeds barely differ: {a:x} vs {b:x}"
139        );
140    }
141}