491 lines
13 KiB
Rust
491 lines
13 KiB
Rust
//! Tile report structures for coherence gate coordination
|
|
//!
|
|
//! Defines the 64-byte cache-line aligned report structure that tiles
|
|
//! produce after each tick. These reports are aggregated by the coordinator
|
|
//! to form witness fragments for the coherence gate.
|
|
|
|
#![allow(missing_docs)]
|
|
|
|
use crate::delta::TileVertexId;
|
|
use crate::evidence::LogEValue;
|
|
use core::mem::size_of;
|
|
|
|
/// Tile status codes
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
#[repr(u8)]
|
|
pub enum TileStatus {
|
|
/// Tile is idle (no work)
|
|
Idle = 0,
|
|
/// Tile is processing deltas
|
|
Processing = 1,
|
|
/// Tile completed tick successfully
|
|
Complete = 2,
|
|
/// Tile encountered an error
|
|
Error = 3,
|
|
/// Tile is waiting for synchronization
|
|
Waiting = 4,
|
|
/// Tile is checkpointing
|
|
Checkpointing = 5,
|
|
/// Tile is recovering from checkpoint
|
|
Recovering = 6,
|
|
/// Tile is shutting down
|
|
Shutdown = 7,
|
|
}
|
|
|
|
impl From<u8> for TileStatus {
|
|
fn from(v: u8) -> Self {
|
|
match v {
|
|
0 => TileStatus::Idle,
|
|
1 => TileStatus::Processing,
|
|
2 => TileStatus::Complete,
|
|
3 => TileStatus::Error,
|
|
4 => TileStatus::Waiting,
|
|
5 => TileStatus::Checkpointing,
|
|
6 => TileStatus::Recovering,
|
|
7 => TileStatus::Shutdown,
|
|
_ => TileStatus::Error,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Witness fragment for aggregation
|
|
///
|
|
/// Compact representation of local cut/partition information
|
|
/// that can be merged across tiles.
|
|
#[derive(Debug, Clone, Copy, Default)]
|
|
#[repr(C, align(8))]
|
|
pub struct WitnessFragment {
|
|
/// Seed vertex for this fragment
|
|
pub seed: TileVertexId,
|
|
/// Boundary size (cut edges crossing fragment)
|
|
pub boundary_size: u16,
|
|
/// Cardinality (vertices in fragment)
|
|
pub cardinality: u16,
|
|
/// Fragment hash for consistency checking
|
|
pub hash: u16,
|
|
/// Local minimum cut value (fixed-point)
|
|
pub local_min_cut: u16,
|
|
/// Component ID this fragment belongs to
|
|
pub component: u16,
|
|
/// Reserved padding
|
|
pub _reserved: u16,
|
|
}
|
|
|
|
impl WitnessFragment {
|
|
/// Create a new witness fragment
|
|
#[inline]
|
|
pub const fn new(
|
|
seed: TileVertexId,
|
|
boundary_size: u16,
|
|
cardinality: u16,
|
|
local_min_cut: u16,
|
|
) -> Self {
|
|
Self {
|
|
seed,
|
|
boundary_size,
|
|
cardinality,
|
|
hash: 0,
|
|
local_min_cut,
|
|
component: 0,
|
|
_reserved: 0,
|
|
}
|
|
}
|
|
|
|
/// Compute fragment hash
|
|
pub fn compute_hash(&mut self) {
|
|
let mut h = self.seed as u32;
|
|
h = h.wrapping_mul(31).wrapping_add(self.boundary_size as u32);
|
|
h = h.wrapping_mul(31).wrapping_add(self.cardinality as u32);
|
|
h = h.wrapping_mul(31).wrapping_add(self.local_min_cut as u32);
|
|
self.hash = (h & 0xFFFF) as u16;
|
|
}
|
|
|
|
/// Check if fragment is empty
|
|
#[inline]
|
|
pub const fn is_empty(&self) -> bool {
|
|
self.cardinality == 0
|
|
}
|
|
}
|
|
|
|
/// Tile report produced after each tick (64 bytes, cache-line aligned)
|
|
///
|
|
/// This structure is designed to fit exactly in one cache line for
|
|
/// efficient memory access patterns in the coordinator.
|
|
#[derive(Debug, Clone, Copy)]
|
|
#[repr(C, align(64))]
|
|
pub struct TileReport {
|
|
// --- Header (8 bytes) ---
|
|
/// Tile ID (0-255)
|
|
pub tile_id: u8,
|
|
/// Tile status
|
|
pub status: TileStatus,
|
|
/// Generation/epoch number
|
|
pub generation: u16,
|
|
/// Current tick number
|
|
pub tick: u32,
|
|
|
|
// --- Graph state (8 bytes) ---
|
|
/// Number of active vertices
|
|
pub num_vertices: u16,
|
|
/// Number of active edges
|
|
pub num_edges: u16,
|
|
/// Number of connected components
|
|
pub num_components: u16,
|
|
/// Graph flags
|
|
pub graph_flags: u16,
|
|
|
|
// --- Evidence state (8 bytes) ---
|
|
/// Global log e-value (tile-local)
|
|
pub log_e_value: LogEValue,
|
|
/// Number of observations processed
|
|
pub obs_count: u16,
|
|
/// Number of rejected hypotheses
|
|
pub rejected_count: u16,
|
|
|
|
// --- Witness fragment (16 bytes) ---
|
|
/// Primary witness fragment
|
|
pub witness: WitnessFragment,
|
|
|
|
// --- Performance metrics (8 bytes) ---
|
|
/// Delta processing time (microseconds)
|
|
pub delta_time_us: u16,
|
|
/// Tick processing time (microseconds)
|
|
pub tick_time_us: u16,
|
|
/// Deltas processed this tick
|
|
pub deltas_processed: u16,
|
|
/// Memory usage (KB)
|
|
pub memory_kb: u16,
|
|
|
|
// --- Cross-tile coordination (8 bytes) ---
|
|
/// Number of ghost vertices
|
|
pub ghost_vertices: u16,
|
|
/// Number of ghost edges
|
|
pub ghost_edges: u16,
|
|
/// Boundary vertices (shared with other tiles)
|
|
pub boundary_vertices: u16,
|
|
/// Pending sync messages
|
|
pub pending_sync: u16,
|
|
|
|
// --- Reserved for future use (8 bytes) ---
|
|
/// Reserved fields
|
|
pub _reserved: [u8; 8],
|
|
}
|
|
|
|
impl Default for TileReport {
|
|
fn default() -> Self {
|
|
Self::new(0)
|
|
}
|
|
}
|
|
|
|
impl TileReport {
|
|
/// Graph flag: graph is connected
|
|
pub const GRAPH_CONNECTED: u16 = 0x0001;
|
|
/// Graph flag: graph is dirty (needs recomputation)
|
|
pub const GRAPH_DIRTY: u16 = 0x0002;
|
|
/// Graph flag: graph is at capacity
|
|
pub const GRAPH_FULL: u16 = 0x0004;
|
|
/// Graph flag: graph has ghost edges
|
|
pub const GRAPH_HAS_GHOSTS: u16 = 0x0008;
|
|
|
|
/// Create a new report for a tile
|
|
#[inline]
|
|
pub const fn new(tile_id: u8) -> Self {
|
|
Self {
|
|
tile_id,
|
|
status: TileStatus::Idle,
|
|
generation: 0,
|
|
tick: 0,
|
|
num_vertices: 0,
|
|
num_edges: 0,
|
|
num_components: 0,
|
|
graph_flags: 0,
|
|
log_e_value: 0,
|
|
obs_count: 0,
|
|
rejected_count: 0,
|
|
witness: WitnessFragment {
|
|
seed: 0,
|
|
boundary_size: 0,
|
|
cardinality: 0,
|
|
hash: 0,
|
|
local_min_cut: 0,
|
|
component: 0,
|
|
_reserved: 0,
|
|
},
|
|
delta_time_us: 0,
|
|
tick_time_us: 0,
|
|
deltas_processed: 0,
|
|
memory_kb: 0,
|
|
ghost_vertices: 0,
|
|
ghost_edges: 0,
|
|
boundary_vertices: 0,
|
|
pending_sync: 0,
|
|
_reserved: [0; 8],
|
|
}
|
|
}
|
|
|
|
/// Mark report as complete
|
|
#[inline]
|
|
pub fn set_complete(&mut self) {
|
|
self.status = TileStatus::Complete;
|
|
}
|
|
|
|
/// Mark report as error
|
|
#[inline]
|
|
pub fn set_error(&mut self) {
|
|
self.status = TileStatus::Error;
|
|
}
|
|
|
|
/// Set connected flag
|
|
#[inline]
|
|
pub fn set_connected(&mut self, connected: bool) {
|
|
if connected {
|
|
self.graph_flags |= Self::GRAPH_CONNECTED;
|
|
} else {
|
|
self.graph_flags &= !Self::GRAPH_CONNECTED;
|
|
}
|
|
}
|
|
|
|
/// Check if graph is connected
|
|
#[inline]
|
|
pub const fn is_connected(&self) -> bool {
|
|
self.graph_flags & Self::GRAPH_CONNECTED != 0
|
|
}
|
|
|
|
/// Check if graph is dirty
|
|
#[inline]
|
|
pub const fn is_dirty(&self) -> bool {
|
|
self.graph_flags & Self::GRAPH_DIRTY != 0
|
|
}
|
|
|
|
/// Get e-value as approximate f32
|
|
pub fn e_value_approx(&self) -> f32 {
|
|
let log2_val = (self.log_e_value as f32) / 65536.0;
|
|
libm::exp2f(log2_val)
|
|
}
|
|
|
|
/// Update witness fragment
|
|
pub fn set_witness(&mut self, witness: WitnessFragment) {
|
|
self.witness = witness;
|
|
}
|
|
|
|
/// Get the witness fragment
|
|
#[inline]
|
|
pub const fn get_witness(&self) -> &WitnessFragment {
|
|
&self.witness
|
|
}
|
|
|
|
/// Check if tile has any rejections
|
|
#[inline]
|
|
pub const fn has_rejections(&self) -> bool {
|
|
self.rejected_count > 0
|
|
}
|
|
|
|
/// Get processing rate (deltas per microsecond)
|
|
pub fn processing_rate(&self) -> f32 {
|
|
if self.tick_time_us == 0 {
|
|
0.0
|
|
} else {
|
|
(self.deltas_processed as f32) / (self.tick_time_us as f32)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Report aggregator for combining multiple tile reports
|
|
#[derive(Debug, Clone, Copy, Default)]
|
|
#[repr(C)]
|
|
pub struct AggregatedReport {
|
|
/// Total vertices across all tiles
|
|
pub total_vertices: u32,
|
|
/// Total edges across all tiles
|
|
pub total_edges: u32,
|
|
/// Total components across all tiles
|
|
pub total_components: u16,
|
|
/// Number of tiles reporting
|
|
pub tiles_reporting: u16,
|
|
/// Tiles with errors
|
|
pub tiles_with_errors: u16,
|
|
/// Tiles with rejections
|
|
pub tiles_with_rejections: u16,
|
|
/// Global log e-value (sum of tile e-values)
|
|
pub global_log_e: i64,
|
|
/// Minimum local cut across tiles
|
|
pub global_min_cut: u16,
|
|
/// Tile with minimum cut
|
|
pub min_cut_tile: u8,
|
|
/// Reserved padding
|
|
pub _reserved: u8,
|
|
/// Total processing time (microseconds)
|
|
pub total_time_us: u32,
|
|
/// Tick number
|
|
pub tick: u32,
|
|
}
|
|
|
|
impl AggregatedReport {
|
|
/// Create a new aggregated report
|
|
pub const fn new(tick: u32) -> Self {
|
|
Self {
|
|
total_vertices: 0,
|
|
total_edges: 0,
|
|
total_components: 0,
|
|
tiles_reporting: 0,
|
|
tiles_with_errors: 0,
|
|
tiles_with_rejections: 0,
|
|
global_log_e: 0,
|
|
global_min_cut: u16::MAX,
|
|
min_cut_tile: 0,
|
|
_reserved: 0,
|
|
total_time_us: 0,
|
|
tick,
|
|
}
|
|
}
|
|
|
|
/// Merge a tile report into the aggregate
|
|
pub fn merge(&mut self, report: &TileReport) {
|
|
self.total_vertices += report.num_vertices as u32;
|
|
self.total_edges += report.num_edges as u32;
|
|
self.total_components += report.num_components;
|
|
self.tiles_reporting += 1;
|
|
|
|
if report.status == TileStatus::Error {
|
|
self.tiles_with_errors += 1;
|
|
}
|
|
|
|
if report.rejected_count > 0 {
|
|
self.tiles_with_rejections += 1;
|
|
}
|
|
|
|
self.global_log_e += report.log_e_value as i64;
|
|
|
|
if report.witness.local_min_cut < self.global_min_cut {
|
|
self.global_min_cut = report.witness.local_min_cut;
|
|
self.min_cut_tile = report.tile_id;
|
|
}
|
|
|
|
self.total_time_us = self.total_time_us.max(report.tick_time_us as u32);
|
|
}
|
|
|
|
/// Check if all tiles completed successfully
|
|
pub fn all_complete(&self, expected_tiles: u16) -> bool {
|
|
self.tiles_reporting == expected_tiles && self.tiles_with_errors == 0
|
|
}
|
|
|
|
/// Get global e-value as approximate f64
|
|
pub fn global_e_value(&self) -> f64 {
|
|
let log2_val = (self.global_log_e as f64) / 65536.0;
|
|
libm::exp2(log2_val)
|
|
}
|
|
}
|
|
|
|
// Compile-time size assertions
|
|
const _: () = assert!(
|
|
size_of::<TileReport>() == 64,
|
|
"TileReport must be exactly 64 bytes"
|
|
);
|
|
const _: () = assert!(
|
|
size_of::<WitnessFragment>() == 16,
|
|
"WitnessFragment must be 16 bytes"
|
|
);
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_tile_report_size() {
|
|
assert_eq!(size_of::<TileReport>(), 64);
|
|
}
|
|
|
|
#[test]
|
|
fn test_tile_report_alignment() {
|
|
assert_eq!(core::mem::align_of::<TileReport>(), 64);
|
|
}
|
|
|
|
#[test]
|
|
fn test_witness_fragment_size() {
|
|
assert_eq!(size_of::<WitnessFragment>(), 16);
|
|
}
|
|
|
|
#[test]
|
|
fn test_new_report() {
|
|
let report = TileReport::new(5);
|
|
assert_eq!(report.tile_id, 5);
|
|
assert_eq!(report.status, TileStatus::Idle);
|
|
assert_eq!(report.tick, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_set_status() {
|
|
let mut report = TileReport::new(0);
|
|
report.set_complete();
|
|
assert_eq!(report.status, TileStatus::Complete);
|
|
|
|
report.set_error();
|
|
assert_eq!(report.status, TileStatus::Error);
|
|
}
|
|
|
|
#[test]
|
|
fn test_connected_flag() {
|
|
let mut report = TileReport::new(0);
|
|
assert!(!report.is_connected());
|
|
|
|
report.set_connected(true);
|
|
assert!(report.is_connected());
|
|
|
|
report.set_connected(false);
|
|
assert!(!report.is_connected());
|
|
}
|
|
|
|
#[test]
|
|
fn test_witness_fragment() {
|
|
let mut frag = WitnessFragment::new(10, 5, 20, 100);
|
|
assert_eq!(frag.seed, 10);
|
|
assert_eq!(frag.boundary_size, 5);
|
|
assert_eq!(frag.cardinality, 20);
|
|
assert_eq!(frag.local_min_cut, 100);
|
|
|
|
frag.compute_hash();
|
|
assert_ne!(frag.hash, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_aggregated_report() {
|
|
let mut agg = AggregatedReport::new(1);
|
|
|
|
let mut report1 = TileReport::new(0);
|
|
report1.num_vertices = 50;
|
|
report1.num_edges = 100;
|
|
report1.witness.local_min_cut = 200;
|
|
|
|
let mut report2 = TileReport::new(1);
|
|
report2.num_vertices = 75;
|
|
report2.num_edges = 150;
|
|
report2.witness.local_min_cut = 150;
|
|
|
|
agg.merge(&report1);
|
|
agg.merge(&report2);
|
|
|
|
assert_eq!(agg.tiles_reporting, 2);
|
|
assert_eq!(agg.total_vertices, 125);
|
|
assert_eq!(agg.total_edges, 250);
|
|
assert_eq!(agg.global_min_cut, 150);
|
|
assert_eq!(agg.min_cut_tile, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_tile_status_roundtrip() {
|
|
for i in 0..=7 {
|
|
let status = TileStatus::from(i);
|
|
assert_eq!(status as u8, i);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_processing_rate() {
|
|
let mut report = TileReport::new(0);
|
|
report.deltas_processed = 100;
|
|
report.tick_time_us = 50;
|
|
|
|
assert!((report.processing_rate() - 2.0).abs() < 0.01);
|
|
}
|
|
}
|