rustkernel_core/
kernel.rs

1//! Kernel metadata and configuration.
2//!
3//! This module defines the kernel execution modes and metadata structures
4//! that mirror the C# `[GpuKernel]` attribute system.
5
6use crate::domain::Domain;
7use serde::{Deserialize, Serialize};
8use std::fmt;
9
10/// Kernel execution mode.
11///
12/// Determines how the kernel is launched and manages state:
13/// - `Batch`: Traditional GPU offload with CPU orchestration
14/// - `Ring`: Persistent GPU-native actor with lock-free messaging
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
16pub enum KernelMode {
17    /// Batch kernel mode (CPU-orchestrated).
18    ///
19    /// Characteristics:
20    /// - 10-50μs launch overhead per invocation
21    /// - State resides in CPU memory between operations
22    /// - Data transfer via pinned memory (PCIe bandwidth limited)
23    /// - Best for: Periodic heavy computation (>10ms compute time)
24    Batch,
25
26    /// Ring kernel mode (GPU-persistent actor).
27    ///
28    /// Characteristics:
29    /// - 100-500ns message processing latency
30    /// - State permanently in GPU memory
31    /// - Communication via lock-free ring buffers (zero-copy)
32    /// - Best for: High-frequency operations (>10K msgs/sec), real-time
33    Ring,
34}
35
36impl KernelMode {
37    /// Returns true if this is a batch kernel.
38    #[must_use]
39    pub const fn is_batch(&self) -> bool {
40        matches!(self, KernelMode::Batch)
41    }
42
43    /// Returns true if this is a ring kernel.
44    #[must_use]
45    pub const fn is_ring(&self) -> bool {
46        matches!(self, KernelMode::Ring)
47    }
48
49    /// Returns the typical launch overhead in microseconds.
50    #[must_use]
51    pub const fn typical_overhead_us(&self) -> f64 {
52        match self {
53            KernelMode::Batch => 30.0, // 10-50μs range, use 30 as typical
54            KernelMode::Ring => 0.3,   // 100-500ns range, use 300ns as typical
55        }
56    }
57
58    /// Returns the mode name as a string.
59    #[must_use]
60    pub const fn as_str(&self) -> &'static str {
61        match self {
62            KernelMode::Batch => "batch",
63            KernelMode::Ring => "ring",
64        }
65    }
66}
67
68impl fmt::Display for KernelMode {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        write!(f, "{}", self.as_str())
71    }
72}
73
74/// Kernel metadata (mirrors C# `[GpuKernel]` attribute).
75///
76/// Contains all configuration and performance expectations for a kernel.
77#[derive(Clone, Debug, Serialize, Deserialize)]
78pub struct KernelMetadata {
79    /// Unique kernel identifier (e.g., "graph/pagerank").
80    pub id: String,
81
82    /// Kernel execution mode.
83    pub mode: KernelMode,
84
85    /// Business domain for licensing and organization.
86    pub domain: Domain,
87
88    /// Human-readable description.
89    pub description: String,
90
91    /// Expected throughput in operations per second.
92    pub expected_throughput: u64,
93
94    /// Target latency in microseconds.
95    pub target_latency_us: f64,
96
97    /// Whether this kernel requires GPU-native execution.
98    ///
99    /// If true, CPU fallback is not available.
100    pub requires_gpu_native: bool,
101
102    /// Version of the kernel implementation.
103    pub version: u32,
104}
105
106impl KernelMetadata {
107    /// Create a new batch kernel metadata.
108    #[must_use]
109    pub fn batch(id: impl Into<String>, domain: Domain) -> Self {
110        Self {
111            id: id.into(),
112            mode: KernelMode::Batch,
113            domain,
114            description: String::new(),
115            expected_throughput: 10_000,
116            target_latency_us: 50.0,
117            requires_gpu_native: false,
118            version: 1,
119        }
120    }
121
122    /// Create a new ring kernel metadata.
123    #[must_use]
124    pub fn ring(id: impl Into<String>, domain: Domain) -> Self {
125        Self {
126            id: id.into(),
127            mode: KernelMode::Ring,
128            domain,
129            description: String::new(),
130            expected_throughput: 100_000,
131            target_latency_us: 1.0,
132            requires_gpu_native: true,
133            version: 1,
134        }
135    }
136
137    /// Set the description.
138    #[must_use]
139    pub fn with_description(mut self, description: impl Into<String>) -> Self {
140        self.description = description.into();
141        self
142    }
143
144    /// Set the expected throughput.
145    #[must_use]
146    pub fn with_throughput(mut self, ops_per_sec: u64) -> Self {
147        self.expected_throughput = ops_per_sec;
148        self
149    }
150
151    /// Set the target latency.
152    #[must_use]
153    pub fn with_latency_us(mut self, latency_us: f64) -> Self {
154        self.target_latency_us = latency_us;
155        self
156    }
157
158    /// Set whether GPU-native execution is required.
159    #[must_use]
160    pub fn with_gpu_native(mut self, required: bool) -> Self {
161        self.requires_gpu_native = required;
162        self
163    }
164
165    /// Set the version.
166    #[must_use]
167    pub fn with_version(mut self, version: u32) -> Self {
168        self.version = version;
169        self
170    }
171
172    /// Returns the feature string for licensing.
173    ///
174    /// Format: `Domain.KernelName` where KernelName is extracted from the ID.
175    #[must_use]
176    pub fn feature_string(&self) -> String {
177        // Extract kernel name from ID (e.g., "graph/pagerank" -> "PageRank")
178        let name = self.id.rsplit('/').next().unwrap_or(&self.id);
179        let name = to_pascal_case(name);
180        format!("{}.{}", self.domain, name)
181    }
182
183    /// Returns the full kernel ID path.
184    #[must_use]
185    pub fn full_id(&self) -> String {
186        format!("{}/{}", self.domain.as_str().to_lowercase(), self.id)
187    }
188}
189
190impl Default for KernelMetadata {
191    fn default() -> Self {
192        Self::batch("unnamed", Domain::Core)
193    }
194}
195
196/// Convert a kebab-case or snake_case string to PascalCase.
197fn to_pascal_case(s: &str) -> String {
198    s.split(['-', '_'])
199        .filter(|part| !part.is_empty())
200        .map(|part| {
201            let mut chars = part.chars();
202            match chars.next() {
203                Some(first) => first.to_uppercase().chain(chars).collect::<String>(),
204                None => String::new(),
205            }
206        })
207        .collect()
208}
209
210/// Builder for kernel metadata.
211#[derive(Default)]
212pub struct KernelMetadataBuilder {
213    id: Option<String>,
214    mode: Option<KernelMode>,
215    domain: Option<Domain>,
216    description: String,
217    expected_throughput: u64,
218    target_latency_us: f64,
219    requires_gpu_native: bool,
220    version: u32,
221}
222
223impl KernelMetadataBuilder {
224    /// Create a new builder.
225    #[must_use]
226    pub fn new() -> Self {
227        Self {
228            expected_throughput: 10_000,
229            target_latency_us: 50.0,
230            version: 1,
231            ..Default::default()
232        }
233    }
234
235    /// Set the kernel ID.
236    #[must_use]
237    pub fn id(mut self, id: impl Into<String>) -> Self {
238        self.id = Some(id.into());
239        self
240    }
241
242    /// Set the kernel mode.
243    #[must_use]
244    pub fn mode(mut self, mode: KernelMode) -> Self {
245        self.mode = Some(mode);
246        self
247    }
248
249    /// Set the domain.
250    #[must_use]
251    pub fn domain(mut self, domain: Domain) -> Self {
252        self.domain = Some(domain);
253        self
254    }
255
256    /// Set the description.
257    #[must_use]
258    pub fn description(mut self, description: impl Into<String>) -> Self {
259        self.description = description.into();
260        self
261    }
262
263    /// Set the expected throughput.
264    #[must_use]
265    pub fn throughput(mut self, ops_per_sec: u64) -> Self {
266        self.expected_throughput = ops_per_sec;
267        self
268    }
269
270    /// Set the target latency.
271    #[must_use]
272    pub fn latency_us(mut self, latency_us: f64) -> Self {
273        self.target_latency_us = latency_us;
274        self
275    }
276
277    /// Set whether GPU-native execution is required.
278    #[must_use]
279    pub fn gpu_native(mut self, required: bool) -> Self {
280        self.requires_gpu_native = required;
281        self
282    }
283
284    /// Set the version.
285    #[must_use]
286    pub fn version(mut self, version: u32) -> Self {
287        self.version = version;
288        self
289    }
290
291    /// Build the metadata.
292    ///
293    /// # Panics
294    ///
295    /// Panics if `id`, `mode`, or `domain` are not set.
296    #[must_use]
297    pub fn build(self) -> KernelMetadata {
298        KernelMetadata {
299            id: self.id.expect("id is required"),
300            mode: self.mode.expect("mode is required"),
301            domain: self.domain.expect("domain is required"),
302            description: self.description,
303            expected_throughput: self.expected_throughput,
304            target_latency_us: self.target_latency_us,
305            requires_gpu_native: self.requires_gpu_native,
306            version: self.version,
307        }
308    }
309
310    /// Try to build the metadata.
311    ///
312    /// Returns `None` if required fields are missing.
313    #[must_use]
314    pub fn try_build(self) -> Option<KernelMetadata> {
315        Some(KernelMetadata {
316            id: self.id?,
317            mode: self.mode?,
318            domain: self.domain?,
319            description: self.description,
320            expected_throughput: self.expected_throughput,
321            target_latency_us: self.target_latency_us,
322            requires_gpu_native: self.requires_gpu_native,
323            version: self.version,
324        })
325    }
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331
332    #[test]
333    fn test_kernel_mode_properties() {
334        assert!(KernelMode::Batch.is_batch());
335        assert!(!KernelMode::Batch.is_ring());
336        assert!(KernelMode::Ring.is_ring());
337        assert!(!KernelMode::Ring.is_batch());
338    }
339
340    #[test]
341    fn test_kernel_metadata_batch() {
342        let meta = KernelMetadata::batch("pagerank", Domain::GraphAnalytics)
343            .with_description("PageRank centrality")
344            .with_throughput(100_000)
345            .with_latency_us(10.0);
346
347        assert_eq!(meta.id, "pagerank");
348        assert_eq!(meta.mode, KernelMode::Batch);
349        assert_eq!(meta.domain, Domain::GraphAnalytics);
350        assert!(!meta.requires_gpu_native);
351    }
352
353    #[test]
354    fn test_kernel_metadata_ring() {
355        let meta = KernelMetadata::ring("pagerank", Domain::GraphAnalytics);
356
357        assert_eq!(meta.mode, KernelMode::Ring);
358        assert!(meta.requires_gpu_native);
359    }
360
361    #[test]
362    fn test_feature_string() {
363        let meta = KernelMetadata::ring("pagerank", Domain::GraphAnalytics);
364        assert_eq!(meta.feature_string(), "GraphAnalytics.Pagerank");
365
366        let meta = KernelMetadata::ring("graph/degree-centrality", Domain::GraphAnalytics);
367        assert_eq!(meta.feature_string(), "GraphAnalytics.DegreeCentrality");
368    }
369
370    #[test]
371    fn test_to_pascal_case() {
372        assert_eq!(to_pascal_case("pagerank"), "Pagerank");
373        assert_eq!(to_pascal_case("degree-centrality"), "DegreeCentrality");
374        assert_eq!(to_pascal_case("snake_case"), "SnakeCase");
375        assert_eq!(to_pascal_case("mixed-snake_case"), "MixedSnakeCase");
376    }
377
378    #[test]
379    fn test_builder() {
380        let meta = KernelMetadataBuilder::new()
381            .id("test-kernel")
382            .mode(KernelMode::Ring)
383            .domain(Domain::Core)
384            .throughput(50_000)
385            .latency_us(0.5)
386            .build();
387
388        assert_eq!(meta.id, "test-kernel");
389        assert_eq!(meta.mode, KernelMode::Ring);
390        assert_eq!(meta.expected_throughput, 50_000);
391    }
392}