Skip to main content

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}