hurray_core/buffer.rs
1//! Buffer protocol types for the Hurray tensor format.
2//!
3//! A **buffer handle** is the unit by which a tensor descriptor references a
4//! contiguous region of memory. This module defines the in-memory representation
5//! of buffer handles, the device tag that identifies where a buffer resides, and
6//! a colocation validator that enforces the spec's rule that all buffers within
7//! a single tensor descriptor must reside on the same device.
8//!
9//! The binary encoding of a buffer handle in the descriptor wire format is
10//! defined in `docs/spec/metadata.md § Buffer Table`. The normative rules
11//! governing alignment, device tags, ownership, and zero-copy invariants are
12//! in `docs/spec/buffer-protocol.md`.
13//!
14//! ## Quick reference
15//!
16//! | Item | Description |
17//! |------|-------------|
18//! | [`DeviceTag`] | Identifies the memory space a buffer resides in |
19//! | [`SyncMode`] | Describes how the producer–consumer memory ordering is established |
20//! | [`BufferHandle`] | Declares a buffer's size, alignment, device, and sync mode |
21//! | [`validate_colocation`] | Checks that a set of handles all share the same device |
22//! | [`MIN_BUFFER_ALIGNMENT`] | 64-byte SIMD minimum for non-empty buffers |
23//! | [`PAGE_ALIGNMENT`] | 4096-byte recommendation for GPU / IPC buffers |
24
25use std::fmt;
26
27use crate::Error;
28
29// ── PrivateTag ────────────────────────────────────────────────────────────────
30
31/// An implementation-private device tag byte, guaranteed to be in `0xF0`–`0xFE`.
32///
33/// Values of this type are constructible only via [`DeviceTag::from_byte`];
34/// direct construction is not possible from outside the crate, preventing
35/// callers from forging an out-of-range private tag.
36///
37/// # Examples
38///
39/// ```
40/// use hurray_core::{DeviceTag, PrivateTag};
41///
42/// let tag = DeviceTag::from_byte(0xF2).unwrap();
43/// if let DeviceTag::Private(pt) = tag {
44/// assert_eq!(pt.byte(), 0xF2);
45/// }
46/// ```
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
48#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
49pub struct PrivateTag(u8);
50
51impl PrivateTag {
52 /// Returns the raw wire byte for this private device tag (`0xF0`–`0xFE`).
53 ///
54 /// # Examples
55 ///
56 /// ```
57 /// use hurray_core::DeviceTag;
58 ///
59 /// let tag = DeviceTag::from_byte(0xF5).unwrap();
60 /// if let DeviceTag::Private(pt) = tag {
61 /// assert_eq!(pt.byte(), 0xF5);
62 /// }
63 /// ```
64 #[inline]
65 pub fn byte(self) -> u8 {
66 self.0
67 }
68}
69
70// ── Alignment constants ───────────────────────────────────────────────────────
71
72/// Minimum buffer alignment for SIMD compatibility (64 bytes).
73///
74/// The base address of every **non-empty** buffer MUST be aligned to at least
75/// this many bytes. This ensures compatibility with all current SIMD instruction
76/// sets (AVX-512, NEON, SVE) without per-operation alignment negotiation.
77///
78/// See `docs/spec/buffer-protocol.md § Minimum Alignment`.
79///
80/// # Examples
81///
82/// ```
83/// use hurray_core::MIN_BUFFER_ALIGNMENT;
84///
85/// assert_eq!(MIN_BUFFER_ALIGNMENT, 64);
86/// ```
87pub const MIN_BUFFER_ALIGNMENT: u32 = 64;
88
89/// Recommended alignment for GPU, IPC, and RDMA buffers (one host page = 4096 bytes).
90///
91/// Buffers shared across process boundaries (IPC) or placed in device memory
92/// (GPU) SHOULD be aligned to at least this value. Writers targeting RDMA MUST
93/// set `alignment` to at least `4096`.
94///
95/// See `docs/spec/buffer-protocol.md § Page Alignment for GPU and IPC`.
96///
97/// # Examples
98///
99/// ```
100/// use hurray_core::PAGE_ALIGNMENT;
101///
102/// assert_eq!(PAGE_ALIGNMENT, 4096);
103/// ```
104pub const PAGE_ALIGNMENT: u32 = 4096;
105
106// ── SyncMode ──────────────────────────────────────────────────────────────────
107
108/// Describes how the producer–consumer memory ordering guarantee is established
109/// for a buffer.
110///
111/// The `sync_mode` field in the binary buffer handle is a single `uint8` at
112/// wire offset 13. This enum is the typed representation of that byte; use
113/// [`SyncMode::from_byte`] to parse and [`SyncMode::to_byte`] to serialize.
114///
115/// CPU buffers (`device_tag == 0x00`) MUST use [`SyncMode::ProducerSynced`];
116/// [`BufferHandle::new`] enforces this and returns [`Error::InvalidSyncMode`]
117/// if any other mode is combined with [`DeviceTag::Cpu`].
118///
119/// | Wire value | Variant |
120/// |------------|---------|
121/// | `0x00` | [`ProducerSynced`][SyncMode::ProducerSynced] |
122/// | `0x01` | [`Event`][SyncMode::Event] |
123/// | `0x02` | [`ConsumerStream`][SyncMode::ConsumerStream] |
124/// | `0x03`–`0xFF` | reserved / permanently invalid → [`Error::InvalidSyncMode`] |
125///
126/// See `docs/spec/buffer-protocol.md § Synchronization Mode` and ADR-018.
127///
128/// # Examples
129///
130/// ```
131/// use hurray_core::{SyncMode, Error};
132///
133/// let mode = SyncMode::from_byte(0x00).unwrap();
134/// assert_eq!(mode, SyncMode::ProducerSynced);
135/// assert_eq!(mode.to_byte(), 0x00);
136/// assert_eq!(mode.to_string(), "producer_synced");
137///
138/// let event = SyncMode::from_byte(0x01).unwrap();
139/// assert_eq!(event, SyncMode::Event);
140///
141/// assert!(matches!(SyncMode::from_byte(0x03), Err(Error::InvalidSyncMode(0x03))));
142/// assert!(matches!(SyncMode::from_byte(0xFF), Err(Error::InvalidSyncMode(0xFF))));
143/// ```
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
145#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
146pub enum SyncMode {
147 /// Producer has issued a host-side wait; consumer may access on any stream.
148 ///
149 /// This is the only valid mode for CPU buffers (`device_tag == 0x00`).
150 /// Wire byte `0x00`.
151 ProducerSynced,
152 /// Producer recorded a device event; consumer must wait on it via the C ABI.
153 ///
154 /// Wire byte `0x01`.
155 Event,
156 /// Consumer declared a target stream; producer ordered it device-side.
157 ///
158 /// Wire byte `0x02`.
159 ConsumerStream,
160}
161
162impl SyncMode {
163 /// Parses a [`SyncMode`] from its one-byte wire representation.
164 ///
165 /// # Errors
166 ///
167 /// - [`Error::InvalidSyncMode`] — byte is `0x03`–`0xFF` (reserved or
168 /// permanently invalid).
169 ///
170 /// # Examples
171 ///
172 /// ```
173 /// use hurray_core::{SyncMode, Error};
174 ///
175 /// assert_eq!(SyncMode::from_byte(0x00).unwrap(), SyncMode::ProducerSynced);
176 /// assert_eq!(SyncMode::from_byte(0x01).unwrap(), SyncMode::Event);
177 /// assert_eq!(SyncMode::from_byte(0x02).unwrap(), SyncMode::ConsumerStream);
178 /// assert!(matches!(SyncMode::from_byte(0x03), Err(Error::InvalidSyncMode(0x03))));
179 /// assert!(matches!(SyncMode::from_byte(0xFE), Err(Error::InvalidSyncMode(0xFE))));
180 /// assert!(matches!(SyncMode::from_byte(0xFF), Err(Error::InvalidSyncMode(0xFF))));
181 /// ```
182 pub fn from_byte(b: u8) -> crate::Result<Self> {
183 match b {
184 0x00 => Ok(Self::ProducerSynced),
185 0x01 => Ok(Self::Event),
186 0x02 => Ok(Self::ConsumerStream),
187 // 0x03–0xFF: all reserved or permanently invalid; reject unconditionally.
188 _ => Err(Error::InvalidSyncMode(b)),
189 }
190 }
191
192 /// Returns the one-byte wire representation of this sync mode.
193 ///
194 /// # Examples
195 ///
196 /// ```
197 /// use hurray_core::SyncMode;
198 ///
199 /// assert_eq!(SyncMode::ProducerSynced.to_byte(), 0x00);
200 /// assert_eq!(SyncMode::Event.to_byte(), 0x01);
201 /// assert_eq!(SyncMode::ConsumerStream.to_byte(), 0x02);
202 /// ```
203 pub fn to_byte(self) -> u8 {
204 match self {
205 Self::ProducerSynced => 0x00,
206 Self::Event => 0x01,
207 Self::ConsumerStream => 0x02,
208 }
209 }
210}
211
212impl fmt::Display for SyncMode {
213 /// Formats the sync mode as a human-readable lowercase string.
214 ///
215 /// # Examples
216 ///
217 /// ```
218 /// use hurray_core::SyncMode;
219 ///
220 /// assert_eq!(SyncMode::ProducerSynced.to_string(), "producer_synced");
221 /// assert_eq!(SyncMode::Event.to_string(), "event");
222 /// assert_eq!(SyncMode::ConsumerStream.to_string(), "consumer_stream");
223 /// ```
224 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225 match self {
226 Self::ProducerSynced => f.write_str("producer_synced"),
227 Self::Event => f.write_str("event"),
228 Self::ConsumerStream => f.write_str("consumer_stream"),
229 }
230 }
231}
232
233// ── DeviceTag ─────────────────────────────────────────────────────────────────
234
235/// Identifies the memory space in which a buffer resides.
236///
237/// The `device_tag` field in the binary buffer handle is a single `uint8`.
238/// This enum is the typed representation of that byte; use [`DeviceTag::from_byte`]
239/// to parse and [`DeviceTag::to_byte`] to serialize.
240///
241/// | Wire value | Variant |
242/// |------------|---------|
243/// | `0x00` | [`Cpu`][DeviceTag::Cpu] |
244/// | `0x01` | [`Cuda`][DeviceTag::Cuda] |
245/// | `0x02` | [`Rocm`][DeviceTag::Rocm] |
246/// | `0x03` | [`Metal`][DeviceTag::Metal] |
247/// | `0x04` | [`Vulkan`][DeviceTag::Vulkan] |
248/// | `0x05` | [`WebGpu`][DeviceTag::WebGpu] |
249/// | `0x06` | [`Hexagon`][DeviceTag::Hexagon] |
250/// | `0x07` | [`LevelZero`][DeviceTag::LevelZero] |
251/// | `0x08` | [`OpenCl`][DeviceTag::OpenCl] |
252/// | `0x09`–`0xEF` | reserved — yields [`Error::ReservedDeviceTag`] |
253/// | `0xF0`–`0xFE` | [`Private(b)`][DeviceTag::Private] |
254/// | `0xFF` | permanently invalid — yields [`Error::InvalidDeviceTag`] |
255///
256/// See `docs/spec/buffer-protocol.md § Device Tags` for the normative table.
257///
258/// # Examples
259///
260/// ```
261/// use hurray_core::DeviceTag;
262///
263/// let tag = DeviceTag::from_byte(0x00).unwrap();
264/// assert_eq!(tag, DeviceTag::Cpu);
265/// assert_eq!(tag.to_byte(), 0x00);
266/// assert_eq!(tag.to_string(), "cpu");
267///
268/// let private = DeviceTag::from_byte(0xF2).unwrap();
269/// assert!(private.is_private());
270/// assert_eq!(private.to_byte(), 0xF2);
271/// ```
272#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
273#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
274pub enum DeviceTag {
275 /// CPU host memory. Wire byte `0x00`.
276 Cpu,
277 /// CUDA device memory. Wire byte `0x01`.
278 Cuda,
279 /// ROCm device memory. Wire byte `0x02`.
280 Rocm,
281 /// Metal device memory (Apple Silicon unified memory). Wire byte `0x03`.
282 Metal,
283 /// Vulkan device memory (cross-vendor GPU API). Wire byte `0x04`.
284 ///
285 /// # Examples
286 ///
287 /// ```
288 /// use hurray_core::DeviceTag;
289 ///
290 /// let tag = DeviceTag::from_byte(0x04).unwrap();
291 /// assert_eq!(tag, DeviceTag::Vulkan);
292 /// assert_eq!(tag.to_byte(), 0x04);
293 /// assert_eq!(tag.to_string(), "vulkan");
294 /// ```
295 Vulkan,
296 /// WebGPU device memory (browser and native WebGPU API). Wire byte `0x05`.
297 ///
298 /// # Examples
299 ///
300 /// ```
301 /// use hurray_core::DeviceTag;
302 ///
303 /// let tag = DeviceTag::from_byte(0x05).unwrap();
304 /// assert_eq!(tag, DeviceTag::WebGpu);
305 /// assert_eq!(tag.to_byte(), 0x05);
306 /// assert_eq!(tag.to_string(), "webgpu");
307 /// ```
308 WebGpu,
309 /// Qualcomm Hexagon DSP memory. Wire byte `0x06`.
310 ///
311 /// # Examples
312 ///
313 /// ```
314 /// use hurray_core::DeviceTag;
315 ///
316 /// let tag = DeviceTag::from_byte(0x06).unwrap();
317 /// assert_eq!(tag, DeviceTag::Hexagon);
318 /// assert_eq!(tag.to_byte(), 0x06);
319 /// assert_eq!(tag.to_string(), "hexagon");
320 /// ```
321 Hexagon,
322 /// Intel oneAPI Level Zero device memory. Wire byte `0x07`.
323 ///
324 /// # Examples
325 ///
326 /// ```
327 /// use hurray_core::DeviceTag;
328 ///
329 /// let tag = DeviceTag::from_byte(0x07).unwrap();
330 /// assert_eq!(tag, DeviceTag::LevelZero);
331 /// assert_eq!(tag.to_byte(), 0x07);
332 /// assert_eq!(tag.to_string(), "level_zero");
333 /// ```
334 LevelZero,
335 /// OpenCL device memory (cross-vendor compute API). Wire byte `0x08`.
336 ///
337 /// # Examples
338 ///
339 /// ```
340 /// use hurray_core::DeviceTag;
341 ///
342 /// let tag = DeviceTag::from_byte(0x08).unwrap();
343 /// assert_eq!(tag, DeviceTag::OpenCl);
344 /// assert_eq!(tag.to_byte(), 0x08);
345 /// assert_eq!(tag.to_string(), "opencl");
346 /// ```
347 OpenCl,
348 /// Implementation-private device type. Wire byte in `0xF0`–`0xFE`.
349 ///
350 /// Descriptors carrying private device tags MUST NOT be exchanged between
351 /// independent implementations unless both parties have agreed on the
352 /// semantics out of band.
353 ///
354 /// Use [`DeviceTag::from_byte`] to construct; direct construction of the
355 /// inner [`PrivateTag`] from outside the crate is not possible.
356 Private(PrivateTag),
357}
358
359impl DeviceTag {
360 /// Parses a [`DeviceTag`] from its one-byte wire representation.
361 ///
362 /// # Errors
363 ///
364 /// - [`Error::InvalidDeviceTag`] — byte is `0xFF` (permanently reserved).
365 /// - [`Error::ReservedDeviceTag`] — byte is in `0x09`–`0xEF` (reserved for
366 /// future specification versions).
367 ///
368 /// # Examples
369 ///
370 /// ```
371 /// use hurray_core::{DeviceTag, Error};
372 ///
373 /// assert_eq!(DeviceTag::from_byte(0x00).unwrap(), DeviceTag::Cpu);
374 /// assert_eq!(DeviceTag::from_byte(0x01).unwrap(), DeviceTag::Cuda);
375 /// assert_eq!(DeviceTag::from_byte(0x02).unwrap(), DeviceTag::Rocm);
376 /// assert_eq!(DeviceTag::from_byte(0x03).unwrap(), DeviceTag::Metal);
377 /// assert_eq!(DeviceTag::from_byte(0x04).unwrap(), DeviceTag::Vulkan);
378 /// assert_eq!(DeviceTag::from_byte(0x05).unwrap(), DeviceTag::WebGpu);
379 /// assert_eq!(DeviceTag::from_byte(0x06).unwrap(), DeviceTag::Hexagon);
380 /// assert_eq!(DeviceTag::from_byte(0x07).unwrap(), DeviceTag::LevelZero);
381 /// assert_eq!(DeviceTag::from_byte(0x08).unwrap(), DeviceTag::OpenCl);
382 /// assert!(DeviceTag::from_byte(0xF0).unwrap().is_private());
383 /// assert_eq!(DeviceTag::from_byte(0xF0).unwrap().to_byte(), 0xF0);
384 /// assert_eq!(DeviceTag::from_byte(0xFE).unwrap().to_byte(), 0xFE);
385 /// assert!(matches!(DeviceTag::from_byte(0x09), Err(Error::ReservedDeviceTag(0x09))));
386 /// assert!(matches!(DeviceTag::from_byte(0xEF), Err(Error::ReservedDeviceTag(0xEF))));
387 /// assert!(matches!(DeviceTag::from_byte(0xFF), Err(Error::InvalidDeviceTag(0xFF))));
388 /// ```
389 pub fn from_byte(b: u8) -> crate::Result<Self> {
390 match b {
391 0x00 => Ok(Self::Cpu),
392 0x01 => Ok(Self::Cuda),
393 0x02 => Ok(Self::Rocm),
394 0x03 => Ok(Self::Metal),
395 0x04 => Ok(Self::Vulkan),
396 0x05 => Ok(Self::WebGpu),
397 0x06 => Ok(Self::Hexagon),
398 0x07 => Ok(Self::LevelZero),
399 0x08 => Ok(Self::OpenCl),
400 0x09..=0xEF => Err(Error::ReservedDeviceTag(b)),
401 0xF0..=0xFE => Ok(Self::Private(PrivateTag(b))),
402 0xFF => Err(Error::InvalidDeviceTag(b)),
403 }
404 }
405
406 /// Returns the one-byte wire representation of this device tag.
407 ///
408 /// # Examples
409 ///
410 /// ```
411 /// use hurray_core::DeviceTag;
412 ///
413 /// assert_eq!(DeviceTag::Cpu.to_byte(), 0x00);
414 /// assert_eq!(DeviceTag::Cuda.to_byte(), 0x01);
415 /// assert_eq!(DeviceTag::Rocm.to_byte(), 0x02);
416 /// assert_eq!(DeviceTag::Metal.to_byte(), 0x03);
417 /// assert_eq!(DeviceTag::Vulkan.to_byte(), 0x04);
418 /// assert_eq!(DeviceTag::WebGpu.to_byte(), 0x05);
419 /// assert_eq!(DeviceTag::Hexagon.to_byte(), 0x06);
420 /// assert_eq!(DeviceTag::LevelZero.to_byte(), 0x07);
421 /// assert_eq!(DeviceTag::OpenCl.to_byte(), 0x08);
422 /// assert_eq!(DeviceTag::from_byte(0xF5).unwrap().to_byte(), 0xF5);
423 /// ```
424 pub fn to_byte(self) -> u8 {
425 match self {
426 Self::Cpu => 0x00,
427 Self::Cuda => 0x01,
428 Self::Rocm => 0x02,
429 Self::Metal => 0x03,
430 Self::Vulkan => 0x04,
431 Self::WebGpu => 0x05,
432 Self::Hexagon => 0x06,
433 Self::LevelZero => 0x07,
434 Self::OpenCl => 0x08,
435 Self::Private(t) => t.0,
436 }
437 }
438
439 /// Returns `true` if this tag is a private/experimental device type
440 /// (`0xF0`–`0xFE`).
441 ///
442 /// Private tags MAY be used by implementations for experimental device
443 /// types but MUST NOT be exchanged between independent implementations
444 /// without an out-of-band agreement.
445 ///
446 /// # Examples
447 ///
448 /// ```
449 /// use hurray_core::DeviceTag;
450 ///
451 /// assert!(DeviceTag::from_byte(0xF0).unwrap().is_private());
452 /// assert!(!DeviceTag::Cpu.is_private());
453 /// assert!(!DeviceTag::Cuda.is_private());
454 /// ```
455 pub fn is_private(self) -> bool {
456 matches!(self, Self::Private(_))
457 }
458}
459
460impl fmt::Display for DeviceTag {
461 /// Formats the device tag as a human-readable lowercase string.
462 ///
463 /// Private tags are formatted as `private(0xNN)` where `NN` is the
464 /// hex wire byte.
465 ///
466 /// # Examples
467 ///
468 /// ```
469 /// use hurray_core::DeviceTag;
470 ///
471 /// assert_eq!(DeviceTag::Cpu.to_string(), "cpu");
472 /// assert_eq!(DeviceTag::Cuda.to_string(), "cuda");
473 /// assert_eq!(DeviceTag::Rocm.to_string(), "rocm");
474 /// assert_eq!(DeviceTag::Metal.to_string(), "metal");
475 /// assert_eq!(DeviceTag::Vulkan.to_string(), "vulkan");
476 /// assert_eq!(DeviceTag::WebGpu.to_string(), "webgpu");
477 /// assert_eq!(DeviceTag::Hexagon.to_string(), "hexagon");
478 /// assert_eq!(DeviceTag::LevelZero.to_string(), "level_zero");
479 /// assert_eq!(DeviceTag::OpenCl.to_string(), "opencl");
480 /// assert_eq!(DeviceTag::from_byte(0xF3).unwrap().to_string(), "private(0xF3)");
481 /// ```
482 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
483 match self {
484 Self::Cpu => f.write_str("cpu"),
485 Self::Cuda => f.write_str("cuda"),
486 Self::Rocm => f.write_str("rocm"),
487 Self::Metal => f.write_str("metal"),
488 Self::Vulkan => f.write_str("vulkan"),
489 Self::WebGpu => f.write_str("webgpu"),
490 Self::Hexagon => f.write_str("hexagon"),
491 Self::LevelZero => f.write_str("level_zero"),
492 Self::OpenCl => f.write_str("opencl"),
493 Self::Private(t) => write!(f, "private(0x{:02X})", t.0),
494 }
495 }
496}
497
498// ── MemoryClass ───────────────────────────────────────────────────────────────
499
500/// An implementation-private memory class byte, guaranteed to be in `0xF0`–`0xFE`.
501///
502/// Constructible only via [`MemoryClass::from_byte`].
503#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
504#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
505pub struct PrivateMemoryClass(u8);
506
507impl PrivateMemoryClass {
508 /// Returns the raw wire byte for this private memory class.
509 ///
510 /// # Examples
511 ///
512 /// ```
513 /// use hurray_core::MemoryClass;
514 ///
515 /// let cls = MemoryClass::from_byte(0xF1).unwrap();
516 /// if let MemoryClass::Private(p) = cls {
517 /// assert_eq!(p.to_byte(), 0xF1);
518 /// }
519 /// ```
520 pub fn to_byte(self) -> u8 {
521 self.0
522 }
523}
524
525/// The memory access class of a buffer handle.
526///
527/// Identifies *how* a buffer is accessible — whether it is device-exclusive or
528/// can be accessed by the CPU and/or peer accelerators without copying.
529///
530/// The `memory_class` field in the binary buffer handle is a single `uint8` at
531/// offset 14. Use [`MemoryClass::from_byte`] to parse and [`MemoryClass::to_byte`]
532/// to serialize. See `docs/spec/buffer-protocol.md § Memory Class` for the
533/// normative definition.
534///
535/// | Value | Variant |
536/// |-------|---------|
537/// | `0x00` | [`Standard`][MemoryClass::Standard] |
538/// | `0x01` | [`HostPinned`][MemoryClass::HostPinned] |
539/// | `0x02` | [`Unified`][MemoryClass::Unified] |
540/// | `0x03` | [`Peer`][MemoryClass::Peer] |
541/// | `0x04`–`0xEF` | reserved → [`Error::ReservedMemoryClass`] |
542/// | `0xF0`–`0xFE` | [`Private(b)`][MemoryClass::Private] |
543/// | `0xFF` | permanently invalid → [`Error::InvalidMemoryClass`] |
544///
545/// # Examples
546///
547/// ```
548/// use hurray_core::{MemoryClass, Error};
549///
550/// assert_eq!(MemoryClass::from_byte(0x00).unwrap(), MemoryClass::Standard);
551/// assert_eq!(MemoryClass::from_byte(0x02).unwrap(), MemoryClass::Unified);
552/// assert!(matches!(MemoryClass::from_byte(0x04), Err(Error::ReservedMemoryClass(0x04))));
553/// assert!(matches!(MemoryClass::from_byte(0xFF), Err(Error::InvalidMemoryClass(0xFF))));
554/// ```
555#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
556#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
557pub enum MemoryClass {
558 /// Device-exclusive memory. Only the primary compute unit of the tagged device
559 /// can access this buffer without a copy. Default for all device types; backward-
560 /// compatible with pre-ADR-020 descriptors whose `_reserved[0]` byte was `0x00`.
561 Standard,
562 /// CPU-accessible, device-mapped. No hardware-managed coherency. Examples:
563 /// `cudaMallocHost`, `hipHostMalloc`, `CL_MEM_ALLOC_HOST_PTR`.
564 HostPinned,
565 /// Hardware-managed unified or coherent memory. CPU and device can access this
566 /// buffer simultaneously; the hardware ensures coherency. Examples:
567 /// `cudaMallocManaged`, ROCm HMM, Metal `MTLStorageModeShared`.
568 Unified,
569 /// Peer-to-peer device memory. Accessible by a set of peer accelerators agreed
570 /// out of band (NVLink, xGMI, PCIe BAR). Not CPU-accessible without a copy.
571 Peer,
572 /// Implementation-private memory class (`0xF0`–`0xFE`). Semantics agreed out of band.
573 Private(PrivateMemoryClass),
574}
575
576impl MemoryClass {
577 /// Parses a [`MemoryClass`] from its one-byte wire representation.
578 ///
579 /// # Errors
580 ///
581 /// - [`Error::ReservedMemoryClass`] — byte is `0x04`–`0xEF`.
582 /// - [`Error::InvalidMemoryClass`] — byte is `0xFF`.
583 ///
584 /// # Examples
585 ///
586 /// ```
587 /// use hurray_core::{MemoryClass, Error};
588 ///
589 /// assert_eq!(MemoryClass::from_byte(0x00).unwrap(), MemoryClass::Standard);
590 /// assert_eq!(MemoryClass::from_byte(0x01).unwrap(), MemoryClass::HostPinned);
591 /// assert_eq!(MemoryClass::from_byte(0x02).unwrap(), MemoryClass::Unified);
592 /// assert_eq!(MemoryClass::from_byte(0x03).unwrap(), MemoryClass::Peer);
593 /// assert!(matches!(MemoryClass::from_byte(0x04), Err(Error::ReservedMemoryClass(0x04))));
594 /// assert!(matches!(MemoryClass::from_byte(0xEF), Err(Error::ReservedMemoryClass(0xEF))));
595 /// assert!(matches!(MemoryClass::from_byte(0xFF), Err(Error::InvalidMemoryClass(0xFF))));
596 /// ```
597 pub fn from_byte(b: u8) -> crate::Result<Self> {
598 match b {
599 0x00 => Ok(Self::Standard),
600 0x01 => Ok(Self::HostPinned),
601 0x02 => Ok(Self::Unified),
602 0x03 => Ok(Self::Peer),
603 0x04..=0xEF => Err(Error::ReservedMemoryClass(b)),
604 0xF0..=0xFE => Ok(Self::Private(PrivateMemoryClass(b))),
605 0xFF => Err(Error::InvalidMemoryClass(b)),
606 }
607 }
608
609 /// Returns the one-byte wire representation of this memory class.
610 ///
611 /// # Examples
612 ///
613 /// ```
614 /// use hurray_core::MemoryClass;
615 ///
616 /// assert_eq!(MemoryClass::Standard.to_byte(), 0x00);
617 /// assert_eq!(MemoryClass::HostPinned.to_byte(), 0x01);
618 /// assert_eq!(MemoryClass::Unified.to_byte(), 0x02);
619 /// assert_eq!(MemoryClass::Peer.to_byte(), 0x03);
620 /// ```
621 pub fn to_byte(self) -> u8 {
622 match self {
623 Self::Standard => 0x00,
624 Self::HostPinned => 0x01,
625 Self::Unified => 0x02,
626 Self::Peer => 0x03,
627 Self::Private(p) => p.0,
628 }
629 }
630
631 /// Returns `true` if this is an implementation-private memory class (`0xF0`–`0xFE`).
632 ///
633 /// # Examples
634 ///
635 /// ```
636 /// use hurray_core::MemoryClass;
637 ///
638 /// assert!(!MemoryClass::Standard.is_private());
639 /// assert!(MemoryClass::from_byte(0xF0).unwrap().is_private());
640 /// ```
641 pub fn is_private(self) -> bool {
642 matches!(self, Self::Private(_))
643 }
644}
645
646impl fmt::Display for MemoryClass {
647 /// # Examples
648 ///
649 /// ```
650 /// use hurray_core::MemoryClass;
651 ///
652 /// assert_eq!(MemoryClass::Standard.to_string(), "standard");
653 /// assert_eq!(MemoryClass::Unified.to_string(), "unified");
654 /// ```
655 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
656 match self {
657 Self::Standard => f.write_str("standard"),
658 Self::HostPinned => f.write_str("host_pinned"),
659 Self::Unified => f.write_str("unified"),
660 Self::Peer => f.write_str("peer"),
661 Self::Private(p) => write!(f, "private(0x{:02X})", p.0),
662 }
663 }
664}
665
666// ── BufferHandle ──────────────────────────────────────────────────────────────
667
668/// A declaration of a buffer's size, alignment, device location, and sync mode.
669///
670/// A `BufferHandle` is the in-memory representation of the 16-byte buffer
671/// handle entry in the tensor descriptor's buffer table (see
672/// `docs/spec/metadata.md § Buffer Table`). It carries the metadata needed to
673/// locate and access a buffer but does not itself hold a pointer — the actual
674/// memory address is communicated out-of-band via the interchange protocol or
675/// the C ABI (see `docs/impl/c-ffi.md`).
676///
677/// ## Wire layout (ADR-018 § 3, ADR-020)
678///
679/// | Offset | Field | Type | Size |
680/// |--------|-------|------|------|
681/// | 0 | `byte_size` | uint64 LE | 8 |
682/// | 8 | `alignment` | uint32 LE | 4 |
683/// | 12 | `device_tag` | uint8 | 1 |
684/// | 13 | `sync_mode` | uint8 | 1 |
685/// | 14 | `memory_class` | uint8 | 1 |
686/// | 15 | `_reserved` | uint8 | 1 |
687///
688/// # Alignment rules
689///
690/// - `alignment` MUST be a power of two.
691/// - For **non-empty** buffers (`byte_size > 0`): `alignment` MUST be at least
692/// [`MIN_BUFFER_ALIGNMENT`] (64 bytes).
693/// - For **empty** buffers (`byte_size == 0`): any power-of-two alignment is
694/// valid, including `1`. A reader MUST NOT dereference the pointer of an empty
695/// buffer.
696///
697/// See `docs/spec/buffer-protocol.md § Alignment` for the normative rules.
698///
699/// # Examples
700///
701/// ```
702/// use hurray_core::{BufferHandle, DeviceTag, MemoryClass, SyncMode, MIN_BUFFER_ALIGNMENT};
703///
704/// // Non-empty CPU buffer, minimum SIMD alignment, default Standard class.
705/// let handle = BufferHandle::new(1024, MIN_BUFFER_ALIGNMENT, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap();
706/// assert_eq!(handle.byte_size(), 1024);
707/// assert_eq!(handle.alignment(), 64);
708/// assert_eq!(handle.device_tag(), DeviceTag::Cpu);
709/// assert_eq!(handle.sync_mode(), SyncMode::ProducerSynced);
710/// assert_eq!(handle.memory_class(), MemoryClass::Standard);
711/// assert!(!handle.is_empty());
712///
713/// // CUDA buffer with Unified memory class.
714/// let unified = BufferHandle::with_memory_class(
715/// 4096, 4096, DeviceTag::Cuda, SyncMode::ProducerSynced, MemoryClass::Unified,
716/// ).unwrap();
717/// assert_eq!(unified.memory_class(), MemoryClass::Unified);
718///
719/// // Empty buffer — alignment 1 is valid.
720/// let empty = BufferHandle::empty(DeviceTag::Cuda);
721/// assert!(empty.is_empty());
722/// assert_eq!(empty.alignment(), 1);
723/// ```
724#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
725#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
726pub struct BufferHandle {
727 byte_size: u64,
728 alignment: u32,
729 device_tag: DeviceTag,
730 sync_mode: SyncMode,
731 memory_class: MemoryClass,
732}
733
734impl BufferHandle {
735 /// Creates a new [`BufferHandle`] with the given size, alignment, device, and sync mode.
736 ///
737 /// # Errors
738 ///
739 /// - [`Error::AlignmentNotPowerOfTwo`] — `alignment` is not a power of two.
740 /// - [`Error::AlignmentBelowMinimum`] — `byte_size > 0` and `alignment` is
741 /// less than [`MIN_BUFFER_ALIGNMENT`] (64).
742 /// - [`Error::InvalidSyncMode`] — `device_tag` is [`DeviceTag::Cpu`] and
743 /// `sync_mode` is not [`SyncMode::ProducerSynced`] (CPU buffers MUST use
744 /// `SYNC_PRODUCER_SYNCED` per the spec).
745 ///
746 /// # Examples
747 ///
748 /// ```
749 /// use hurray_core::{BufferHandle, DeviceTag, Error, SyncMode, MIN_BUFFER_ALIGNMENT};
750 ///
751 /// // Valid: non-empty CPU buffer with minimum SIMD alignment.
752 /// assert!(BufferHandle::new(512, 64, DeviceTag::Cpu, SyncMode::ProducerSynced).is_ok());
753 ///
754 /// // Valid: CUDA buffer with Event sync.
755 /// assert!(BufferHandle::new(512, 64, DeviceTag::Cuda, SyncMode::Event).is_ok());
756 ///
757 /// // Valid: empty buffer with alignment 1.
758 /// assert!(BufferHandle::new(0, 1, DeviceTag::Cpu, SyncMode::ProducerSynced).is_ok());
759 ///
760 /// // Error: alignment is not a power of two.
761 /// assert!(matches!(
762 /// BufferHandle::new(512, 3, DeviceTag::Cpu, SyncMode::ProducerSynced),
763 /// Err(Error::AlignmentNotPowerOfTwo { alignment: 3 })
764 /// ));
765 ///
766 /// // Error: non-empty buffer with alignment below the 64-byte minimum.
767 /// assert!(matches!(
768 /// BufferHandle::new(512, 32, DeviceTag::Cpu, SyncMode::ProducerSynced),
769 /// Err(Error::AlignmentBelowMinimum { alignment: 32, minimum: 64 })
770 /// ));
771 ///
772 /// // Error: CPU buffer with non-ProducerSynced mode.
773 /// assert!(matches!(
774 /// BufferHandle::new(512, 64, DeviceTag::Cpu, SyncMode::Event),
775 /// Err(Error::InvalidSyncMode(0x01))
776 /// ));
777 /// ```
778 pub fn new(
779 byte_size: u64,
780 alignment: u32,
781 device_tag: DeviceTag,
782 sync_mode: SyncMode,
783 ) -> crate::Result<Self> {
784 Self::with_memory_class(
785 byte_size,
786 alignment,
787 device_tag,
788 sync_mode,
789 MemoryClass::Standard,
790 )
791 }
792
793 /// Creates a new [`BufferHandle`] with an explicit memory class.
794 ///
795 /// Identical to [`BufferHandle::new`] but accepts a [`MemoryClass`] value.
796 /// Use this constructor when the buffer's memory class is not [`MemoryClass::Standard`]
797 /// (e.g., CUDA managed memory, Metal shared storage, or peer-to-peer memory).
798 ///
799 /// # Errors
800 ///
801 /// - [`Error::AlignmentNotPowerOfTwo`] — `alignment` is not a power of two.
802 /// - [`Error::AlignmentBelowMinimum`] — `byte_size > 0` and `alignment < 64`.
803 /// - [`Error::InvalidSyncMode`] — `device_tag` is [`DeviceTag::Cpu`] and
804 /// `sync_mode` is not [`SyncMode::ProducerSynced`].
805 ///
806 /// # Examples
807 ///
808 /// ```
809 /// use hurray_core::{BufferHandle, DeviceTag, MemoryClass, SyncMode};
810 ///
811 /// // CUDA unified (managed) memory buffer.
812 /// let handle = BufferHandle::with_memory_class(
813 /// 4096, 4096, DeviceTag::Cuda, SyncMode::ProducerSynced, MemoryClass::Unified,
814 /// ).unwrap();
815 /// assert_eq!(handle.memory_class(), MemoryClass::Unified);
816 ///
817 /// // CPU host-pinned buffer.
818 /// let pinned = BufferHandle::with_memory_class(
819 /// 512, 64, DeviceTag::Cpu, SyncMode::ProducerSynced, MemoryClass::HostPinned,
820 /// ).unwrap();
821 /// assert_eq!(pinned.memory_class(), MemoryClass::HostPinned);
822 /// ```
823 pub fn with_memory_class(
824 byte_size: u64,
825 alignment: u32,
826 device_tag: DeviceTag,
827 sync_mode: SyncMode,
828 memory_class: MemoryClass,
829 ) -> crate::Result<Self> {
830 if !alignment.is_power_of_two() {
831 return Err(Error::AlignmentNotPowerOfTwo { alignment });
832 }
833 if byte_size > 0 && alignment < MIN_BUFFER_ALIGNMENT {
834 return Err(Error::AlignmentBelowMinimum {
835 alignment,
836 minimum: MIN_BUFFER_ALIGNMENT,
837 });
838 }
839 // CPU buffers must use ProducerSynced: no device-side sync primitives exist.
840 if device_tag == DeviceTag::Cpu && sync_mode != SyncMode::ProducerSynced {
841 return Err(Error::InvalidSyncMode(sync_mode.to_byte()));
842 }
843 Ok(Self {
844 byte_size,
845 alignment,
846 device_tag,
847 sync_mode,
848 memory_class,
849 })
850 }
851
852 /// Creates an **empty** [`BufferHandle`] (zero bytes) on the given device.
853 ///
854 /// The alignment is set to `1` — the minimum valid power-of-two for an
855 /// empty buffer — and `sync_mode` is always [`SyncMode::ProducerSynced`].
856 /// This constructor is infallible.
857 ///
858 /// # Examples
859 ///
860 /// ```
861 /// use hurray_core::{BufferHandle, DeviceTag, SyncMode};
862 ///
863 /// let handle = BufferHandle::empty(DeviceTag::Cpu);
864 /// assert!(handle.is_empty());
865 /// assert_eq!(handle.byte_size(), 0);
866 /// assert_eq!(handle.alignment(), 1);
867 /// assert_eq!(handle.device_tag(), DeviceTag::Cpu);
868 /// assert_eq!(handle.sync_mode(), SyncMode::ProducerSynced);
869 /// ```
870 pub fn empty(device_tag: DeviceTag) -> Self {
871 // ProducerSynced is the safest universal default: empty buffers carry no
872 // data and no device-side synchronisation is required.
873 Self {
874 byte_size: 0,
875 alignment: 1,
876 device_tag,
877 sync_mode: SyncMode::ProducerSynced,
878 memory_class: MemoryClass::Standard,
879 }
880 }
881
882 /// Returns the size of the buffer in bytes.
883 ///
884 /// A value of `0` denotes an empty buffer whose backing pointer MUST NOT
885 /// be dereferenced.
886 ///
887 /// # Examples
888 ///
889 /// ```
890 /// use hurray_core::{BufferHandle, DeviceTag, SyncMode};
891 ///
892 /// let handle = BufferHandle::new(4096, 4096, DeviceTag::Cuda, SyncMode::Event).unwrap();
893 /// assert_eq!(handle.byte_size(), 4096);
894 /// ```
895 pub fn byte_size(self) -> u64 {
896 self.byte_size
897 }
898
899 /// Returns the minimum alignment of the buffer's base address in bytes.
900 ///
901 /// Always a power of two. For non-empty buffers, always at least
902 /// [`MIN_BUFFER_ALIGNMENT`].
903 ///
904 /// # Examples
905 ///
906 /// ```
907 /// use hurray_core::{BufferHandle, DeviceTag, SyncMode, PAGE_ALIGNMENT};
908 ///
909 /// let handle = BufferHandle::new(8192, PAGE_ALIGNMENT, DeviceTag::Cuda, SyncMode::Event).unwrap();
910 /// assert_eq!(handle.alignment(), 4096);
911 /// ```
912 pub fn alignment(self) -> u32 {
913 self.alignment
914 }
915
916 /// Returns the [`DeviceTag`] identifying the memory space this buffer resides in.
917 ///
918 /// # Examples
919 ///
920 /// ```
921 /// use hurray_core::{BufferHandle, DeviceTag, SyncMode};
922 ///
923 /// let handle = BufferHandle::new(256, 64, DeviceTag::Metal, SyncMode::Event).unwrap();
924 /// assert_eq!(handle.device_tag(), DeviceTag::Metal);
925 /// ```
926 pub fn device_tag(self) -> DeviceTag {
927 self.device_tag
928 }
929
930 /// Returns the [`SyncMode`] describing the producer–consumer ordering guarantee
931 /// for this buffer.
932 ///
933 /// # Examples
934 ///
935 /// ```
936 /// use hurray_core::{BufferHandle, DeviceTag, SyncMode};
937 ///
938 /// let handle = BufferHandle::new(1024, 64, DeviceTag::Cuda, SyncMode::ConsumerStream).unwrap();
939 /// assert_eq!(handle.sync_mode(), SyncMode::ConsumerStream);
940 ///
941 /// // CPU buffers are always ProducerSynced.
942 /// let cpu = BufferHandle::new(1024, 64, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap();
943 /// assert_eq!(cpu.sync_mode(), SyncMode::ProducerSynced);
944 /// ```
945 pub fn sync_mode(self) -> SyncMode {
946 self.sync_mode
947 }
948
949 /// Returns the [`MemoryClass`] describing how this buffer is accessible.
950 ///
951 /// # Examples
952 ///
953 /// ```
954 /// use hurray_core::{BufferHandle, DeviceTag, MemoryClass, SyncMode};
955 ///
956 /// // new() defaults to Standard.
957 /// let handle = BufferHandle::new(1024, 64, DeviceTag::Cuda, SyncMode::ProducerSynced).unwrap();
958 /// assert_eq!(handle.memory_class(), MemoryClass::Standard);
959 ///
960 /// // with_memory_class() sets an explicit class.
961 /// let unified = BufferHandle::with_memory_class(
962 /// 1024, 64, DeviceTag::Cuda, SyncMode::ProducerSynced, MemoryClass::Unified,
963 /// ).unwrap();
964 /// assert_eq!(unified.memory_class(), MemoryClass::Unified);
965 /// ```
966 pub fn memory_class(self) -> MemoryClass {
967 self.memory_class
968 }
969
970 /// Returns `true` if this buffer has zero bytes (`byte_size == 0`).
971 ///
972 /// Readers MUST NOT dereference the pointer of an empty buffer. In C ABI
973 /// contexts an empty buffer MAY be represented by a null pointer.
974 ///
975 /// # Examples
976 ///
977 /// ```
978 /// use hurray_core::{BufferHandle, DeviceTag, SyncMode};
979 ///
980 /// assert!(BufferHandle::empty(DeviceTag::Cpu).is_empty());
981 /// assert!(!BufferHandle::new(1, 64, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap().is_empty());
982 /// ```
983 pub fn is_empty(self) -> bool {
984 self.byte_size == 0
985 }
986}
987
988// ── validate_colocation ───────────────────────────────────────────────────────
989
990/// Checks that all buffer handles in `handles` share the same [`DeviceTag`] and [`MemoryClass`].
991///
992/// All buffers referenced by a single tensor descriptor — the data buffer plus
993/// all quantization-parameter buffers — MUST share the same `device_tag` AND the
994/// same `memory_class` (see `docs/spec/buffer-protocol.md § Device Colocation`).
995///
996/// Returns the common [`DeviceTag`] on success.
997///
998/// # Errors
999///
1000/// - [`Error::EmptyBufferList`] — `handles` is empty.
1001/// - [`Error::DeviceTagMismatch`] — two or more handles carry different device tags.
1002/// - [`Error::MemoryClassMismatch`] — two or more handles carry different memory classes.
1003///
1004/// # Examples
1005///
1006/// ```
1007/// use hurray_core::{BufferHandle, DeviceTag, Error, MemoryClass, SyncMode, validate_colocation};
1008///
1009/// // All handles on CPU, all Standard — succeeds.
1010/// let handles = [
1011/// BufferHandle::new(1024, 64, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap(),
1012/// BufferHandle::new(256, 64, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap(),
1013/// ];
1014/// assert_eq!(validate_colocation(&handles).unwrap(), DeviceTag::Cpu);
1015///
1016/// // Empty slice — error.
1017/// assert!(matches!(validate_colocation(&[]), Err(Error::EmptyBufferList)));
1018///
1019/// // Mixed devices — error.
1020/// let mixed_device = [
1021/// BufferHandle::new(1024, 64, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap(),
1022/// BufferHandle::new(256, 64, DeviceTag::Cuda, SyncMode::ProducerSynced).unwrap(),
1023/// ];
1024/// assert!(matches!(
1025/// validate_colocation(&mixed_device),
1026/// Err(Error::DeviceTagMismatch { expected: 0x00, found: 0x01 })
1027/// ));
1028///
1029/// // Mixed memory classes — error.
1030/// let mixed_class = [
1031/// BufferHandle::new(1024, 64, DeviceTag::Cuda, SyncMode::ProducerSynced).unwrap(),
1032/// BufferHandle::with_memory_class(256, 64, DeviceTag::Cuda, SyncMode::ProducerSynced, MemoryClass::Unified).unwrap(),
1033/// ];
1034/// assert!(matches!(
1035/// validate_colocation(&mixed_class),
1036/// Err(Error::MemoryClassMismatch { expected: 0x00, found: 0x02 })
1037/// ));
1038/// ```
1039pub fn validate_colocation(handles: &[BufferHandle]) -> crate::Result<DeviceTag> {
1040 let first = handles.first().ok_or(Error::EmptyBufferList)?;
1041 let expected_tag = first.device_tag;
1042 let expected_tag_byte = expected_tag.to_byte();
1043 let expected_class = first.memory_class;
1044 let expected_class_byte = expected_class.to_byte();
1045
1046 for handle in handles.iter().skip(1) {
1047 if handle.device_tag != expected_tag {
1048 return Err(Error::DeviceTagMismatch {
1049 expected: expected_tag_byte,
1050 found: handle.device_tag.to_byte(),
1051 });
1052 }
1053 if handle.memory_class != expected_class {
1054 return Err(Error::MemoryClassMismatch {
1055 expected: expected_class_byte,
1056 found: handle.memory_class.to_byte(),
1057 });
1058 }
1059 }
1060
1061 Ok(expected_tag)
1062}
1063
1064// ── Tests ─────────────────────────────────────────────────────────────────────
1065
1066#[cfg(test)]
1067mod tests {
1068 use super::*;
1069
1070 // ── DeviceTag::from_byte — named variants ────────────────────────────────
1071
1072 /// Spec § buffer-protocol.md Device Tags: 0x00 decodes to Cpu.
1073 #[test]
1074 fn from_byte_cpu() {
1075 assert_eq!(DeviceTag::from_byte(0x00).unwrap(), DeviceTag::Cpu);
1076 }
1077
1078 /// Spec § buffer-protocol.md Device Tags: 0x01 decodes to Cuda.
1079 #[test]
1080 fn from_byte_cuda() {
1081 assert_eq!(DeviceTag::from_byte(0x01).unwrap(), DeviceTag::Cuda);
1082 }
1083
1084 /// Spec § buffer-protocol.md Device Tags: 0x02 decodes to Rocm.
1085 #[test]
1086 fn from_byte_rocm() {
1087 assert_eq!(DeviceTag::from_byte(0x02).unwrap(), DeviceTag::Rocm);
1088 }
1089
1090 /// Spec § buffer-protocol.md Device Tags: 0x03 decodes to Metal.
1091 #[test]
1092 fn from_byte_metal() {
1093 assert_eq!(DeviceTag::from_byte(0x03).unwrap(), DeviceTag::Metal);
1094 }
1095
1096 // ── DeviceTag::from_byte — reserved range 0x09–0xEF ─────────────────────
1097
1098 /// Spec § buffer-protocol.md Device Tags: 0x09 (lower bound of reserved range
1099 /// after ADR-016 assigned 0x04–0x08) must return ReservedDeviceTag.
1100 #[test]
1101 fn from_byte_reserved_lower_bound() {
1102 assert!(matches!(
1103 DeviceTag::from_byte(0x09),
1104 Err(Error::ReservedDeviceTag(0x09))
1105 ));
1106 }
1107
1108 /// Spec § buffer-protocol.md Device Tags: 0xEF (upper bound of reserved range)
1109 /// must return ReservedDeviceTag.
1110 #[test]
1111 fn from_byte_reserved_upper_bound() {
1112 assert!(matches!(
1113 DeviceTag::from_byte(0xEF),
1114 Err(Error::ReservedDeviceTag(0xEF))
1115 ));
1116 }
1117
1118 /// Spot-check mid-reserved byte 0x80 returns ReservedDeviceTag.
1119 #[test]
1120 fn from_byte_reserved_mid_range() {
1121 assert!(matches!(
1122 DeviceTag::from_byte(0x80),
1123 Err(Error::ReservedDeviceTag(0x80))
1124 ));
1125 }
1126
1127 // ── DeviceTag::from_byte — private range 0xF0–0xFE ──────────────────────
1128
1129 /// Spec § buffer-protocol.md Device Tags: 0xF0 (lower bound of private range)
1130 /// must decode to Private(0xF0).
1131 #[test]
1132 fn from_byte_private_lower_bound() {
1133 let tag = DeviceTag::from_byte(0xF0).unwrap();
1134 assert!(tag.is_private());
1135 assert_eq!(tag.to_byte(), 0xF0);
1136 }
1137
1138 /// Spec § buffer-protocol.md Device Tags: 0xFE (upper bound of private range)
1139 /// must decode to Private(0xFE).
1140 #[test]
1141 fn from_byte_private_upper_bound() {
1142 let tag = DeviceTag::from_byte(0xFE).unwrap();
1143 assert!(tag.is_private());
1144 assert_eq!(tag.to_byte(), 0xFE);
1145 }
1146
1147 // ── DeviceTag::from_byte — permanently invalid sentinel 0xFF ────────────
1148
1149 /// Spec § buffer-protocol.md Device Tags: 0xFF is permanently reserved and
1150 /// MUST be rejected with InvalidDeviceTag.
1151 #[test]
1152 fn from_byte_invalid_sentinel() {
1153 assert!(matches!(
1154 DeviceTag::from_byte(0xFF),
1155 Err(Error::InvalidDeviceTag(0xFF))
1156 ));
1157 }
1158
1159 // ── DeviceTag::to_byte — round-trip for named variants ──────────────────
1160
1161 /// Each named variant must serialize back to its documented wire byte.
1162 #[test]
1163 fn to_byte_cpu() {
1164 assert_eq!(DeviceTag::Cpu.to_byte(), 0x00);
1165 }
1166
1167 #[test]
1168 fn to_byte_cuda() {
1169 assert_eq!(DeviceTag::Cuda.to_byte(), 0x01);
1170 }
1171
1172 #[test]
1173 fn to_byte_rocm() {
1174 assert_eq!(DeviceTag::Rocm.to_byte(), 0x02);
1175 }
1176
1177 #[test]
1178 fn to_byte_metal() {
1179 assert_eq!(DeviceTag::Metal.to_byte(), 0x03);
1180 }
1181
1182 /// Private(b) must serialize to exactly b.
1183 #[test]
1184 fn to_byte_private() {
1185 assert_eq!(DeviceTag::from_byte(0xF2).unwrap().to_byte(), 0xF2);
1186 }
1187
1188 // ── DeviceTag round-trip: from_byte → to_byte ────────────────────────────
1189
1190 /// For every valid byte (named variants + private range), from_byte then
1191 /// to_byte must be the identity.
1192 #[test]
1193 fn round_trip_named_variants() {
1194 for b in [0x00u8, 0x01, 0x02, 0x03] {
1195 let tag = DeviceTag::from_byte(b).unwrap();
1196 assert_eq!(tag.to_byte(), b, "round-trip failed for byte 0x{b:02X}");
1197 }
1198 }
1199
1200 #[test]
1201 fn round_trip_private_range() {
1202 for b in 0xF0u8..=0xFE {
1203 let tag = DeviceTag::from_byte(b).unwrap();
1204 assert_eq!(tag.to_byte(), b, "round-trip failed for byte 0x{b:02X}");
1205 }
1206 }
1207
1208 // ── DeviceTag::is_private ────────────────────────────────────────────────
1209
1210 #[test]
1211 fn is_private_true_for_private_variant() {
1212 assert!(DeviceTag::from_byte(0xF0).unwrap().is_private());
1213 }
1214
1215 #[test]
1216 fn is_private_false_for_cpu() {
1217 assert!(!DeviceTag::Cpu.is_private());
1218 }
1219
1220 #[test]
1221 fn is_private_false_for_cuda() {
1222 assert!(!DeviceTag::Cuda.is_private());
1223 }
1224
1225 #[test]
1226 fn is_private_false_for_rocm() {
1227 assert!(!DeviceTag::Rocm.is_private());
1228 }
1229
1230 #[test]
1231 fn is_private_false_for_metal() {
1232 assert!(!DeviceTag::Metal.is_private());
1233 }
1234
1235 // ── DeviceTag Display ────────────────────────────────────────────────────
1236
1237 /// Spec § buffer-protocol.md Display: named variants use lowercase ASCII.
1238 #[test]
1239 fn display_cpu() {
1240 assert_eq!(DeviceTag::Cpu.to_string(), "cpu");
1241 }
1242
1243 #[test]
1244 fn display_cuda() {
1245 assert_eq!(DeviceTag::Cuda.to_string(), "cuda");
1246 }
1247
1248 #[test]
1249 fn display_rocm() {
1250 assert_eq!(DeviceTag::Rocm.to_string(), "rocm");
1251 }
1252
1253 #[test]
1254 fn display_metal() {
1255 assert_eq!(DeviceTag::Metal.to_string(), "metal");
1256 }
1257
1258 /// Private tags display as "private(0xNN)" with uppercase hex digits.
1259 #[test]
1260 fn display_private() {
1261 assert_eq!(
1262 DeviceTag::from_byte(0xF1).unwrap().to_string(),
1263 "private(0xF1)"
1264 );
1265 }
1266
1267 #[test]
1268 fn display_private_lower_bound() {
1269 assert_eq!(
1270 DeviceTag::from_byte(0xF0).unwrap().to_string(),
1271 "private(0xF0)"
1272 );
1273 }
1274
1275 #[test]
1276 fn display_private_upper_bound() {
1277 assert_eq!(
1278 DeviceTag::from_byte(0xFE).unwrap().to_string(),
1279 "private(0xFE)"
1280 );
1281 }
1282
1283 // ── MIN_BUFFER_ALIGNMENT and PAGE_ALIGNMENT constants ───────────────────
1284
1285 /// Spec § buffer-protocol.md Minimum Alignment: SIMD minimum is 64 bytes.
1286 #[test]
1287 fn min_buffer_alignment_is_64() {
1288 assert_eq!(MIN_BUFFER_ALIGNMENT, 64);
1289 }
1290
1291 /// Spec § buffer-protocol.md Page Alignment: page-aligned value is 4096 bytes.
1292 #[test]
1293 fn page_alignment_is_4096() {
1294 assert_eq!(PAGE_ALIGNMENT, 4096);
1295 }
1296
1297 /// Both constants must be powers of two (required by the alignment contract).
1298 #[test]
1299 fn min_buffer_alignment_is_power_of_two() {
1300 assert!(MIN_BUFFER_ALIGNMENT.is_power_of_two());
1301 }
1302
1303 #[test]
1304 fn page_alignment_is_power_of_two() {
1305 assert!(PAGE_ALIGNMENT.is_power_of_two());
1306 }
1307
1308 // ── BufferHandle::new — success cases ────────────────────────────────────
1309
1310 /// Spec § buffer-protocol.md Alignment: non-empty buffer with minimum SIMD
1311 /// alignment (64) on CPU is valid.
1312 #[test]
1313 fn new_nonempty_cpu_min_alignment() {
1314 let result = BufferHandle::new(1024, 64, DeviceTag::Cpu, SyncMode::ProducerSynced);
1315 assert!(result.is_ok());
1316 }
1317
1318 /// A non-empty CUDA buffer at page alignment is valid.
1319 #[test]
1320 fn new_nonempty_cuda_page_alignment() {
1321 let result = BufferHandle::new(4096, 4096, DeviceTag::Cuda, SyncMode::Event);
1322 assert!(result.is_ok());
1323 }
1324
1325 /// Spec § buffer-protocol.md Alignment: an empty buffer (byte_size == 0)
1326 /// may have any power-of-two alignment, including 1.
1327 #[test]
1328 fn new_empty_alignment_1_is_valid() {
1329 let result = BufferHandle::new(0, 1, DeviceTag::Cpu, SyncMode::ProducerSynced);
1330 assert!(result.is_ok());
1331 }
1332
1333 /// An empty buffer with the SIMD minimum alignment is also valid.
1334 #[test]
1335 fn new_empty_alignment_64_is_valid() {
1336 let result = BufferHandle::new(0, 64, DeviceTag::Cpu, SyncMode::ProducerSynced);
1337 assert!(result.is_ok());
1338 }
1339
1340 // ── BufferHandle::new — failure cases ────────────────────────────────────
1341
1342 /// alignment=0 is not a power of two; must return AlignmentNotPowerOfTwo.
1343 #[test]
1344 fn new_alignment_zero_not_power_of_two() {
1345 assert!(matches!(
1346 BufferHandle::new(1024, 0, DeviceTag::Cpu, SyncMode::ProducerSynced),
1347 Err(Error::AlignmentNotPowerOfTwo { alignment: 0 })
1348 ));
1349 }
1350
1351 /// alignment=63 is not a power of two; must return AlignmentNotPowerOfTwo.
1352 #[test]
1353 fn new_alignment_63_not_power_of_two() {
1354 assert!(matches!(
1355 BufferHandle::new(1024, 63, DeviceTag::Cpu, SyncMode::ProducerSynced),
1356 Err(Error::AlignmentNotPowerOfTwo { alignment: 63 })
1357 ));
1358 }
1359
1360 /// alignment=100 is not a power of two; must return AlignmentNotPowerOfTwo.
1361 #[test]
1362 fn new_alignment_100_not_power_of_two() {
1363 assert!(matches!(
1364 BufferHandle::new(1024, 100, DeviceTag::Cpu, SyncMode::ProducerSynced),
1365 Err(Error::AlignmentNotPowerOfTwo { alignment: 100 })
1366 ));
1367 }
1368
1369 /// Non-empty buffer with alignment=32 (power of two but below MIN_BUFFER_ALIGNMENT)
1370 /// must return AlignmentBelowMinimum.
1371 #[test]
1372 fn new_nonempty_alignment_below_minimum_32() {
1373 assert!(matches!(
1374 BufferHandle::new(1, 32, DeviceTag::Cpu, SyncMode::ProducerSynced),
1375 Err(Error::AlignmentBelowMinimum {
1376 alignment: 32,
1377 minimum: 64
1378 })
1379 ));
1380 }
1381
1382 /// Non-empty buffer with alignment=1 (power of two but well below MIN_BUFFER_ALIGNMENT)
1383 /// must return AlignmentBelowMinimum.
1384 #[test]
1385 fn new_nonempty_alignment_below_minimum_1() {
1386 assert!(matches!(
1387 BufferHandle::new(1, 1, DeviceTag::Cpu, SyncMode::ProducerSynced),
1388 Err(Error::AlignmentBelowMinimum {
1389 alignment: 1,
1390 minimum: 64
1391 })
1392 ));
1393 }
1394
1395 /// ADR-018: CPU buffer with non-ProducerSynced mode must return InvalidSyncMode.
1396 #[test]
1397 fn new_cpu_with_event_sync_rejected() {
1398 assert!(matches!(
1399 BufferHandle::new(1024, 64, DeviceTag::Cpu, SyncMode::Event),
1400 Err(Error::InvalidSyncMode(0x01))
1401 ));
1402 }
1403
1404 /// ADR-018: CPU buffer with ConsumerStream must return InvalidSyncMode.
1405 #[test]
1406 fn new_cpu_with_consumer_stream_rejected() {
1407 assert!(matches!(
1408 BufferHandle::new(1024, 64, DeviceTag::Cpu, SyncMode::ConsumerStream),
1409 Err(Error::InvalidSyncMode(0x02))
1410 ));
1411 }
1412
1413 // ── BufferHandle::empty ──────────────────────────────────────────────────
1414
1415 /// BufferHandle::empty must produce a zero-sized handle with alignment 1.
1416 #[test]
1417 fn empty_byte_size_is_zero() {
1418 assert_eq!(BufferHandle::empty(DeviceTag::Cpu).byte_size(), 0);
1419 }
1420
1421 #[test]
1422 fn empty_alignment_is_one() {
1423 assert_eq!(BufferHandle::empty(DeviceTag::Cpu).alignment(), 1);
1424 }
1425
1426 #[test]
1427 fn empty_is_empty_returns_true() {
1428 assert!(BufferHandle::empty(DeviceTag::Cpu).is_empty());
1429 }
1430
1431 /// The device tag passed to empty() must be preserved.
1432 #[test]
1433 fn empty_preserves_device_tag_cpu() {
1434 assert_eq!(
1435 BufferHandle::empty(DeviceTag::Cpu).device_tag(),
1436 DeviceTag::Cpu
1437 );
1438 }
1439
1440 #[test]
1441 fn empty_preserves_device_tag_cuda() {
1442 assert_eq!(
1443 BufferHandle::empty(DeviceTag::Cuda).device_tag(),
1444 DeviceTag::Cuda
1445 );
1446 }
1447
1448 #[test]
1449 fn empty_preserves_device_tag_private() {
1450 let private = DeviceTag::from_byte(0xF5).unwrap();
1451 assert_eq!(BufferHandle::empty(private).device_tag(), private);
1452 }
1453
1454 // ── BufferHandle accessors ───────────────────────────────────────────────
1455
1456 /// byte_size(), alignment(), device_tag(), sync_mode(), and is_empty() must
1457 /// return the values that were passed to new().
1458 #[test]
1459 fn accessors_byte_size() {
1460 let handle = BufferHandle::new(8192, 4096, DeviceTag::Cuda, SyncMode::Event).unwrap();
1461 assert_eq!(handle.byte_size(), 8192);
1462 }
1463
1464 #[test]
1465 fn accessors_alignment() {
1466 let handle = BufferHandle::new(256, 256, DeviceTag::Metal, SyncMode::Event).unwrap();
1467 assert_eq!(handle.alignment(), 256);
1468 }
1469
1470 #[test]
1471 fn accessors_device_tag() {
1472 let handle = BufferHandle::new(512, 64, DeviceTag::Rocm, SyncMode::ConsumerStream).unwrap();
1473 assert_eq!(handle.device_tag(), DeviceTag::Rocm);
1474 }
1475
1476 #[test]
1477 fn accessors_sync_mode_producer_synced() {
1478 let handle = BufferHandle::new(512, 64, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap();
1479 assert_eq!(handle.sync_mode(), SyncMode::ProducerSynced);
1480 }
1481
1482 #[test]
1483 fn accessors_sync_mode_event() {
1484 let handle = BufferHandle::new(512, 64, DeviceTag::Cuda, SyncMode::Event).unwrap();
1485 assert_eq!(handle.sync_mode(), SyncMode::Event);
1486 }
1487
1488 #[test]
1489 fn accessors_sync_mode_consumer_stream() {
1490 let handle = BufferHandle::new(512, 64, DeviceTag::Cuda, SyncMode::ConsumerStream).unwrap();
1491 assert_eq!(handle.sync_mode(), SyncMode::ConsumerStream);
1492 }
1493
1494 #[test]
1495 fn accessors_is_empty_false_for_nonempty() {
1496 let handle = BufferHandle::new(1, 64, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap();
1497 assert!(!handle.is_empty());
1498 }
1499
1500 #[test]
1501 fn accessors_is_empty_true_for_zero_size() {
1502 let handle = BufferHandle::new(0, 1, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap();
1503 assert!(handle.is_empty());
1504 }
1505
1506 // ── validate_colocation ──────────────────────────────────────────────────
1507
1508 /// Empty slice must return EmptyBufferList.
1509 #[test]
1510 fn colocation_empty_slice_returns_error() {
1511 assert!(matches!(
1512 validate_colocation(&[]),
1513 Err(Error::EmptyBufferList)
1514 ));
1515 }
1516
1517 /// Single-element slice must succeed and return the handle's device tag.
1518 #[test]
1519 fn colocation_single_handle_returns_its_tag() {
1520 let handle = BufferHandle::new(1024, 64, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap();
1521 let result = validate_colocation(&[handle]);
1522 assert_eq!(result.unwrap(), DeviceTag::Cpu);
1523 }
1524
1525 /// All-CPU slice of three handles must succeed and return Cpu.
1526 #[test]
1527 fn colocation_all_cpu_three_handles() {
1528 let handles = [
1529 BufferHandle::new(1024, 64, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap(),
1530 BufferHandle::new(512, 64, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap(),
1531 BufferHandle::new(256, 64, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap(),
1532 ];
1533 assert_eq!(validate_colocation(&handles).unwrap(), DeviceTag::Cpu);
1534 }
1535
1536 /// Mixed Cpu + Cuda must return DeviceTagMismatch with correct wire bytes.
1537 #[test]
1538 fn colocation_mixed_cpu_cuda_returns_mismatch() {
1539 let handles = [
1540 BufferHandle::new(1024, 64, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap(),
1541 BufferHandle::new(256, 64, DeviceTag::Cuda, SyncMode::ProducerSynced).unwrap(),
1542 ];
1543 assert!(matches!(
1544 validate_colocation(&handles),
1545 Err(Error::DeviceTagMismatch {
1546 expected: 0x00,
1547 found: 0x01
1548 })
1549 ));
1550 }
1551
1552 /// All-Private(0xF0) slice must succeed and return Private(0xF0).
1553 #[test]
1554 fn colocation_all_private_same_tag() {
1555 let private = DeviceTag::from_byte(0xF0).unwrap();
1556 let handles = [
1557 BufferHandle::new(1024, 64, private, SyncMode::ProducerSynced).unwrap(),
1558 BufferHandle::new(512, 64, private, SyncMode::ProducerSynced).unwrap(),
1559 ];
1560 assert_eq!(validate_colocation(&handles).unwrap(), private);
1561 }
1562
1563 /// Mixed named + private must return DeviceTagMismatch.
1564 #[test]
1565 fn colocation_named_and_private_returns_mismatch() {
1566 let private = DeviceTag::from_byte(0xF0).unwrap();
1567 let handles = [
1568 BufferHandle::new(1024, 64, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap(),
1569 BufferHandle::new(512, 64, private, SyncMode::ProducerSynced).unwrap(),
1570 ];
1571 assert!(matches!(
1572 validate_colocation(&handles),
1573 Err(Error::DeviceTagMismatch {
1574 expected: 0x00,
1575 found: 0xF0
1576 })
1577 ));
1578 }
1579
1580 /// colocation validates the first mismatch position: 2nd handle agrees,
1581 /// 3rd handle disagrees. Error captures the first vs. third wire bytes.
1582 #[test]
1583 fn colocation_mismatch_at_third_element() {
1584 let handles = [
1585 BufferHandle::new(1024, 64, DeviceTag::Cuda, SyncMode::Event).unwrap(),
1586 BufferHandle::new(512, 64, DeviceTag::Cuda, SyncMode::Event).unwrap(),
1587 BufferHandle::new(256, 64, DeviceTag::Metal, SyncMode::Event).unwrap(),
1588 ];
1589 assert!(matches!(
1590 validate_colocation(&handles),
1591 Err(Error::DeviceTagMismatch {
1592 expected: 0x01,
1593 found: 0x03
1594 })
1595 ));
1596 }
1597
1598 // ── SyncMode ──────────────────────────────────────────────────────────────
1599
1600 mod sync_mode {
1601 use super::*;
1602 use std::collections::HashSet;
1603
1604 // ── from_byte — named values ─────────────────────────────────────────
1605
1606 /// Spec § buffer-protocol.md Sync Mode: 0x00 decodes to ProducerSynced.
1607 #[test]
1608 fn from_byte_producer_synced() {
1609 assert_eq!(SyncMode::from_byte(0x00).unwrap(), SyncMode::ProducerSynced);
1610 }
1611
1612 /// Spec § buffer-protocol.md Sync Mode: 0x01 decodes to Event.
1613 #[test]
1614 fn from_byte_event() {
1615 assert_eq!(SyncMode::from_byte(0x01).unwrap(), SyncMode::Event);
1616 }
1617
1618 /// Spec § buffer-protocol.md Sync Mode: 0x02 decodes to ConsumerStream.
1619 #[test]
1620 fn from_byte_consumer_stream() {
1621 assert_eq!(SyncMode::from_byte(0x02).unwrap(), SyncMode::ConsumerStream);
1622 }
1623
1624 // ── from_byte — reserved / invalid bytes ─────────────────────────────
1625
1626 /// Spec § buffer-protocol.md Sync Mode: 0x03 (first reserved) must
1627 /// return InvalidSyncMode(0x03).
1628 #[test]
1629 fn from_byte_0x03_is_invalid() {
1630 assert!(matches!(
1631 SyncMode::from_byte(0x03),
1632 Err(Error::InvalidSyncMode(0x03))
1633 ));
1634 }
1635
1636 /// Spec § buffer-protocol.md Sync Mode: 0xFE (reserved) must return
1637 /// InvalidSyncMode(0xFE).
1638 #[test]
1639 fn from_byte_0xfe_is_invalid() {
1640 assert!(matches!(
1641 SyncMode::from_byte(0xFE),
1642 Err(Error::InvalidSyncMode(0xFE))
1643 ));
1644 }
1645
1646 /// Spec § buffer-protocol.md Sync Mode: 0xFF (reserved) must return
1647 /// InvalidSyncMode(0xFF).
1648 #[test]
1649 fn from_byte_0xff_is_invalid() {
1650 assert!(matches!(
1651 SyncMode::from_byte(0xFF),
1652 Err(Error::InvalidSyncMode(0xFF))
1653 ));
1654 }
1655
1656 // ── to_byte — wire byte values ────────────────────────────────────────
1657
1658 /// ProducerSynced serializes to wire byte 0x00.
1659 #[test]
1660 fn to_byte_producer_synced() {
1661 assert_eq!(SyncMode::ProducerSynced.to_byte(), 0x00);
1662 }
1663
1664 /// Event serializes to wire byte 0x01.
1665 #[test]
1666 fn to_byte_event() {
1667 assert_eq!(SyncMode::Event.to_byte(), 0x01);
1668 }
1669
1670 /// ConsumerStream serializes to wire byte 0x02.
1671 #[test]
1672 fn to_byte_consumer_stream() {
1673 assert_eq!(SyncMode::ConsumerStream.to_byte(), 0x02);
1674 }
1675
1676 // ── from_byte → to_byte identity ─────────────────────────────────────
1677
1678 /// For each named value, from_byte(m.to_byte()) == m.
1679 #[test]
1680 fn from_byte_to_byte_identity_producer_synced() {
1681 let m = SyncMode::ProducerSynced;
1682 assert_eq!(SyncMode::from_byte(m.to_byte()).unwrap(), m);
1683 }
1684
1685 #[test]
1686 fn from_byte_to_byte_identity_event() {
1687 let m = SyncMode::Event;
1688 assert_eq!(SyncMode::from_byte(m.to_byte()).unwrap(), m);
1689 }
1690
1691 #[test]
1692 fn from_byte_to_byte_identity_consumer_stream() {
1693 let m = SyncMode::ConsumerStream;
1694 assert_eq!(SyncMode::from_byte(m.to_byte()).unwrap(), m);
1695 }
1696
1697 // ── Display ───────────────────────────────────────────────────────────
1698
1699 /// Spec § buffer-protocol.md Display: ProducerSynced displays as
1700 /// "producer_synced".
1701 #[test]
1702 fn display_producer_synced() {
1703 assert_eq!(SyncMode::ProducerSynced.to_string(), "producer_synced");
1704 }
1705
1706 /// Event displays as "event".
1707 #[test]
1708 fn display_event() {
1709 assert_eq!(SyncMode::Event.to_string(), "event");
1710 }
1711
1712 /// ConsumerStream displays as "consumer_stream".
1713 #[test]
1714 fn display_consumer_stream() {
1715 assert_eq!(SyncMode::ConsumerStream.to_string(), "consumer_stream");
1716 }
1717
1718 // ── Clone, Copy, PartialEq, Eq, Hash smoke tests ─────────────────────
1719
1720 /// Clone produces an equal value for each variant.
1721 #[test]
1722 fn clone_equals_original() {
1723 for m in [
1724 SyncMode::ProducerSynced,
1725 SyncMode::Event,
1726 SyncMode::ConsumerStream,
1727 ] {
1728 assert_eq!(m, m.clone());
1729 }
1730 }
1731
1732 /// Copy: assigning to a new binding leaves the original usable (Copy
1733 /// semantics verified by using both after the assignment).
1734 #[test]
1735 fn copy_semantics() {
1736 let original = SyncMode::Event;
1737 let copied = original;
1738 assert_eq!(original, copied);
1739 }
1740
1741 /// PartialEq: distinct variants are not equal.
1742 #[test]
1743 fn partial_eq_distinct_variants() {
1744 assert_ne!(SyncMode::ProducerSynced, SyncMode::Event);
1745 assert_ne!(SyncMode::ProducerSynced, SyncMode::ConsumerStream);
1746 assert_ne!(SyncMode::Event, SyncMode::ConsumerStream);
1747 }
1748
1749 /// Hash: all three variants produce distinct hashes (smoke test — hash
1750 /// collisions are possible in theory but the stdlib hasher avoids them
1751 /// for small integers).
1752 #[test]
1753 fn hash_all_variants_distinct() {
1754 let set: HashSet<SyncMode> = [
1755 SyncMode::ProducerSynced,
1756 SyncMode::Event,
1757 SyncMode::ConsumerStream,
1758 ]
1759 .into_iter()
1760 .collect();
1761 assert_eq!(set.len(), 3);
1762 }
1763 }
1764
1765 // ── DeviceTag new variants (ADR-016) ──────────────────────────────────────
1766
1767 mod device_tag_new_variants {
1768 use super::*;
1769
1770 // ── Vulkan (0x04) ────────────────────────────────────────────────────
1771
1772 /// ADR-016: 0x04 decodes to Vulkan.
1773 #[test]
1774 fn vulkan_from_byte() {
1775 assert_eq!(DeviceTag::from_byte(0x04).unwrap(), DeviceTag::Vulkan);
1776 }
1777
1778 /// Vulkan serializes to wire byte 0x04.
1779 #[test]
1780 fn vulkan_to_byte() {
1781 assert_eq!(DeviceTag::Vulkan.to_byte(), 0x04);
1782 }
1783
1784 /// from_byte(0x04) → to_byte() == 0x04.
1785 #[test]
1786 fn vulkan_round_trip() {
1787 let tag = DeviceTag::from_byte(0x04).unwrap();
1788 assert_eq!(tag.to_byte(), 0x04);
1789 }
1790
1791 /// Vulkan displays as "vulkan".
1792 #[test]
1793 fn vulkan_display() {
1794 assert_eq!(DeviceTag::Vulkan.to_string(), "vulkan");
1795 }
1796
1797 /// Vulkan is not a private tag.
1798 #[test]
1799 fn vulkan_is_not_private() {
1800 assert!(!DeviceTag::Vulkan.is_private());
1801 }
1802
1803 // ── WebGpu (0x05) ────────────────────────────────────────────────────
1804
1805 /// ADR-016: 0x05 decodes to WebGpu.
1806 #[test]
1807 fn webgpu_from_byte() {
1808 assert_eq!(DeviceTag::from_byte(0x05).unwrap(), DeviceTag::WebGpu);
1809 }
1810
1811 /// WebGpu serializes to wire byte 0x05.
1812 #[test]
1813 fn webgpu_to_byte() {
1814 assert_eq!(DeviceTag::WebGpu.to_byte(), 0x05);
1815 }
1816
1817 /// from_byte(0x05) → to_byte() == 0x05.
1818 #[test]
1819 fn webgpu_round_trip() {
1820 let tag = DeviceTag::from_byte(0x05).unwrap();
1821 assert_eq!(tag.to_byte(), 0x05);
1822 }
1823
1824 /// WebGpu displays as "webgpu".
1825 #[test]
1826 fn webgpu_display() {
1827 assert_eq!(DeviceTag::WebGpu.to_string(), "webgpu");
1828 }
1829
1830 /// WebGpu is not a private tag.
1831 #[test]
1832 fn webgpu_is_not_private() {
1833 assert!(!DeviceTag::WebGpu.is_private());
1834 }
1835
1836 // ── Hexagon (0x06) ───────────────────────────────────────────────────
1837
1838 /// ADR-016: 0x06 decodes to Hexagon.
1839 #[test]
1840 fn hexagon_from_byte() {
1841 assert_eq!(DeviceTag::from_byte(0x06).unwrap(), DeviceTag::Hexagon);
1842 }
1843
1844 /// Hexagon serializes to wire byte 0x06.
1845 #[test]
1846 fn hexagon_to_byte() {
1847 assert_eq!(DeviceTag::Hexagon.to_byte(), 0x06);
1848 }
1849
1850 /// from_byte(0x06) → to_byte() == 0x06.
1851 #[test]
1852 fn hexagon_round_trip() {
1853 let tag = DeviceTag::from_byte(0x06).unwrap();
1854 assert_eq!(tag.to_byte(), 0x06);
1855 }
1856
1857 /// Hexagon displays as "hexagon".
1858 #[test]
1859 fn hexagon_display() {
1860 assert_eq!(DeviceTag::Hexagon.to_string(), "hexagon");
1861 }
1862
1863 /// Hexagon is not a private tag.
1864 #[test]
1865 fn hexagon_is_not_private() {
1866 assert!(!DeviceTag::Hexagon.is_private());
1867 }
1868
1869 // ── LevelZero (0x07) ─────────────────────────────────────────────────
1870
1871 /// ADR-016: 0x07 decodes to LevelZero.
1872 #[test]
1873 fn level_zero_from_byte() {
1874 assert_eq!(DeviceTag::from_byte(0x07).unwrap(), DeviceTag::LevelZero);
1875 }
1876
1877 /// LevelZero serializes to wire byte 0x07.
1878 #[test]
1879 fn level_zero_to_byte() {
1880 assert_eq!(DeviceTag::LevelZero.to_byte(), 0x07);
1881 }
1882
1883 /// from_byte(0x07) → to_byte() == 0x07.
1884 #[test]
1885 fn level_zero_round_trip() {
1886 let tag = DeviceTag::from_byte(0x07).unwrap();
1887 assert_eq!(tag.to_byte(), 0x07);
1888 }
1889
1890 /// LevelZero displays as "level_zero".
1891 #[test]
1892 fn level_zero_display() {
1893 assert_eq!(DeviceTag::LevelZero.to_string(), "level_zero");
1894 }
1895
1896 /// LevelZero is not a private tag.
1897 #[test]
1898 fn level_zero_is_not_private() {
1899 assert!(!DeviceTag::LevelZero.is_private());
1900 }
1901
1902 // ── OpenCl (0x08) ────────────────────────────────────────────────────
1903
1904 /// ADR-016: 0x08 decodes to OpenCl.
1905 #[test]
1906 fn opencl_from_byte() {
1907 assert_eq!(DeviceTag::from_byte(0x08).unwrap(), DeviceTag::OpenCl);
1908 }
1909
1910 /// OpenCl serializes to wire byte 0x08.
1911 #[test]
1912 fn opencl_to_byte() {
1913 assert_eq!(DeviceTag::OpenCl.to_byte(), 0x08);
1914 }
1915
1916 /// from_byte(0x08) → to_byte() == 0x08.
1917 #[test]
1918 fn opencl_round_trip() {
1919 let tag = DeviceTag::from_byte(0x08).unwrap();
1920 assert_eq!(tag.to_byte(), 0x08);
1921 }
1922
1923 /// OpenCl displays as "opencl".
1924 #[test]
1925 fn opencl_display() {
1926 assert_eq!(DeviceTag::OpenCl.to_string(), "opencl");
1927 }
1928
1929 /// OpenCl is not a private tag.
1930 #[test]
1931 fn opencl_is_not_private() {
1932 assert!(!DeviceTag::OpenCl.is_private());
1933 }
1934
1935 // ── Edge cases ───────────────────────────────────────────────────────
1936
1937 /// 0x08 is the last named tag; it must succeed.
1938 #[test]
1939 fn from_byte_0x08_is_last_named_tag() {
1940 assert!(DeviceTag::from_byte(0x08).is_ok());
1941 }
1942
1943 /// 0x09 is the first reserved byte after the new range; must return
1944 /// ReservedDeviceTag(0x09).
1945 #[test]
1946 fn from_byte_0x09_is_first_reserved_after_new_range() {
1947 assert!(matches!(
1948 DeviceTag::from_byte(0x09),
1949 Err(Error::ReservedDeviceTag(0x09))
1950 ));
1951 }
1952
1953 /// The full new range 0x04–0x08 all round-trip cleanly.
1954 #[test]
1955 fn round_trip_new_range_all_bytes() {
1956 for b in 0x04u8..=0x08 {
1957 let tag = DeviceTag::from_byte(b)
1958 .unwrap_or_else(|e| panic!("from_byte(0x{b:02X}) failed: {e}"));
1959 assert_eq!(tag.to_byte(), b, "round-trip failed for byte 0x{b:02X}");
1960 }
1961 }
1962 }
1963
1964 // ── MemoryClass ───────────────────────────────────────────────────────────
1965
1966 mod memory_class {
1967 use super::*;
1968
1969 /// Spec § buffer-protocol.md Memory Class: 0x00 decodes to Standard.
1970 #[test]
1971 fn from_byte_standard() {
1972 assert_eq!(MemoryClass::from_byte(0x00).unwrap(), MemoryClass::Standard);
1973 }
1974
1975 /// Spec § buffer-protocol.md Memory Class: 0x01 decodes to HostPinned.
1976 #[test]
1977 fn from_byte_host_pinned() {
1978 assert_eq!(
1979 MemoryClass::from_byte(0x01).unwrap(),
1980 MemoryClass::HostPinned
1981 );
1982 }
1983
1984 /// Spec § buffer-protocol.md Memory Class: 0x02 decodes to Unified.
1985 #[test]
1986 fn from_byte_unified() {
1987 assert_eq!(MemoryClass::from_byte(0x02).unwrap(), MemoryClass::Unified);
1988 }
1989
1990 /// Spec § buffer-protocol.md Memory Class: 0x03 decodes to Peer.
1991 #[test]
1992 fn from_byte_peer() {
1993 assert_eq!(MemoryClass::from_byte(0x03).unwrap(), MemoryClass::Peer);
1994 }
1995
1996 /// 0x04 is the first reserved byte; must return ReservedMemoryClass.
1997 #[test]
1998 fn from_byte_reserved_lower_bound() {
1999 assert!(matches!(
2000 MemoryClass::from_byte(0x04),
2001 Err(Error::ReservedMemoryClass(0x04))
2002 ));
2003 }
2004
2005 /// 0xEF is the upper bound of the reserved range.
2006 #[test]
2007 fn from_byte_reserved_upper_bound() {
2008 assert!(matches!(
2009 MemoryClass::from_byte(0xEF),
2010 Err(Error::ReservedMemoryClass(0xEF))
2011 ));
2012 }
2013
2014 /// 0xF0 (lower bound of private range) decodes to Private(0xF0).
2015 #[test]
2016 fn from_byte_private_lower_bound() {
2017 let cls = MemoryClass::from_byte(0xF0).unwrap();
2018 assert!(cls.is_private());
2019 assert_eq!(cls.to_byte(), 0xF0);
2020 }
2021
2022 /// 0xFE (upper bound of private range) decodes to Private(0xFE).
2023 #[test]
2024 fn from_byte_private_upper_bound() {
2025 let cls = MemoryClass::from_byte(0xFE).unwrap();
2026 assert!(cls.is_private());
2027 assert_eq!(cls.to_byte(), 0xFE);
2028 }
2029
2030 /// 0xFF is permanently reserved; must return InvalidMemoryClass.
2031 #[test]
2032 fn from_byte_invalid_sentinel() {
2033 assert!(matches!(
2034 MemoryClass::from_byte(0xFF),
2035 Err(Error::InvalidMemoryClass(0xFF))
2036 ));
2037 }
2038
2039 /// All four named variants round-trip through to_byte → from_byte.
2040 #[test]
2041 fn named_variants_round_trip() {
2042 for cls in [
2043 MemoryClass::Standard,
2044 MemoryClass::HostPinned,
2045 MemoryClass::Unified,
2046 MemoryClass::Peer,
2047 ] {
2048 assert_eq!(MemoryClass::from_byte(cls.to_byte()).unwrap(), cls);
2049 }
2050 }
2051
2052 /// BufferHandle::new() defaults to MemoryClass::Standard.
2053 #[test]
2054 fn buffer_handle_new_defaults_to_standard() {
2055 let h = BufferHandle::new(64, 64, DeviceTag::Cuda, SyncMode::ProducerSynced).unwrap();
2056 assert_eq!(h.memory_class(), MemoryClass::Standard);
2057 }
2058
2059 /// BufferHandle::with_memory_class() stores the given class.
2060 #[test]
2061 fn buffer_handle_with_memory_class_unified() {
2062 let h = BufferHandle::with_memory_class(
2063 64,
2064 64,
2065 DeviceTag::Cuda,
2066 SyncMode::ProducerSynced,
2067 MemoryClass::Unified,
2068 )
2069 .unwrap();
2070 assert_eq!(h.memory_class(), MemoryClass::Unified);
2071 }
2072
2073 /// BufferHandle::empty() always uses Standard.
2074 #[test]
2075 fn empty_handle_is_standard() {
2076 assert_eq!(
2077 BufferHandle::empty(DeviceTag::Cpu).memory_class(),
2078 MemoryClass::Standard
2079 );
2080 }
2081
2082 /// validate_colocation rejects mixed memory classes.
2083 #[test]
2084 fn validate_colocation_rejects_mixed_memory_class() {
2085 let standard =
2086 BufferHandle::new(64, 64, DeviceTag::Cuda, SyncMode::ProducerSynced).unwrap();
2087 let unified = BufferHandle::with_memory_class(
2088 64,
2089 64,
2090 DeviceTag::Cuda,
2091 SyncMode::ProducerSynced,
2092 MemoryClass::Unified,
2093 )
2094 .unwrap();
2095 assert!(matches!(
2096 validate_colocation(&[standard, unified]),
2097 Err(Error::MemoryClassMismatch {
2098 expected: 0x00,
2099 found: 0x02
2100 })
2101 ));
2102 }
2103
2104 /// validate_colocation accepts uniform memory class.
2105 #[test]
2106 fn validate_colocation_accepts_uniform_memory_class() {
2107 let a = BufferHandle::with_memory_class(
2108 64,
2109 64,
2110 DeviceTag::Cuda,
2111 SyncMode::ProducerSynced,
2112 MemoryClass::Unified,
2113 )
2114 .unwrap();
2115 let b = BufferHandle::with_memory_class(
2116 128,
2117 64,
2118 DeviceTag::Cuda,
2119 SyncMode::ProducerSynced,
2120 MemoryClass::Unified,
2121 )
2122 .unwrap();
2123 assert_eq!(validate_colocation(&[a, b]).unwrap(), DeviceTag::Cuda);
2124 }
2125 }
2126
2127 // ── BufferHandle + SyncMode integration ───────────────────────────────────
2128
2129 mod buffer_handle_sync_mode {
2130 use super::*;
2131 use crate::descriptor::TensorDescriptor;
2132 use crate::layout::LayoutDescriptor;
2133 use crate::{ElementType, Shape, MIN_BUFFER_ALIGNMENT};
2134
2135 // ── Non-CPU buffers accept Event and ConsumerStream ───────────────────
2136
2137 /// A non-CPU buffer with SyncMode::Event must be accepted and the
2138 /// sync_mode() accessor must return Event.
2139 #[test]
2140 fn new_with_event_sync_non_cpu_buffer() {
2141 let handle =
2142 BufferHandle::new(512, MIN_BUFFER_ALIGNMENT, DeviceTag::Cuda, SyncMode::Event)
2143 .unwrap();
2144 assert_eq!(handle.sync_mode(), SyncMode::Event);
2145 assert_eq!(handle.device_tag(), DeviceTag::Cuda);
2146 }
2147
2148 /// A non-CPU buffer with SyncMode::ConsumerStream must be accepted and
2149 /// the sync_mode() accessor must return ConsumerStream.
2150 #[test]
2151 fn new_with_consumer_stream_sync_non_cpu_buffer() {
2152 let handle = BufferHandle::new(
2153 512,
2154 MIN_BUFFER_ALIGNMENT,
2155 DeviceTag::Rocm,
2156 SyncMode::ConsumerStream,
2157 )
2158 .unwrap();
2159 assert_eq!(handle.sync_mode(), SyncMode::ConsumerStream);
2160 assert_eq!(handle.device_tag(), DeviceTag::Rocm);
2161 }
2162
2163 // ── CPU buffers must reject non-ProducerSynced modes ─────────────────
2164
2165 /// ADR-018: CPU buffer with SyncMode::Event must be rejected with
2166 /// InvalidSyncMode(0x01).
2167 #[test]
2168 fn new_cpu_rejects_event() {
2169 let result =
2170 BufferHandle::new(512, MIN_BUFFER_ALIGNMENT, DeviceTag::Cpu, SyncMode::Event);
2171 assert!(matches!(result, Err(Error::InvalidSyncMode(0x01))));
2172 }
2173
2174 /// ADR-018: CPU buffer with SyncMode::ConsumerStream must be rejected
2175 /// with InvalidSyncMode(0x02).
2176 #[test]
2177 fn new_cpu_rejects_consumer_stream() {
2178 let result = BufferHandle::new(
2179 512,
2180 MIN_BUFFER_ALIGNMENT,
2181 DeviceTag::Cpu,
2182 SyncMode::ConsumerStream,
2183 );
2184 assert!(matches!(result, Err(Error::InvalidSyncMode(0x02))));
2185 }
2186
2187 /// ADR-018: CPU buffer with SyncMode::ProducerSynced must succeed.
2188 #[test]
2189 fn new_cpu_allows_producer_synced() {
2190 let result = BufferHandle::new(
2191 512,
2192 MIN_BUFFER_ALIGNMENT,
2193 DeviceTag::Cpu,
2194 SyncMode::ProducerSynced,
2195 );
2196 assert!(result.is_ok());
2197 }
2198
2199 // ── BufferHandle::empty always uses ProducerSynced ────────────────────
2200
2201 /// BufferHandle::empty always sets sync_mode to ProducerSynced,
2202 /// even for non-CPU devices.
2203 #[test]
2204 fn empty_has_producer_synced() {
2205 assert_eq!(
2206 BufferHandle::empty(DeviceTag::Cuda).sync_mode(),
2207 SyncMode::ProducerSynced
2208 );
2209 }
2210
2211 // ── encode/decode round-trip preserves sync_mode ─────────────────────
2212
2213 /// A TensorDescriptor containing a buffer with SyncMode::Event must
2214 /// survive encode → decode with sync_mode unchanged.
2215 #[test]
2216 fn encode_decode_preserves_sync_mode_event() {
2217 let shape = Shape::new(vec![4u64]).unwrap();
2218 let buffer =
2219 BufferHandle::new(64, MIN_BUFFER_ALIGNMENT, DeviceTag::Cuda, SyncMode::Event)
2220 .unwrap();
2221 let desc = TensorDescriptor::new(
2222 1,
2223 0,
2224 ElementType::Float32,
2225 shape,
2226 0,
2227 LayoutDescriptor::RowMajor,
2228 vec![buffer],
2229 None,
2230 None,
2231 None,
2232 None,
2233 )
2234 .unwrap();
2235
2236 let encoded = desc.encode().unwrap();
2237 let decoded = TensorDescriptor::decode(&encoded).unwrap();
2238
2239 assert_eq!(decoded.buffers[0].sync_mode(), SyncMode::Event);
2240 assert_eq!(decoded.buffers[0].device_tag(), DeviceTag::Cuda);
2241 assert_eq!(decoded, desc);
2242 }
2243
2244 /// A TensorDescriptor containing a buffer with SyncMode::ConsumerStream
2245 /// must survive encode → decode with sync_mode unchanged.
2246 #[test]
2247 fn encode_decode_preserves_sync_mode_consumer_stream() {
2248 let shape = Shape::new(vec![8u64]).unwrap();
2249 let buffer = BufferHandle::new(
2250 128,
2251 MIN_BUFFER_ALIGNMENT,
2252 DeviceTag::Vulkan,
2253 SyncMode::ConsumerStream,
2254 )
2255 .unwrap();
2256 let desc = TensorDescriptor::new(
2257 1,
2258 0,
2259 ElementType::Float32,
2260 shape,
2261 0,
2262 LayoutDescriptor::RowMajor,
2263 vec![buffer],
2264 None,
2265 None,
2266 None,
2267 None,
2268 )
2269 .unwrap();
2270
2271 let encoded = desc.encode().unwrap();
2272 let decoded = TensorDescriptor::decode(&encoded).unwrap();
2273
2274 assert_eq!(decoded.buffers[0].sync_mode(), SyncMode::ConsumerStream);
2275 assert_eq!(decoded.buffers[0].device_tag(), DeviceTag::Vulkan);
2276 assert_eq!(decoded, desc);
2277 }
2278 }
2279}