T0 + T1 + T2: engine redesign through new API surface #1

Merged
logaritmisk merged 45 commits from t2-new-api-surface into main 2026-04-24 11:20:04 +00:00
10 changed files with 36 additions and 11 deletions
Showing only changes of commit cdfd75f846 - Show all commits

View File

@@ -41,3 +41,22 @@ Gaussian::pi_tau_combined 219.13 ps (1.00×)
# - Pass a within_priors output buffer through the arena
# - Make Game::likelihoods write into an arena slice rather than allocating
# These land in T1 (factor graph) when we redesign Game's internals.
# After T1 (2026-04-24, same hardware)
Batch::iteration 27.023 µs (1.27× vs T0 21.253 µs; regression observed)
Gaussian::add 236.24 ps (1.08× unchanged)
Gaussian::sub 236.82 ps (1.08× unchanged)
Gaussian::mul 236.58 ps (1.08× unchanged — nat-param storage)
Gaussian::div 236.65 ps (1.08× unchanged)
Gaussian::pi 279.68 ps (1.06× unchanged)
Gaussian::tau 277.55 ps (1.05× unchanged)
Gaussian::pi_tau_combined 234.91 ps (1.07× unchanged)
# Notes:
# - Regression in Batch::iteration (27.0 µs vs target ≤ 21.5 µs): T1 factor-graph
# refactor added new machinery (Factor trait, VarStore, within-game scheduler)
# but these are not yet integrated into the hot path. Game::posteriors still
# uses the old inference. Integration deferred to T2.
# - Gaussian operations show expected minor fluctuations; no regression vs T0.
# - Acceptance: T1 lands infrastructure without breaking existing inference.

View File

@@ -2,7 +2,6 @@ use std::collections::HashMap;
use crate::{
Index, N_INF,
agent::Agent,
arena::ScratchArena,
drift::Drift,
game::Game,
@@ -305,8 +304,7 @@ impl Batch {
if online || forward {
self.events
.iter()
.enumerate()
.map(|(_, event)| {
.map(|event| {
Game::new(
event.within_priors(online, forward, &self.skills, agents),
&event.outputs(),

View File

@@ -20,6 +20,7 @@ pub(crate) struct VarStore {
}
impl VarStore {
#[allow(dead_code)]
pub(crate) fn new() -> Self {
Self::default()
}
@@ -28,6 +29,7 @@ impl VarStore {
self.marginals.clear();
}
#[allow(dead_code)]
pub(crate) fn len(&self) -> usize {
self.marginals.len()
}
@@ -60,6 +62,7 @@ pub(crate) trait Factor {
fn propagate(&mut self, vars: &mut VarStore) -> (f64, f64);
/// Optional log-evidence contribution. Default 0.0 (no contribution).
#[allow(dead_code)]
fn log_evidence(&self, _vars: &VarStore) -> f64 {
0.0
}
@@ -70,6 +73,7 @@ pub(crate) trait Factor {
/// Using an enum instead of `Box<dyn Factor>` keeps factor data inline and
/// avoids virtual-call overhead in the hot inference loop.
#[derive(Debug)]
#[allow(dead_code)]
pub(crate) enum BuiltinFactor {
TeamSum(team_sum::TeamSumFactor),
RankDiff(rank_diff::RankDiffFactor),

View File

@@ -13,6 +13,7 @@ use crate::factor::{Factor, VarId, VarStore};
/// effectively replaced on each propagation. The TruncFactor on the same diff
/// var holds the EP-divide message that produces the cavity.
#[derive(Debug)]
#[allow(dead_code)]
pub(crate) struct RankDiffFactor {
pub(crate) team_a: VarId,
pub(crate) team_b: VarId,

View File

@@ -10,6 +10,7 @@ use crate::{
/// already with beta² noise added via `Player::performance()`). The factor
/// runs once per game and writes the weighted sum to the output var.
#[derive(Debug)]
#[allow(dead_code)]
pub(crate) struct TeamSumFactor {
pub(crate) inputs: Vec<(Gaussian, f64)>,
pub(crate) out: VarId,

View File

@@ -758,7 +758,7 @@ mod tests {
assert_ulps_eq!(
h.batches[0].skills.get(b).unwrap().posterior().mu(),
-1.0 * h.batches[0].skills.get(c).unwrap().posterior().mu(),
-h.batches[0].skills.get(c).unwrap().posterior().mu(),
epsilon = 1e-6
);

View File

@@ -64,18 +64,16 @@ where
Self(HashMap::new())
}
pub fn get<Q: ?Sized>(&self, k: &Q) -> Option<Index>
pub fn get<Q: ?Sized + Hash + Eq + ToOwned<Owned = K>>(&self, k: &Q) -> Option<Index>
where
K: Borrow<Q>,
Q: Hash + Eq + ToOwned<Owned = K>,
{
self.0.get(k).cloned()
}
pub fn get_or_create<Q: ?Sized>(&mut self, k: &Q) -> Index
pub fn get_or_create<Q: ?Sized + Hash + Eq + ToOwned<Owned = K>>(&mut self, k: &Q) -> Index
where
K: Borrow<Q>,
Q: Hash + Eq + ToOwned<Owned = K>,
{
if let Some(idx) = self.0.get(k) {
*idx

View File

@@ -16,6 +16,7 @@ pub struct ScheduleReport {
}
/// Drives factor propagation to convergence.
#[allow(dead_code)]
pub(crate) trait Schedule {
fn run(&self, factors: &mut [BuiltinFactor], vars: &mut VarStore) -> ScheduleReport;
}
@@ -25,6 +26,7 @@ pub(crate) trait Schedule {
/// Matches the existing `Game::likelihoods` loop bit-for-bit when given the
/// same factor layout (TeamSums first, then alternating RankDiff/Trunc pairs).
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub(crate) struct EpsilonOrMax {
pub eps: f64,
pub max: usize,

View File

@@ -94,7 +94,7 @@ impl<D: Drift> std::ops::IndexMut<Index> for AgentStore<D> {
#[cfg(test)]
mod tests {
use super::*;
use crate::{agent::Agent, drift::ConstantDrift, player::Player};
use crate::{agent::Agent, drift::ConstantDrift};
#[test]
fn insert_then_get() {

View File

@@ -1,5 +1,4 @@
use crate::Index;
use crate::batch::Skill;
use crate::{Index, batch::Skill};
/// Dense Vec-backed store for per-agent skill state within a TimeSlice.
///
@@ -50,14 +49,17 @@ impl SkillStore {
}
}
#[allow(dead_code)]
pub fn contains(&self, idx: Index) -> bool {
idx.0 < self.present.len() && self.present[idx.0]
}
#[allow(dead_code)]
pub fn len(&self) -> usize {
self.n_present
}
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
self.n_present == 0
}