Coverage Report

Created: 2026-01-25 15:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/noah/src/trueno/src/brick/mod.rs
Line
Count
Source
1
//! ComputeBrick: Token-Centric Compute Units
2
//!
3
//! A **ComputeBrick** is a self-verifying, token-centric compute unit that bundles:
4
//! - **Operation**: The compute operation (matmul, dot, softmax, etc.)
5
//! - **Assertions**: Falsifiable claims about the output (equivalence, bounds)
6
//! - **Budget**: Performance target in µs/token or tokens/sec
7
//! - **Backend**: Execution target (Scalar, AVX2, CUDA, etc.)
8
//!
9
//! # Core Insight
10
//!
11
//! A **token** is the unit of data; a **ComputeBrick** is the unit of compute.
12
//!
13
//! ```text
14
//! Token ──▶ [ComputeBrick] ──▶ Token
15
//!            (matmul, softmax, attention)
16
//! ```
17
//!
18
//! # Example
19
//!
20
//! ```rust,ignore
21
//! use trueno::brick::{ComputeBrick, ComputeBackend, MatmulOp};
22
//!
23
//! let matmul = ComputeBrick::new(MatmulOp::new(1024, 1024, 1024))
24
//!     .assert_equiv(ComputeBackend::Scalar)
25
//!     .budget_tok_per_sec(50_000.0)
26
//!     .backend(ComputeBackend::Avx2);
27
//!
28
//! let result = matmul.run((a, b))?;
29
//! println!("Throughput: {:.0} tok/s", result.tokens_per_sec);
30
//! ```
31
//!
32
//! # Scientific Basis
33
//!
34
//! Per Popper (1959), a theory that makes no falsifiable predictions is not scientific.
35
//! A ComputeBrick with no assertions makes no testable claims and is therefore invalid.
36
37
// Submodules
38
mod batch;
39
mod buffer;
40
mod circuit;
41
mod connection;
42
mod memory;
43
mod perf_metrics;
44
mod profiling;
45
mod rate_limit;
46
mod resource_pool;
47
mod shutdown;
48
49
// Re-export profiling functions
50
pub use profiling::{
51
    cached_nanos, cached_nanos_or_now, cpu_cycles, get_page_faults, init_time_service,
52
    with_page_fault_tracking,
53
};
54
55
// Re-export perf_metrics types
56
pub use perf_metrics::{InferencePhase, PerfMetrics};
57
58
// Re-export memory types
59
#[cfg(not(target_arch = "wasm32"))]
60
pub use memory::AlignedBuffer;
61
pub use memory::{
62
    is_direct_io_aligned, madvise_region, prefetch_for_inference, prefetch_ptr, prefetch_slice,
63
    CacheAligned, MemoryAdvice, PrefetchLocality, CACHE_LINE_SIZE, CACHE_LINE_SIZE_F32,
64
    DIRECT_IO_ALIGNMENT,
65
};
66
67
// Re-export buffer types
68
pub use buffer::{BufferWatermarks, WatermarkedBuffer};
69
70
// Re-export circuit breaker types
71
pub use circuit::{CircuitBreaker, CircuitState};
72
73
// Re-export shutdown types
74
pub use shutdown::{GracefulShutdown, ShutdownGuard, ShutdownResult};
75
76
// Re-export resource pool types
77
pub use resource_pool::{PooledResource, ResourcePool};
78
79
// Re-export rate limiting types
80
pub use rate_limit::{LimitError, ServeLimits};
81
82
// Re-export connection types
83
pub use connection::{ConnectionState, KeepAliveConfig, ManagedConnection};
84
85
// Re-export batch types
86
pub use batch::{balance211, Balance211Iter, BatchSplitStrategy, split_batch};
87
88
// KV cache management
89
mod kv_cache;
90
pub use kv_cache::{KvCacheManager, KvCacheSlotInfo, SequentialBatchOrderer};
91
92
// SIMD configuration
93
mod simd_config;
94
pub use simd_config::{
95
    unroll_tail_process, AmxTileConfig, LazySimdConfig, SimdBackendState, UnrollFactor,
96
    UnrollTailIterator,
97
};
98
99
// Execution graph and brick profiling types (PAR-073, PAR-200, PAR-201)
100
mod exec_graph;
101
pub use exec_graph::{
102
    BrickBottleneck, BrickCategory, BrickId, BrickSample, BrickStats, CategoryStats, EdgeType,
103
    ExecutionEdge, ExecutionGraph, ExecutionNode, ExecutionNodeId, PtxRegistry, SyncMode,
104
    TransferDirection,
105
};
106
107
// BrickProfiler and tile profiling (TILING-SPEC-001)
108
mod profiler;
109
pub use profiler::{
110
    fnv1a_f32_checksum, BrickIdTimer, BrickProfiler, BrickTimer, DivergenceInfo, KernelChecksum,
111
    TileLevel, TileStats, TileTimer,
112
};
113
114
// Model-level inference tracing (Phase 13, E.11)
115
mod tracing;
116
pub use tracing::{
117
    AttentionTraceConfig, AttentionWeightTrace, KvCacheSessionTrace, KvCacheStateTrace,
118
    LayerActivationTrace, LogitEvolutionTrace, ModelActivationTrace, ModelQuantizationError,
119
    ModelTracer, ModelTracerConfig, ModelTracerSummary, QuantType, QuantizationErrorTrace,
120
    TensorStats, TokenLogitEvolution,
121
};
122
123
// Async and buffer patterns (Phase 12, E.10)
124
mod patterns;
125
pub use patterns::{
126
    reserve_capacity, AsyncResult, BoundedQueue, DualWakerState, FlowControlError,
127
    GraphReuseCounter, ReserveStrategy, StrategicBuffer, StreamCapacity, WakeDecision,
128
    WakeSkipState,
129
};
130
131
// Built-in compute operations
132
mod ops;
133
pub use ops::{AddOp, DotOp, MatmulOp, SoftmaxOp};
134
135
// Fused operations for transformer inference (PMAT-PERF-009)
136
mod fused_ops;
137
pub use fused_ops::{FusedGateUpOp, FusedGateUpWeights, FusedQKVOp, FusedQKVWeights};
138
139
// SIMD-optimized attention operation (PMAT-017)
140
mod attention;
141
pub use attention::AttentionOp;
142
143
// Q5_K and Q6_K quantization operations (llama.cpp compatible)
144
mod quant_ops;
145
pub use quant_ops::{BlockQ5K, BlockQ6K, DotQ5KOp, DotQ6KOp};
146
147
// Tests (7,400+ lines extracted for TDG compliance)
148
#[cfg(test)]
149
mod tests;
150
151
use crate::error::TruenoError;
152
use std::fmt;
153
use std::marker::PhantomData;
154
use std::time::Instant;
155
156
// ============================================================================
157
// Async Task Profiler (Pattern 3 from actix-web)
158
// ============================================================================
159
160
/// Async task profiler for measuring poll efficiency (Phase 11, E.9.4).
161
///
162
/// Tracks how many times a future is polled before completion.
163
/// High poll counts indicate inefficient async code or spurious wakeups.
164
///
165
/// # Example
166
/// ```rust,ignore
167
/// let mut profiler = AsyncTaskProfiler::new("inference_request");
168
///
169
/// profiler.on_poll_start();
170
/// // ... poll the future ...
171
/// profiler.on_poll_end(is_ready);
172
///
173
/// println!("Poll efficiency: {:.1}%", profiler.efficiency() * 100.0);
174
/// ```
175
#[derive(Debug, Clone)]
176
pub struct AsyncTaskProfiler {
177
    /// Task name for identification
178
    pub name: String,
179
    /// Number of times poll() was called
180
    pub poll_count: u64,
181
    /// Number of times poll() returned Pending
182
    pub yield_count: u64,
183
    /// Total time spent in poll() (nanoseconds)
184
    pub total_poll_ns: u64,
185
    /// Start time of current poll
186
    last_poll_start: u64,
187
    /// CPU cycles at poll start
188
    last_poll_cycles: u64,
189
    /// Total CPU cycles in poll()
190
    pub total_poll_cycles: u64,
191
}
192
193
impl AsyncTaskProfiler {
194
    /// Create a new async task profiler.
195
0
    pub fn new(name: impl Into<String>) -> Self {
196
0
        Self {
197
0
            name: name.into(),
198
0
            poll_count: 0,
199
0
            yield_count: 0,
200
0
            total_poll_ns: 0,
201
0
            last_poll_start: 0,
202
0
            last_poll_cycles: 0,
203
0
            total_poll_cycles: 0,
204
0
        }
205
0
    }
206
207
    /// Call at the start of each poll() invocation.
208
    #[inline]
209
0
    pub fn on_poll_start(&mut self) {
210
0
        self.poll_count += 1;
211
0
        self.last_poll_start = cached_nanos_or_now();
212
0
        self.last_poll_cycles = cpu_cycles();
213
0
    }
214
215
    /// Call at the end of each poll() invocation.
216
    ///
217
    /// # Arguments
218
    /// - `is_ready`: true if the future returned Poll::Ready
219
    #[inline]
220
0
    pub fn on_poll_end(&mut self, is_ready: bool) {
221
0
        let now = cached_nanos_or_now();
222
0
        let cycles = cpu_cycles();
223
224
0
        self.total_poll_ns += now.saturating_sub(self.last_poll_start);
225
0
        self.total_poll_cycles += cycles.saturating_sub(self.last_poll_cycles);
226
227
0
        if !is_ready {
228
0
            self.yield_count += 1;
229
0
        }
230
0
    }
231
232
    /// Poll efficiency ratio (0.0 to 1.0).
233
    ///
234
    /// - 1.0 = Perfect (ready on first poll)
235
    /// - 0.5 = 2 polls required
236
    /// - Lower = more wakeups/polls needed
237
    #[must_use]
238
0
    pub fn efficiency(&self) -> f64 {
239
0
        if self.poll_count == 0 {
240
0
            0.0
241
        } else {
242
0
            1.0 / self.poll_count as f64
243
        }
244
0
    }
245
246
    /// Average time per poll in microseconds.
247
    #[must_use]
248
0
    pub fn avg_poll_us(&self) -> f64 {
249
0
        if self.poll_count == 0 {
250
0
            0.0
251
        } else {
252
0
            self.total_poll_ns as f64 / self.poll_count as f64 / 1000.0
253
        }
254
0
    }
255
256
    /// Yield ratio (Pending / total polls).
257
    ///
258
    /// High yield ratio indicates the task is often not ready when polled.
259
    #[must_use]
260
0
    pub fn yield_ratio(&self) -> f64 {
261
0
        if self.poll_count == 0 {
262
0
            0.0
263
        } else {
264
0
            self.yield_count as f64 / self.poll_count as f64
265
        }
266
0
    }
267
268
    /// Convert to ExecutionNode for graph integration.
269
0
    pub fn to_execution_node(&self) -> ExecutionNode {
270
0
        ExecutionNode::AsyncTask {
271
0
            name: self.name.clone(),
272
0
            poll_count: self.poll_count,
273
0
            yield_count: self.yield_count,
274
0
            total_poll_ns: self.total_poll_ns,
275
0
        }
276
0
    }
277
}
278
279
impl Default for AsyncTaskProfiler {
280
0
    fn default() -> Self {
281
0
        Self::new("unnamed")
282
0
    }
283
}
284
285
286
/// Execution backend for compute operations.
287
/// This is the brick-specific backend enum with additional GPU backends.
288
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
289
pub enum ComputeBackend {
290
    /// Pure Rust scalar fallback (always available, baseline for correctness)
291
    Scalar,
292
    /// SSE2 SIMD (x86_64 baseline)
293
    Sse2,
294
    /// AVX2 256-bit SIMD with FMA
295
    #[default]
296
    Avx2,
297
    /// AVX-512 512-bit SIMD
298
    Avx512,
299
    /// ARM NEON SIMD
300
    Neon,
301
    /// WebAssembly SIMD128
302
    Wasm,
303
    /// NVIDIA CUDA via PTX
304
    Cuda,
305
    /// Cross-platform GPU via wgpu
306
    Wgpu,
307
    /// Auto-select best available backend
308
    Auto,
309
}
310
311
impl fmt::Display for ComputeBackend {
312
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
313
0
        match self {
314
0
            ComputeBackend::Scalar => write!(f, "Scalar"),
315
0
            ComputeBackend::Sse2 => write!(f, "SSE2"),
316
0
            ComputeBackend::Avx2 => write!(f, "AVX2"),
317
0
            ComputeBackend::Avx512 => write!(f, "AVX-512"),
318
0
            ComputeBackend::Neon => write!(f, "NEON"),
319
0
            ComputeBackend::Wasm => write!(f, "WASM"),
320
0
            ComputeBackend::Cuda => write!(f, "CUDA"),
321
0
            ComputeBackend::Wgpu => write!(f, "wgpu"),
322
0
            ComputeBackend::Auto => write!(f, "Auto"),
323
        }
324
0
    }
