hurray_ffi/field/mod.rs
1//! Descriptor field reads for the Hurray C ABI (ADR-039, ADR-040).
2//!
3//! [`hurray_descriptor_decode`](crate::hurray_descriptor_decode) validates every
4//! layout and every optional section `hurray-core` supports, but the scalar
5//! accessors beside it stop at the fixed header. A C caller handed a CSR
6//! descriptor would learn `layout_tag == 0x07` and nothing more — not `nnz`, not
7//! which buffer is `row_ptr` — and one handed a per-block-quantized tensor would
8//! learn nothing at all about the scheme. This module is what closes that gap.
9//!
10//! ## Five functions, not seventy
11//!
12//! The layout payloads and the five optional sections hold about seventy fields
13//! between them. Rather than one accessor each, a field is named by a
14//! [`HurrayDescriptorField`] identifier and read through one of five typed
15//! functions:
16//!
17//! | Function | Reads |
18//! |---|---|
19//! | [`hurray_descriptor_field_u64`] | every unsigned or boolean scalar |
20//! | [`hurray_descriptor_field_i64`] | signed scalars (`zero_point`) |
21//! | [`hurray_descriptor_field_f64`] | real scalars (`scale`, the statistics) |
22//! | [`hurray_descriptor_field_u64_array`] | `tile_shape`, `morton_bits`, `mode_order`, `parent_shape`, `shard_offset` |
23//! | [`hurray_descriptor_field_i64_array`] | `strides`, `outer_strides`, `inner_strides` |
24//!
25//! A new layout or section adds identifier constants, not symbols — which is how
26//! the wire format itself evolves (ADR-019), applied to the ABI. The trade is
27//! that a C compiler cannot check a field against the sub-object in hand; a
28//! mismatch is [`HURRAY_ERR_FIELD_NOT_APPLICABLE`] at runtime instead.
29//!
30//! ## Reading one
31//!
32//! Scalars are a single call. Arrays use the capacity/length in-out pattern
33//! [`hurray_descriptor_shape`](crate::descriptor::hurray_descriptor_shape)
34//! established: pass the capacity in `*out_len`, read the true length back out,
35//! and retry after [`HURRAY_ERR_BUFFER_TOO_SMALL`].
36
37mod layout;
38mod section;
39
40use std::ptr;
41
42use hurray_core::descriptor::TensorDescriptor;
43
44pub use layout::{
45 HURRAY_LAYOUT_FIELD_BLOCK_PAGED_INDEX_TYPE, HURRAY_LAYOUT_FIELD_BLOCK_PAGED_KV_ROLE,
46 HURRAY_LAYOUT_FIELD_BLOCK_PAGED_LAYER_INDEX, HURRAY_LAYOUT_FIELD_BLOCK_PAGED_NUM_PAGES,
47 HURRAY_LAYOUT_FIELD_BLOCK_PAGED_NUM_SEQS, HURRAY_LAYOUT_FIELD_BLOCK_PAGED_PAGED_AXIS,
48 HURRAY_LAYOUT_FIELD_BLOCK_PAGED_PAGE_SIZE, HURRAY_LAYOUT_FIELD_COMPOSITE_COMBINE_OP,
49 HURRAY_LAYOUT_FIELD_COMPOSITE_MEMBER_COUNT, HURRAY_LAYOUT_FIELD_COMPOSITE_RULE,
50 HURRAY_LAYOUT_FIELD_COO_IS_SORTED, HURRAY_LAYOUT_FIELD_COO_NNZ, HURRAY_LAYOUT_FIELD_CSC_NNZ,
51 HURRAY_LAYOUT_FIELD_CSF_MODE_ORDER, HURRAY_LAYOUT_FIELD_CSF_NNZ, HURRAY_LAYOUT_FIELD_CSR_NNZ,
52 HURRAY_LAYOUT_FIELD_HILBERT_ORDER, HURRAY_LAYOUT_FIELD_HILBERT_RANK,
53 HURRAY_LAYOUT_FIELD_MORTON_BITS, HURRAY_LAYOUT_FIELD_STRIDED_STRIDES,
54 HURRAY_LAYOUT_FIELD_TILED_DEPTH, HURRAY_LAYOUT_FIELD_TILED_INNER_LAYOUT,
55 HURRAY_LAYOUT_FIELD_TILED_INNER_STRIDES, HURRAY_LAYOUT_FIELD_TILED_OUTER_LAYOUT,
56 HURRAY_LAYOUT_FIELD_TILED_OUTER_STRIDES, HURRAY_LAYOUT_FIELD_TILED_TILE_SHAPE,
57};
58pub use section::{
59 HURRAY_COMPOSITE_MEMBER_FIELD_ROLE, HURRAY_EXT_TYPE_FIELD_BIT_WIDTH,
60 HURRAY_EXT_TYPE_FIELD_EXPONENT_BIAS, HURRAY_EXT_TYPE_FIELD_EXPONENT_BITS,
61 HURRAY_EXT_TYPE_FIELD_HAS_INF, HURRAY_EXT_TYPE_FIELD_HAS_NAN, HURRAY_EXT_TYPE_FIELD_IS_FLOAT,
62 HURRAY_EXT_TYPE_FIELD_IS_SIGNED, HURRAY_EXT_TYPE_FIELD_MANTISSA_BITS,
63 HURRAY_EXT_TYPE_FIELD_PACKING_FACTOR, HURRAY_EXT_TYPE_FIELD_SIGN_BITS,
64 HURRAY_QUANT_FIELD_MXFP_AXIS, HURRAY_QUANT_FIELD_MXFP_BLOCK_SIZE,
65 HURRAY_QUANT_FIELD_MXFP_SCALE_BUFFER, HURRAY_QUANT_FIELD_NF4_AXIS,
66 HURRAY_QUANT_FIELD_NF4_BLOCK_SIZE, HURRAY_QUANT_FIELD_NF4_SCALE_BUFFER,
67 HURRAY_QUANT_FIELD_PER_BLOCK_AXIS, HURRAY_QUANT_FIELD_PER_BLOCK_BLOCK_SIZE,
68 HURRAY_QUANT_FIELD_PER_BLOCK_SCALE_BUFFER, HURRAY_QUANT_FIELD_PER_BLOCK_SCALE_TYPE,
69 HURRAY_QUANT_FIELD_PER_BLOCK_ZERO_POINT_BUFFER, HURRAY_QUANT_FIELD_PER_CHANNEL_AXIS,
70 HURRAY_QUANT_FIELD_PER_CHANNEL_SCALE_BUFFER, HURRAY_QUANT_FIELD_PER_CHANNEL_SCALE_TYPE,
71 HURRAY_QUANT_FIELD_PER_CHANNEL_ZERO_POINT_BUFFER, HURRAY_QUANT_FIELD_PER_TENSOR_SCALE,
72 HURRAY_QUANT_FIELD_PER_TENSOR_ZERO_POINT, HURRAY_QUANT_FIELD_SCHEME_TAG,
73 HURRAY_SHARD_FIELD_PARENT_SHAPE, HURRAY_SHARD_FIELD_SHARD_OFFSET,
74 HURRAY_STATS_FIELD_COMPUTED_MASK, HURRAY_STATS_FIELD_HAS_INF, HURRAY_STATS_FIELD_HAS_NAN,
75 HURRAY_STATS_FIELD_NM_M, HURRAY_STATS_FIELD_NM_N, HURRAY_STATS_FIELD_NNZ,
76 HURRAY_STATS_FIELD_SPARSITY_RATIO, HURRAY_STATS_FIELD_VALUE_ABS_MAX,
77 HURRAY_STATS_FIELD_VALUE_MAX, HURRAY_STATS_FIELD_VALUE_MEAN, HURRAY_STATS_FIELD_VALUE_MIN,
78 HURRAY_STATS_FIELD_VALUE_STDDEV,
79};
80
81use crate::{
82 descriptor::HurrayDescriptor,
83 panic::{catch, null_check},
84 status::{
85 HurrayStatus, HURRAY_ERR_BUFFER_TOO_SMALL, HURRAY_ERR_FIELD_NOT_APPLICABLE, HURRAY_OK,
86 },
87};
88
89// ── Field identifier ──────────────────────────────────────────────────────────
90
91/// Identifies one field of a tensor descriptor's layout payload or of one of its
92/// optional sections.
93///
94/// A plain `uint32_t` rather than a `#[repr(C)]` enum: a C caller can pass any
95/// integer, and an out-of-range value materialised as a Rust enum is undefined
96/// behaviour. Validation happens in Rust, which is sound for every input.
97///
98/// The encoding is
99///
100/// ```text
101/// field_id = (range << 16) | (discriminant << 8) | field_index
102/// ```
103///
104/// so an identifier carries the sub-object it belongs to and the applicability
105/// check is a comparison rather than a table.
106///
107/// | Range | Sub-object | Discriminant byte |
108/// |---|---|---|
109/// | `0x0001` | layout payload | layout tag |
110/// | `0x0002` | quantization | scheme tag; `0x00` addresses the section itself |
111/// | `0x0003` | statistics | `0x00` |
112/// | `0x0004` | shard | `0x00` |
113/// | `0x0005` | extension type | `0x00` |
114/// | `0x0006` | composite member | `0x00` |
115///
116/// Only quantization has an inner sum type, so it is the only section that uses
117/// the discriminant byte; [`HURRAY_QUANT_FIELD_SCHEME_TAG`] reads the scheme a
118/// descriptor carries, which a caller needs before it can pick applicable
119/// identifiers. `0x0000_0000` is never a valid identifier, so a zeroed variable
120/// is caught rather than silently read as the first field.
121///
122/// Generated headers render these in decimal, so to decode one by hand: CSR
123/// `nnz` is `0x0001_0700` = 67328 — range `0x0001`, layout tag `0x07`, field
124/// index `0x00`.
125pub type HurrayDescriptorField = u32;
126
127/// Layout-payload range: `0x0001_xxxx`.
128const RANGE_LAYOUT: u32 = 0x0001;
129
130/// Section ranges, contiguous: quantization `0x0002` through composite member
131/// `0x0006`. Dispatch only needs the bounds; each section's own range is the
132/// high half-word of its constants in [`section`].
133const RANGE_SECTION_FIRST: u32 = 0x0002;
134const RANGE_SECTION_LAST: u32 = 0x0006;
135
136/// Extracts the sub-object range from a field identifier.
137#[inline]
138fn range_of(field: HurrayDescriptorField) -> u32 {
139 field >> 16
140}
141
142// ── Shared array source ───────────────────────────────────────────────────────
143
144/// An unsigned array field's source, borrowed from the descriptor.
145///
146/// `morton_bits` and `mode_order` are `u32` on the wire while `tile_shape` and
147/// the shard vectors are `u64`, so the widening cannot be a `memcpy` for all of
148/// them. Keeping the source borrowed and widening on write avoids allocating a
149/// temporary for a field a caller may only be measuring.
150pub(crate) enum U64Source<'a> {
151 Direct(&'a [u64]),
152 Widened(&'a [u32]),
153}
154
155impl U64Source<'_> {
156 fn len(&self) -> usize {
157 match self {
158 Self::Direct(s) => s.len(),
159 Self::Widened(s) => s.len(),
160 }
161 }
162
163 fn write_to(&self, dst: &mut [u64]) {
164 match self {
165 Self::Direct(s) => dst.copy_from_slice(s),
166 Self::Widened(s) => {
167 for (d, v) in dst.iter_mut().zip(s.iter()) {
168 *d = u64::from(*v);
169 }
170 }
171 }
172 }
173}
174
175// ── Dispatch ──────────────────────────────────────────────────────────────────
176
177/// Routes an unsigned scalar read to the sub-object its identifier names.
178fn u64_scalar(
179 desc: &TensorDescriptor,
180 field: HurrayDescriptorField,
181 level: u32,
182) -> Result<u64, HurrayStatus> {
183 match range_of(field) {
184 RANGE_LAYOUT => layout::u64_scalar(&desc.layout, field, level),
185 RANGE_SECTION_FIRST..=RANGE_SECTION_LAST => section::u64_scalar(desc, field, level),
186 _ => Err(HURRAY_ERR_FIELD_NOT_APPLICABLE),
187 }
188}
189
190/// Routes an unsigned array read to the sub-object its identifier names.
191fn u64_array<'a>(
192 desc: &'a TensorDescriptor,
193 field: HurrayDescriptorField,
194 level: u32,
195) -> Result<U64Source<'a>, HurrayStatus> {
196 match range_of(field) {
197 RANGE_LAYOUT => layout::u64_array(&desc.layout, field, level),
198 RANGE_SECTION_FIRST..=RANGE_SECTION_LAST => section::u64_array(desc, field, level),
199 _ => Err(HURRAY_ERR_FIELD_NOT_APPLICABLE),
200 }
201}
202
203// ── Scalar accessors ──────────────────────────────────────────────────────────
204
205/// Reads an unsigned or boolean scalar field of the descriptor.
206///
207/// Covers every scalar field that is unsigned or boolean, which is all of them
208/// but two: a boolean reads as `0` or `1`, and a wire tag byte — `kv_role`,
209/// `block_table_index_type`, `scale_type`, `member_role`, the quantization
210/// scheme tag — reads as its documented value. The two exceptions are
211/// [`hurray_descriptor_field_i64`] and [`hurray_descriptor_field_f64`].
212///
213/// # Arguments
214///
215/// - `handle` — Descriptor created by
216/// [`hurray_descriptor_decode`](crate::hurray_descriptor_decode).
217/// - `field` — One of the `HURRAY_*_FIELD_*` unsigned scalar constants.
218/// - `level` — Tiled nesting level to read; MUST be `0` for every other layout
219/// and for every section field.
220/// - `out_value` — Non-null pointer; written only on [`HURRAY_OK`].
221///
222/// # Errors
223///
224/// - [`HURRAY_ERR_FIELD_NOT_APPLICABLE`] — `field` does not belong to this
225/// descriptor's layout or quantization scheme, or is not an unsigned scalar.
226/// - [`HURRAY_ERR_FIELD_NOT_PRESENT`](crate::status::HURRAY_ERR_FIELD_NOT_PRESENT) — the field is optional and this
227/// descriptor does not carry it: an absent section, an unset optional field,
228/// or a statistic whose `computed_mask` guard bit is clear.
229/// - [`HURRAY_ERR_INDEX_OUT_OF_BOUNDS`](crate::status::HURRAY_ERR_INDEX_OUT_OF_BOUNDS) — `level` exceeds the nesting depth, or
230/// is non-zero for a field that does not nest.
231///
232/// # Safety
233///
234/// `handle` and `out_value` MUST be valid, non-null pointers, and `handle` MUST
235/// NOT have been destroyed.
236#[no_mangle]
237pub unsafe extern "C" fn hurray_descriptor_field_u64(
238 handle: *const HurrayDescriptor,
239 field: HurrayDescriptorField,
240 level: u32,
241 out_value: *mut u64,
242) -> HurrayStatus {
243 catch(|| {
244 null_check!(handle, out_value);
245
246 // SAFETY: handle is non-null and points to a live HurrayDescriptor.
247 match u64_scalar((*handle).inner(), field, level) {
248 Ok(value) => {
249 // SAFETY: out_value is non-null and writable for one u64.
250 *out_value = value;
251 HURRAY_OK
252 }
253 Err(status) => status,
254 }
255 })
256}
257
258/// Reads a signed scalar field of the descriptor.
259///
260/// Only per-tensor affine `zero_point` is signed; it is an `int32` on the wire
261/// and widens losslessly. Separate from [`hurray_descriptor_field_u64`] because
262/// an unsigned accessor would report a negative zero point as a huge positive
263/// number, which is exactly the kind of silent corruption a quantized read
264/// cannot absorb.
265///
266/// # Errors
267///
268/// As [`hurray_descriptor_field_u64`], with
269/// [`HURRAY_ERR_FIELD_NOT_APPLICABLE`] for any field that is not a signed
270/// scalar.
271///
272/// # Safety
273///
274/// `handle` and `out_value` MUST be valid, non-null pointers, and `handle` MUST
275/// NOT have been destroyed.
276#[no_mangle]
277pub unsafe extern "C" fn hurray_descriptor_field_i64(
278 handle: *const HurrayDescriptor,
279 field: HurrayDescriptorField,
280 level: u32,
281 out_value: *mut i64,
282) -> HurrayStatus {
283 catch(|| {
284 null_check!(handle, out_value);
285
286 // Signed scalars exist only in the sections; no layout field is signed.
287 if range_of(field) == RANGE_LAYOUT {
288 return HURRAY_ERR_FIELD_NOT_APPLICABLE;
289 }
290
291 // SAFETY: handle is non-null and points to a live HurrayDescriptor.
292 match section::i64_scalar((*handle).inner(), field, level) {
293 Ok(value) => {
294 // SAFETY: out_value is non-null and writable for one i64.
295 *out_value = value;
296 HURRAY_OK
297 }
298 Err(status) => status,
299 }
300 })
301}
302
303/// Reads a real scalar field of the descriptor.
304///
305/// Covers per-tensor affine `scale` — a `float32` on the wire, widened — and the
306/// six `float64` statistics. A statistic whose `computed_mask` guard bit is
307/// clear returns [`HURRAY_ERR_FIELD_NOT_PRESENT`](crate::status::HURRAY_ERR_FIELD_NOT_PRESENT) rather than the stored
308/// placeholder, so a caller cannot read an uncomputed value by forgetting to
309/// test the mask.
310///
311/// # Errors
312///
313/// As [`hurray_descriptor_field_u64`], with
314/// [`HURRAY_ERR_FIELD_NOT_APPLICABLE`] for any field that is not a real scalar.
315///
316/// # Safety
317///
318/// `handle` and `out_value` MUST be valid, non-null pointers, and `handle` MUST
319/// NOT have been destroyed.
320#[no_mangle]
321pub unsafe extern "C" fn hurray_descriptor_field_f64(
322 handle: *const HurrayDescriptor,
323 field: HurrayDescriptorField,
324 level: u32,
325 out_value: *mut f64,
326) -> HurrayStatus {
327 catch(|| {
328 null_check!(handle, out_value);
329
330 // Real scalars exist only in the sections; every layout field is integral.
331 if range_of(field) == RANGE_LAYOUT {
332 return HURRAY_ERR_FIELD_NOT_APPLICABLE;
333 }
334
335 // SAFETY: handle is non-null and points to a live HurrayDescriptor.
336 match section::f64_scalar((*handle).inner(), field, level) {
337 Ok(value) => {
338 // SAFETY: out_value is non-null and writable for one f64.
339 *out_value = value;
340 HURRAY_OK
341 }
342 Err(status) => status,
343 }
344 })
345}
346
347// ── Array accessors ───────────────────────────────────────────────────────────
348
349/// Reads an unsigned array field of the descriptor.
350///
351/// Uses the capacity/length in-out pattern
352/// [`hurray_descriptor_shape`](crate::descriptor::hurray_descriptor_shape) uses:
353///
354/// 1. Set `*out_len` to the capacity of `out_values` in elements, or `0` if
355/// `out_values` is null.
356/// 2. On return `*out_len` always holds the field's true length.
357/// 3. A null `out_values` or an insufficient capacity returns
358/// [`HURRAY_ERR_BUFFER_TOO_SMALL`], so the caller can allocate and retry.
359///
360/// # Errors
361///
362/// As [`hurray_descriptor_field_u64`], plus [`HURRAY_ERR_BUFFER_TOO_SMALL`] per
363/// the protocol above.
364///
365/// # Safety
366///
367/// - `handle` and `out_len` MUST be valid, non-null pointers.
368/// - If `out_values` is non-null it MUST be writable for `capacity` × 8 bytes,
369/// where `capacity` is the value at `*out_len` on entry.
370#[no_mangle]
371pub unsafe extern "C" fn hurray_descriptor_field_u64_array(
372 handle: *const HurrayDescriptor,
373 field: HurrayDescriptorField,
374 level: u32,
375 out_values: *mut u64,
376 out_len: *mut usize,
377) -> HurrayStatus {
378 catch(|| {
379 // out_values may be null (capacity-query mode); out_len is always required.
380 null_check!(handle, out_len);
381
382 // SAFETY: handle is non-null and points to a live HurrayDescriptor.
383 let source = match u64_array((*handle).inner(), field, level) {
384 Ok(s) => s,
385 Err(status) => return status,
386 };
387
388 let true_len = source.len();
389 // SAFETY: out_len is non-null and writable; read the caller's capacity
390 // before overwriting it with the true length.
391 let capacity = *out_len;
392 *out_len = true_len;
393
394 if out_values.is_null() || capacity < true_len {
395 return HURRAY_ERR_BUFFER_TOO_SMALL;
396 }
397
398 // SAFETY: out_values is non-null and writable for at least true_len
399 // elements, checked immediately above.
400 source.write_to(std::slice::from_raw_parts_mut(out_values, true_len));
401 HURRAY_OK
402 })
403}
404
405/// Reads a signed array field of the descriptor.
406///
407/// Signed because strides may be negative (reversed) or zero (broadcast), which
408/// an unsigned accessor would misreport. The capacity/length protocol is the one
409/// [`hurray_descriptor_field_u64_array`] documents.
410///
411/// # Errors
412///
413/// As [`hurray_descriptor_field_u64_array`].
414///
415/// # Safety
416///
417/// - `handle` and `out_len` MUST be valid, non-null pointers.
418/// - If `out_values` is non-null it MUST be writable for `capacity` × 8 bytes,
419/// where `capacity` is the value at `*out_len` on entry.
420#[no_mangle]
421pub unsafe extern "C" fn hurray_descriptor_field_i64_array(
422 handle: *const HurrayDescriptor,
423 field: HurrayDescriptorField,
424 level: u32,
425 out_values: *mut i64,
426 out_len: *mut usize,
427) -> HurrayStatus {
428 catch(|| {
429 // out_values may be null (capacity-query mode); out_len is always required.
430 null_check!(handle, out_len);
431
432 // Signed arrays exist only in the layout payload; no section has one.
433 if range_of(field) != RANGE_LAYOUT {
434 return HURRAY_ERR_FIELD_NOT_APPLICABLE;
435 }
436
437 // SAFETY: handle is non-null and points to a live HurrayDescriptor.
438 let values = match layout::i64_array(&(*handle).inner().layout, field, level) {
439 Ok(v) => v,
440 Err(status) => return status,
441 };
442
443 let true_len = values.len();
444 // SAFETY: out_len is non-null and writable; read the caller's capacity
445 // before overwriting it with the true length.
446 let capacity = *out_len;
447 *out_len = true_len;
448
449 if out_values.is_null() || capacity < true_len {
450 return HURRAY_ERR_BUFFER_TOO_SMALL;
451 }
452
453 // SAFETY: out_values is non-null and writable for at least true_len
454 // elements, checked immediately above.
455 ptr::copy_nonoverlapping(values.as_ptr(), out_values, true_len);
456 HURRAY_OK
457 })
458}