Structured views over byte buffers#
- Date:
2026-08
discussion
Motivation#
TypeProto.Opaque identifies a runtime-owned value by domain and name,
but it gives no information about its serialized representation. A generic
reader cannot determine how many values are present, where fields begin,
or how many bytes may safely be read.
Conversely, adding one protobuf message for every quantization or custom format creates a closed hierarchy that must grow whenever a new layout is introduced.
This proposal assumes that a StructProto already exists. It owns
or references a byte buffer and references the physical type from which its
exact byte size is computed:
message StructProto {
int32 type = 1; // model-level index, or -1 when struct_type is present
optional StructTypeProto struct_type = 2;
bytes raw_data = 3;
repeated StringStringEntryProto external_data = 4;
string name = 5;
string doc_string = 6;
}
The container itself is outside the scope of this page. The purpose of the
specification is to define StructTypeProto: a portable structure
that can be overlaid on the bytes of a StructProto.
The type system adds three serialized structural kinds and constant fields:
an array of a statically sized
TypeProto;a bit packing of repeated named components;
a structure containing named, statically sized
TypeProtofields;a tensor constant consuming no payload bytes.
Scalars and ordinary tensors continue to use TypeProto.Tensor. Quantized
values, packed records, custom numeric types, image pixels, and other static
binary formats are recursive compositions of existing ONNX types and these
additions.
Requirements#
The size implied by the physical type equals the inline or external payload length.
Every read performed by the structured view is bounds-checked.
Bits and multi-byte values use one canonical ordering convention.
A structure may be nested and repeated without introducing a new proto for each format.
Every array and bit-packing length is a concrete non-negative integer.
The number of payload bytes of a value is computable from its type without reading the payload.
The physical structure is inspectable without loading a vendor plugin.
An optional standard ONNX decoder defines logical semantics such as dequantization.
Stable contract#
The proposal has three valid uses of StructTypeProto:
concrete declarationSelects
array,bit_packing, orstructure. It appears inModelProto.struct_typesor inStructProto.struct_type. It completely determines the payload size.exact static referenceSelects
type_indexand appears insideTypeProto. It accepts only the referenced model-level declaration.unconstrained static categoryLeaves
kindunset and appears only insideTypeProto. It accepts any concrete structured declaration. This form is used by heterogeneous sequences and maps.
type_index may also occur below a concrete root through
Array.element_type or Structure.Field.type. A constant tensor value is
attached directly to a Structure.Field. A concrete root may not
be a type_index or an unset kind. Static forms may not carry
decoder, encoder, name, or metadata.
Only the decoder and encoder attached to the selected concrete root
are invoked. A declaration reached through a nested type_index contributes
only its physical structure and constants; its decoder and encoder are not
composed implicitly.
No other interpretation of an absent field is permitted. In particular, counts are concrete, and there are no inferred lengths, implicit alignment, hidden padding, semantic traits, or alternate byte orders.
Physical size function#
The serialized size is computed recursively in bits from the concrete root declaration:
size(scalar(T)) = bit_width(T)
size(Array(T, n)) = n * size(T)
size(BitPacking(c..., n)) = n * sum(c.bit_width)
size(Field(constant)) = 0
size(Field(T)) = size(T)
size(Structure(f...)) = sum(size(f))
size(type_index=i) = size(ModelProto.struct_types[i])
All arithmetic is checked in uint64. References must be acyclic. The
concrete root size must be divisible by eight and equal the inline
raw_data length or the external-data length, so the physical schema
and payload remain independently checkable.
StructTypeProto#
The complete proposal adds one top-level structured type message.
message StructTypeProto {
message BitPacking {
message Component {
string name = 1;
uint32 bit_width = 2;
}
repeated Component component = 1;
uint64 dimension = 2;
}
message Array {
TypeProto element_type = 1;
uint64 dimension = 2;
}
message Field {
string name = 1;
oneof content {
TypeProto type = 2;
TensorProto constant = 4;
}
string doc_string = 3;
}
message Structure {
repeated Field field = 1;
}
oneof kind {
Array array = 1;
Structure structure = 2;
BitPacking bit_packing = 3;
int32 type_index = 5;
}
optional FunctionProto decoder = 6;
optional FunctionProto encoder = 7;
string name = 8;
string doc_string = 9;
repeated StringStringEntryProto metadata_props = 10;
}
Integration#
message ModelProto {
repeated StructTypeProto struct_types = <N>;
}
message TypeProto {
oneof value {
...
StructTypeProto struct_type = <N>;
}
}
A reusable value selects an entry in ModelProto.struct_types with
StructProto.type. An inline value sets type to -1 and provides
StructProto.struct_type. Nested structures use type_index to reference
another model-level declaration.
Physical rules#
Arrays and bit packings are tightly packed.
Structure fields are serialized in declaration order.
Constants consume no payload bytes.
Padding must be represented explicitly.
Bits are ordered from least to most significant within each byte.
Multi-byte values are little-endian.
Only fixed-width ONNX scalar types are valid physical leaves.
The decoder maps physical fields to one logical ONNX value.
Example#
The following type stores 128 INT4 values plus format constants:
StructTypeProto {
name: "LINEAR_INT4_128"
structure: Structure {
field: {
name: "values"
type: array(INT4, dimension=128)
}
field: {
name: "scale"
constant: tensor(FLOAT, [], 0.125)
}
field: {
name: "zero_point"
constant: tensor(INT64, [], 0)
}
}
}
StructProto {
type: <LINEAR_INT4_128 type index>
raw_data: <64 bytes>
name: "weight"
}
The payload size is 128 * 4 / 8 = 64 bytes. Constants are stored in the
type and do not contribute to that size.
Validation#
A checker rejects:
an invalid or cyclic type reference;
a field without exactly one of
typeandconstant;duplicate field or component names;
zero component widths or unsupported physical leaf types;
a physical size that is not byte-aligned;
a payload whose length differs from the computed size;
implicit padding or untyped trailing bytes.