325
}
326
327
/// Type alias for backward compatibility
328
pub type Backend = ComputeBackend;
329
330
/// Performance budget expressed in token terms.
331
/// Aligns compute costs with LLM inference metrics.
332
#[derive(Debug, Clone, Copy)]
333
pub struct TokenBudget {
334
    /// Latency budget per token (microseconds)
335
    pub us_per_token: f64,
336
    /// Throughput target (tokens/second)
337
    pub tokens_per_sec: f64,
338
    /// Batch size for amortization
339
    pub batch_size: usize,
340
}
341
342
/// Performance budget for byte-oriented operations (compression, I/O).
343
/// Use this for trueno-zram, disk I/O, network throughput, etc.
344
///
345
/// PMAT-452: Serializable for hardware.toml export.
346
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
347
pub struct ByteBudget {
348
    /// Latency budget per page (microseconds)
349
    pub us_per_page: f64,
350
    /// Throughput target (GB/s)
351
    pub gb_per_sec: f64,
352
    /// Page size in bytes (default 4096)
353
    pub page_size: usize,
354
}
355
356
impl Default for ByteBudget {
357
0
    fn default() -> Self {
358
        // Default: 25 GB/s (trueno-zram ZSTD target)
359
0
        Self::from_throughput(25.0)
360
0
    }
361
}
362
363
impl ByteBudget {
364
    /// Create budget from throughput target (GB/s).
365
    /// 25 GB/s = 0.16µs per 4KB page
366
0
    pub fn from_throughput(gb_per_sec: f64) -> Self {
367
0
        let bytes_per_sec = gb_per_sec * 1e9;
368
0
        let pages_per_sec = bytes_per_sec / 4096.0;
369
0
        Self {
370
0
            us_per_page: 1_000_000.0 / pages_per_sec,
371
0
            gb_per_sec,
372
0
            page_size: 4096,
373
0
        }
374
0
    }
375
376
    /// Create budget from latency target (µs per page).
377
0
    pub fn from_latency(us_per_page: f64) -> Self {
378
0
        let pages_per_sec = 1_000_000.0 / us_per_page;
379
0
        let bytes_per_sec = pages_per_sec * 4096.0;
380
0
        Self {
381
0
            us_per_page,
382
0
            gb_per_sec: bytes_per_sec / 1e9,
383
0
            page_size: 4096,
384
0
        }
385
0
    }
386
387
    /// Set custom page size (e.g., 64KB for huge pages).
388
    #[must_use]
389
0
    pub fn with_page_size(mut self, page_size: usize) -> Self {
390
        // Recalculate us_per_page based on new page size
391
0
        let bytes_per_sec = self.gb_per_sec * 1e9;
392
0
        let pages_per_sec = bytes_per_sec / page_size as f64;
393
0
        self.us_per_page = 1_000_000.0 / pages_per_sec;
394
0
        self.page_size = page_size;
395
0
        self
396
0
    }
397
398
    /// Convert to TokenBudget (1 token = 1 page).
399
    /// Useful for integrating byte workloads with token-centric monitoring.
400
0
    pub fn to_token_budget(&self) -> TokenBudget {
401
0
        TokenBudget {
402
0
            us_per_token: self.us_per_page,
403
0
            tokens_per_sec: 1_000_000.0 / self.us_per_page,
404
0
            batch_size: 1,
405
0
        }
406
0
    }
407
408
    /// Check if actual performance meets budget.
409
0
    pub fn is_met(&self, actual_us_per_page: f64) -> bool {
410
0
        actual_us_per_page <= self.us_per_page
411
0
    }
412
413
    /// Calculate budget utilization.
414
0
    pub fn utilization(&self, actual_us_per_page: f64) -> f64 {
415
0
        actual_us_per_page / self.us_per_page
416
0
    }
417
418
    /// Calculate actual throughput from latency.
419
0
    pub fn throughput_from_latency(us_per_page: f64, page_size: usize) -> f64 {
420
0
        let pages_per_sec = 1_000_000.0 / us_per_page;
421
0
        pages_per_sec * page_size as f64 / 1e9
422
0
    }
423
}
424
425
impl Default for TokenBudget {
426
0
    fn default() -> Self {
427
        // Default: 50µs/token = 20,000 tokens/sec
428
0
        Self::from_latency(50.0)
429
0
    }
430
}
431
432
impl TokenBudget {
433
    /// Create budget from latency target.
434
    /// 50µs/token = 20,000 tokens/sec
435
0
    pub fn from_latency(us_per_token: f64) -> Self {
436
0
        Self {
437
0
            us_per_token,
438
0
            tokens_per_sec: 1_000_000.0 / us_per_token,
439
0
            batch_size: 1,
440
0
        }
441
0
    }
442
443
    /// Create budget from throughput target.
444
    /// 20,000 tokens/sec = 50µs/token
445
0
    pub fn from_throughput(tokens_per_sec: f64) -> Self {
446
0
        Self {
447
0
            us_per_token: 1_000_000.0 / tokens_per_sec,
448
0
            tokens_per_sec,
449
0
            batch_size: 1,
450
0
        }
451
0
    }
452
453
    /// Set batch size for amortization.
454
    #[must_use]
455
0
    pub fn with_batch_size(mut self, batch_size: usize) -> Self {
456
0
        self.batch_size = batch_size.max(1);
457
0
        self
458
0
    }
459
460
    /// Check if actual performance meets budget.
461
0
    pub fn is_met(&self, actual_us_per_token: f64) -> bool {
462
0
        actual_us_per_token <= self.us_per_token
463
0
    }
464
465
    /// Calculate budget utilization (0.0 = unused, 1.0 = exactly at budget, >1.0 = over budget).
466
0
    pub fn utilization(&self, actual_us_per_token: f64) -> f64 {
467
0
        actual_us_per_token / self.us_per_token
468
0
    }
469
}
470
471
/// Result of ComputeBrick execution with token metrics.
472
#[derive(Debug, Clone)]
473
pub struct TokenResult<T> {
474
    /// Computed output
475
    pub output: T,
476
    /// Number of tokens processed
477
    pub tokens_processed: usize,
478
    /// Actual latency (microseconds/token)
479
    pub us_per_token: f64,
480
    /// Actual throughput (tokens/second)
481
    pub tokens_per_sec: f64,
482
    /// Did we meet the budget?
483
    pub budget_met: bool,
484
    /// Budget utilization (0.0-1.0+ where 1.0 = exactly at budget)
485
    pub budget_utilization: f64,
486
}
487
488
impl<T> TokenResult<T> {
489
    /// Map the output to a new type.
490
0
    pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> TokenResult<U> {
491
0
        TokenResult {
492
0
            output: f(self.output),
493
0
            tokens_processed: self.tokens_processed,
494
0
            us_per_token: self.us_per_token,
495
0
            tokens_per_sec: self.tokens_per_sec,
496
0
            budget_met: self.budget_met,
497
0
            budget_utilization: self.budget_utilization,
498
0
        }
499
0
    }
500
}
501
502
/// Errors from ComputeBrick execution.
503
/// Tells you exactly what failed (Jidoka: stop and signal).
504
#[derive(Debug, thiserror::Error)]
505
pub enum BrickError {
506
    /// Assertion failed during verification
507
    #[error("Assertion failed: {name} - expected {expected}, got {actual}")]
508
    AssertionFailed {
509
        name: String,
510
        expected: String,
511
        actual: String,
512
    },
513
514
    /// Performance budget exceeded
515
    #[error("Budget exceeded: {limit_us:.1}µs/tok limit, {actual_us:.1}µs/tok actual ({utilization:.0}% of budget)")]
516
    BudgetExceeded {
517
        limit_us: f64,
518
        actual_us: f64,
519
        utilization: f64,
520
    },
521
522
    /// Underlying compute error
523
    #[error("Compute error: {0}")]
524
    ComputeError(#[from] TruenoError),
525
526
    /// No assertions defined (violates Popperian falsifiability)
527
    #[error("Brick has no assertions - violates Popperian falsifiability requirement")]
528
    NoAssertions,
529
530
    /// Backend not available
531
    #[error("Backend {0} not available on this system")]
532
    BackendUnavailable(Backend),
533
}
534
535
/// Type of assertion for compute verification.
536
#[derive(Debug, Clone)]
537
pub enum ComputeAssertion {
538
    /// Output must match baseline backend within tolerance
539
    Equivalence {
540
        baseline: Backend,
541
        tolerance: f64,
542
    },
543
    /// Output values must be within bounds
544
    Bounds {
545
        min: f64,
546
        max: f64,
547
    },
548
    /// Output must not contain NaN or infinity
549
    Finite,
550
    /// Custom assertion with name and check function index
551
    Custom {
552
        name: String,
553
    },
554
}
555
556
impl ComputeAssertion {
557
    /// Create equivalence assertion with default tolerance (1e-5).
558
0
    pub fn equiv(baseline: Backend) -> Self {
559
0
        Self::Equivalence {
560
0
            baseline,
561
0
            tolerance: 1e-5,
562
0
        }
563
0
    }
564
565
    /// Create equivalence assertion with custom tolerance.
566
0
    pub fn equiv_with_tolerance(baseline: Backend, tolerance: f64) -> Self {
567
0
        Self::Equivalence { baseline, tolerance }
568
0
    }
569
570
    /// Create bounds assertion.
571
0
    pub fn bounds(min: f64, max: f64) -> Self {
572
0
        Self::Bounds { min, max }
573
0
    }
574
575
    /// Create finite assertion (no NaN/Inf).
576
0
    pub fn finite() -> Self {
577
0
        Self::Finite
578
0
    }
579
}
580
581
/// Verification result from ComputeBrick.
582
#[derive(Debug, Clone)]
583
pub struct BrickVerification {
584
    /// Overall pass/fail
585
    pub passed: bool,
586
    /// Individual assertion results
587
    pub assertion_results: Vec<AssertionResult>,
588
    /// Verification time in microseconds
589
    pub verification_us: f64,
590
}
591
592
impl BrickVerification {
593
    /// Check if all assertions passed.
594
0
    pub fn is_valid(&self) -> bool {
595
0
        self.passed
596
0
    }
597
598
    /// Get failed assertions.
599
0
    pub fn failures(&self) -> impl Iterator<Item = &AssertionResult> {
600
0
        self.assertion_results.iter().filter(|r| !r.passed)
601
0
    }
602
}
603
604
/// Result of a single assertion check.
605
#[derive(Debug, Clone)]
606
pub struct AssertionResult {
607
    /// Assertion that was checked
608
    pub assertion: ComputeAssertion,
609
    /// Did it pass?
610
    pub passed: bool,
611
    /// Error message if failed
612
    pub error: Option<String>,
613
}
614
615
/// Trait for compute operations that can be wrapped in a ComputeBrick.
616
pub trait ComputeOp: Send + Sync {
617
    /// Input type for this operation
618
    type Input;
619
    /// Output type for this operation
620
    type Output;
621
622
    /// Operation name for identification
623
    fn name(&self) -> &'static str;
624
625
    /// Execute the operation on the given backend
626
    fn execute(&self, input: Self::Input, backend: Backend) -> Result<Self::Output, TruenoError>;
627
628
    /// Number of tokens this operation processes (for budget calculation)
629
    fn tokens(&self, input: &Self::Input) -> usize;
630
631
    /// Clone the input for verification (if needed)
632
0
    fn clone_input(&self, input: &Self::Input) -> Option<Self::Input>
633
0
    where
634
0
        Self::Input: Clone,
635
    {
636
0
        Some(input.clone())
637
0
    }
638
}
639
640
/// Self-verifying, token-centric compute unit.
641
/// Bundles: operation + assertions + budget + verification
642
pub struct ComputeBrick<Op: ComputeOp> {
643
    /// The compute operation
644
    op: Op,
645
    /// Falsifiable assertions
646
    assertions: Vec<ComputeAssertion>,
647
    /// Token-centric performance budget
648
    budget: TokenBudget,
649
    /// Execution backend
650
    backend: Backend,
651
    /// Enforce budget (fail if exceeded)
652
    enforce_budget: bool,
653
    /// Phantom for variance
654
    _phantom: PhantomData<Op>,
655
}
656
657
impl<Op: ComputeOp> ComputeBrick<Op> {
658
    /// Create a new compute brick with the given operation.
659
0
    pub fn new(op: Op) -> Self {
660
0
        Self {
661
0
            op,
662
0
            assertions: Vec::new(),
663
0
            budget: TokenBudget::default(),
664
0
            backend: Backend::Auto,
665
0
            enforce_budget: false,
666
0
            _phantom: PhantomData,
667
0
        }
668
0
    }
669
670
    /// Add equivalence assertion (output must match baseline backend).
671
    #[must_use]
672
0
    pub fn assert_equiv(mut self, baseline: Backend) -> Self {
673
0
        self.assertions.push(ComputeAssertion::equiv(baseline));
674
0
        self
675
0
    }
676
677
    /// Add equivalence assertion with custom tolerance.
678
    #[must_use]
679
0
    pub fn assert_equiv_with_tolerance(mut self, baseline: Backend, tolerance: f64) -> Self {
680
0
        self.assertions
681
0
            .push(ComputeAssertion::equiv_with_tolerance(baseline, tolerance));
682
0
        self
683
0
    }
684
685
    /// Add bounds assertion (output values within range).
686
    #[must_use]
687
0
    pub fn assert_bounds(mut self, min: f64, max: f64) -> Self {
688
0
        self.assertions.push(ComputeAssertion::bounds(min, max));
689
0
        self
690
0
    }
691
692
    /// Add finite assertion (no NaN/Inf in output).
693
    #[must_use]
694
0
    pub fn assert_finite(mut self) -> Self {
695
0
        self.assertions.push(ComputeAssertion::finite());
696
0
        self
697
0
    }
698
699
    /// Set token throughput budget (tokens/second).
700
    #[must_use]
701
0
    pub fn budget_tok_per_sec(mut self, tps: f64) -> Self {
702
0
        self.budget = TokenBudget::from_throughput(tps);
703
0
        self
704
0
    }
705
706
    /// Set token latency budget (microseconds/token).
707
    #[must_use]
708
0
    pub fn budget_us_per_tok(mut self, us: f64) -> Self {
709
0
        self.budget = TokenBudget::from_latency(us);
710
0
        self
711
0
    }
712
713
    /// Set full budget configuration.
714
    #[must_use]
715
0
    pub fn budget(mut self, budget: TokenBudget) -> Self {
716
0
        self.budget = budget;
717
0
        self
718
0
    }
719
720
    /// Set execution backend.
721
    #[must_use]
722
0
    pub fn backend(mut self, backend: Backend) -> Self {
723
0
        self.backend = backend;
724
0
        self
725
0
    }
726
727
    /// Enforce budget (fail if exceeded). Default is false (just report).
728
    #[must_use]
729
0
    pub fn enforce_budget(mut self, enforce: bool) -> Self {
730
0
        self.enforce_budget = enforce;
731
0
        self
732
0
    }
733
734
    /// Get the brick name (from operation).
735
0
    pub fn name(&self) -> &'static str {
736
0
        self.op.name()
737
0
    }
738
739
    /// Get current budget.
740
0
    pub fn get_budget(&self) -> TokenBudget {
741
0
        self.budget
742
0
    }
743
744
    /// Get current backend.
745
0
    pub fn get_backend(&self) -> Backend {
746
0
        self.backend
747
0
    }
748
749
    /// Get assertions.
750
0
    pub fn get_assertions(&self) -> &[ComputeAssertion] {
751
0
        &self.assertions
752
0
    }
753
754
    /// Run the compute brick with full verification (Jidoka gate).
755
0
    pub fn run(&self, input: Op::Input) -> Result<TokenResult<Op::Output>, BrickError> {
756
0
        let tokens = self.op.tokens(&input);
757
758
        // Execute with timing
759
0
        let start = Instant::now();
760
0
        let output = self.op.execute(input, self.backend)?;
761
0
        let elapsed_us = start.elapsed().as_secs_f64() * 1_000_000.0;
762
763
        // Calculate metrics
764
0
        let us_per_token = if tokens > 0 {
765
0
            elapsed_us / tokens as f64
766
        } else {
767
0
            elapsed_us
768
        };
769
0
        let tokens_per_sec = if elapsed_us > 0.0 {
770
0
            tokens as f64 * 1_000_000.0 / elapsed_us
771
        } else {
772
0
            f64::INFINITY
773
        };
774
0
        let budget_met = self.budget.is_met(us_per_token);
775
0
        let budget_utilization = self.budget.utilization(us_per_token);
776
777
        // Check budget enforcement
778
0
        if self.enforce_budget && !budget_met {
779
0
            return Err(BrickError::BudgetExceeded {
780
0
                limit_us: self.budget.us_per_token,
781
0
                actual_us: us_per_token,
782
0
                utilization: budget_utilization * 100.0,
783
0
            });
784
0
        }
785
786
0
        Ok(TokenResult {
787
0
            output,
788
0
            tokens_processed: tokens,
789
0
            us_per_token,
790
0
            tokens_per_sec,
791
0
            budget_met,
792
0
            budget_utilization,
793
0
        })
794
0
    }
795
796
    /// Verify assertions without full execution.
797
    /// Returns verification status.
798
0
    pub fn verify(&self) -> BrickVerification {
799
0
        let start = Instant::now();
800
801
        // Check if we have assertions (Popperian requirement)
802
0
        if self.assertions.is_empty() {
803
0
            return BrickVerification {
804
0
                passed: false,
805
0
                assertion_results: vec![AssertionResult {
806
0
                    assertion: ComputeAssertion::Custom {
807
0
                        name: "popperian_falsifiability".to_string(),
808
0
                    },
809
0
                    passed: false,
810
0
                    error: Some("No assertions defined - violates Popperian falsifiability".to_string()),
811
0
                }],
812
0
                verification_us: start.elapsed().as_secs_f64() * 1_000_000.0,
813
0
            };
814
0
        }
815
816
        // For now, just validate assertion structure
817
        // Full verification requires input data
818
0
        let results: Vec<AssertionResult> = self
819
0
            .assertions
820
0
            .iter()
821
0
            .map(|a| AssertionResult {
822
0
                assertion: a.clone(),
823
                passed: true,
824
0
                error: None,
825
0
            })
826
0
            .collect();
827
828
0
        let passed = results.iter().all(|r| r.passed);
829
830
0
        BrickVerification {
831
0
            passed,
832
0
            assertion_results: results,
833
0
            verification_us: start.elapsed().as_secs_f64() * 1_000_000.0,
834
0
        }
835
0
    }
836
}
837
838
impl<Op: ComputeOp + Clone> Clone for ComputeBrick<Op> {
839
0
    fn clone(&self) -> Self {
840
0
        Self {
841
0
            op: self.op.clone(),
842
0
            assertions: self.assertions.clone(),
843
0
            budget: self.budget,
844
0
            backend: self.backend,
845
0
            enforce_budget: self.enforce_budget,
846
0
            _phantom: PhantomData,
847
0
        }
848
0
    }
849
}
850
851
impl<Op: ComputeOp> fmt::Debug for ComputeBrick<Op> {
852
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
853
0
        f.debug_struct("ComputeBrick")
854
0
            .field("name", &self.op.name())
855
0
            .field("backend", &self.backend)
856
0
            .field("budget", &self.budget)
857
0
            .field("assertions", &self.assertions.len())
858
0
            .field("enforce_budget", &self.enforce_budget)
859
0
            .finish()
860
0
    }
861
}
862
863
// ============================================================================
864
// LLM Transformer Fused Operations (PMAT-PERF-009)
865
// BrickLayer: Compose multiple bricks
866
// ============================================================================
867
868
/// A layer of compute bricks that execute sequentially.
869
/// Throughput ceiling = min(component throughputs).
870
#[derive(Debug, Default)]
871
pub struct BrickLayer {
872
    /// Named bricks in this layer
873
    bricks: Vec<(String, f64)>, // (name, budget_tok_per_sec)
874
}
875
876
impl BrickLayer {
877
    /// Create a new empty layer.
878
0
    pub fn new() -> Self {
879
0
        Self::default()
880
0
    }
881
882
    /// Add a brick to the layer.
883
    #[must_use]
884
0
    pub fn with_brick<Op: ComputeOp>(mut self, brick: &ComputeBrick<Op>) -> Self {
885
0
        self.bricks
886
0
            .push((brick.name().to_string(), brick.budget.tokens_per_sec));
887
0
        self
888
0
    }
889
890
    /// Add a named entry with throughput budget.
891
    #[must_use]
892
0
    pub fn with_named(mut self, name: &str, budget_tok_per_sec: f64) -> Self {
893
0
        self.bricks.push((name.to_string(), budget_tok_per_sec));
894
0
        self
895
0
    }
896
897
    /// Get the throughput ceiling (bottleneck).
898
    /// Layer throughput = min(component throughputs).
899
0
    pub fn throughput_ceiling(&self) -> f64 {
900
0
        self.bricks
901
0
            .iter()
902
0
            .map(|(_, tps)| *tps)
903
0
            .fold(f64::INFINITY, f64::min)
904
0
    }
905
906
    /// Get the bottleneck brick name.
907
0
    pub fn bottleneck(&self) -> Option<&str> {
908
0
        self.bricks
909
0
            .iter()
910
0
            .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
911
0
            .map(|(name, _)| name.as_str())
912
0
    }
913
914
    /// Get all bricks with their budgets.
915
0
    pub fn bricks(&self) -> &[(String, f64)] {
916
0
        &self.bricks
917
0
    }
918
}