Skip to content

Add packed 4-bit SafeTensors weight support - #19

Merged
mtavenrath merged 6 commits into
rustnn:mainfrom
FelixKrall:fkrall/packed4-external-weights
Sep 23, 2026
Merged

mtavenrath merged 6 commits into
rustnn:mainfrom
FelixKrall:fkrall/packed4-external-weights

Conversation

@FelixKrall

@FelixKrall FelixKrall commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

This PR is used and depended on by RustNN #238 and onnx2webnn #5

Summary

Centralize .webnn external-weight reading and writing in webnn-graph. The resolver now supports versioned packed Int4/Uint4 SafeTensors storage, memory-maps external archives, and shares the same validation rules with a new atomic SafeTensors writer.

Feature additions

  • Support logical Int4 and Uint4 constants stored as packed U8 SafeTensors tensors using the existing rustnn.webnn.packed4=1 metadata contract.
  • Add write_external_weights_safetensors, which validates external declarations and writes ordinary and packed tensors through a same-directory temporary file.
  • Document .webnn, @weights, sidecar discovery, dtype mappings, packed-4-bit storage, naming rules, and compatibility guarantees.

Bugfixes

  • Reject missing or unsupported packed-format markers, invalid storage dtypes, malformed physical shapes and lengths, element-count overflow, missing references, and ambiguous sanitized names.
  • Clean temporary archives when serialization or final installation fails.

Refactors

  • Resolve ordinary and packed tensors during one traversal of one SafeTensors archive.
  • Centralize packed-format constants and validation in webnn-graph for reuse by consumers such as RustNN.

Behavioral impact and compatibility

  • The existing resolve_external_weights API remains compatible.
  • Existing archives using rustnn.webnn.packed4=1 and the low-nibble-first layout remain supported.
  • Packed tensors are physically stored as one-dimensional U8 tensors of length ceil(logical_elements / 2); the .webnn declaration remains authoritative for their logical dtype and shape.
  • Ordinary SafeTensors and manifest-backed external weights retain their existing format and discovery behavior.
  • Resolved tensors are copied once into owned InlineBytes; externally backed GraphJson storage is not introduced.

Validation

  • cargo fmt --all -- --check — passed.
  • cargo test external_weights --no-default-features — 13 focused external-weight tests passed.
  • cargo test --all-features — 195 unit tests and the ResNet integration test passed.
  • cargo test --no-default-features — 72 tests passed.
  • cargo clippy --all-targets --no-default-features -- -D warnings — passed.
  • Release builds with all features and with no default features passed.
  • cargo clippy --all-targets --all-features -- -D warnings — reaches a pre-existing clippy::question_mark failure in src/onnx/shape_inference.rs; no changed external-weight code is implicated.
  • git diff --check — passed.

Reviewer focus

  • Confirm the packed-U8 representation and versioned metadata marker are appropriate as the compatibility contract for logical Int4/Uint4 tensors.
  • Review the read-only memory-mapping boundary and atomic temporary-file installation behavior.

Extend the shared external-weight resolver and writer to support packed logical Int4 and Uint4 tensors in versioned SafeTensors archives. Resolve ordinary and packed constants in one memory-mapped traversal, validate names, metadata, storage types, shapes, and lengths, and write archives atomically without an archive-sized output allocation.

Document the .webnn external-weight contract and cover packed, mixed, malformed, sanitized-name, manifest, and raw-weight cases.
## Feature additions

- Add a canonical `.webnn` format reference covering graph versions, shapes, initializers, node options, multi-output operations, output bindings, and `GraphJson` round trips.
- Add a maintained v2 format example and parser → serializer → parser regression coverage.
- Document SafeTensors, packed Int4/Uint4 storage, manifest-backed raw weights, sidecar discovery, mmap ownership, and atomic writer behavior.

## Bugfixes

- Make the example build workflow reconstruct untracked tensor inputs from the checked-in weight archive.
- Update the example workflow to use the current direct JavaScript emitter command.

## Refactors

- Consolidate duplicated format material into canonical documentation pages.
- Rewrite the README, dynamic-dimension guide, ONNX-lowering guide, and example documentation against current source behavior.
- Remove stale model-specific guidance, unsupported quantitative claims, and obsolete agent-oriented files.

## Behavioral impact and compatibility

- No library API, file-format, parser, serializer, or runtime behavior changes are intended.
- The example build script now works without locally retained raw tensor files but produces the same documented artifacts.
- The removed pages are replaced by canonical format and workflow documentation.

## Validation

- `cargo fmt --all -- --check` passed.
- `cargo test --all-features` passed, including 195 library tests and the format-reference and ResNet conversion integration tests.
- `cargo test --no-default-features` passed, including 72 library tests and the format-reference integration test.
- Strict Clippy passed for all targets with all features and without default features.
- Parse, serialize, structural validation, manifest validation, pack, unpack, and JavaScript-emitter smoke checks passed.
- Relative Markdown links, script syntax, line lengths, stale-language searches, and `git diff --check` passed.
## Feature additions

- None.

## Bugfixes

- Replace fixed-size `chunks_exact` calls with `as_chunks` across external-weight and ONNX byte decoding, satisfying the new Rust 1.98 Clippy lint enforced by CI.

## Refactors

- None.

## Behavioral impact and compatibility

- No intended externally observable behavior change; complete chunks are decoded as before and trailing incomplete bytes remain ignored.

## Validation

- `RUSTUP_TOOLCHAIN=1.98.1 make fmt-check lint test release` passed with 197 tests.
- `RUSTUP_TOOLCHAIN=1.98.1 CARGO_FLAGS=--no-default-features make fmt-check lint test release` passed with 73 tests.
- `git diff --cached --check` passed.
@mtavenrath
mtavenrath merged commit 0bcd517 into rustnn:main Sep 23, 2026
7 checks passed
mtavenrath pushed a commit to rustnn/rustnn that referenced this pull request Sep 23, 2026
## Summary

RustNN changes to enable full roundtrip numeric validation of onnx2webnn
conversion. Tied to [onnx2webnn
#5](rustnn/onnx2webnn#5) and dependend on
[webnn-graph #19](rustnn/webnn-graph#19). The
reference in this PR needs to updated after the webnn-graph was merged.

Make serialized WebNN graphs independently reloadable and executable
while unifying graph recording and shape inference between
`MLGraphBuilder` and the `.webnn` loader.

Completed graphs now use an unambiguous shape model: `[]` always means a
known rank-zero scalar, bounded dynamic dimensions remain explicit, and
unresolved descriptors exist only as temporary internal inference state.

## Feature additions

- Add `MLContext::rustnn_build_graph` as a direct compilation entry
point for deserialized `GraphInfo`. This avoids constructing a second,
unused `GraphRecorder` when compiling a graph reconstructed by the
`.webnn` loader.
- Add `run_onnx_path_with_inputs` so native ONNX Runtime execution can
resolve external-data sidecars relative to the model file. This supports
reference validation of filesystem-backed models whose weights are not
embedded in the ONNX protobuf.
- Serialize complete operation arguments, including reshape targets,
Slice parameters, concat axes, permutations, and operand-valued options
referenced by stable names.
- Reload serialized graphs through the same recording and inference path
used by `MLGraphBuilder`.
- Add a versioned packed-4-bit Safetensors extension:
  - Logical `Int4` and `Uint4` dtype and shape remain in `.webnn`.
  - Packed low-nibble-first bytes are stored as Safetensors `U8`.
  - Archive metadata identifies the RustNN extension.
- Reload validates marker, dtype, shape, byte length, and tensor-name
resolution.
- Support mixed ordinary and packed-4-bit constants in one Safetensors
archive.

## Bugfixes

- Infer and record every output of multi-output operations rather than
only the first.
- Prevent unresolved operands from silently becoming scalar descriptors.
- Treat scalar GRU hidden states as rank-zero values and reject them
through normal GRU rank validation.
- Preserve scalar inputs, constants, intermediates, outputs,
quantize/dequantize operands, and converter shape-map entries.
- Always serialize required scalar `Reshape` and `Expand` targets as
`newShape: []`.
- Reject missing or malformed required shape-valued arguments
explicitly.
- Serialize operand-valued options by stable name instead of unstable
numeric operand IDs.
- Validate packed 4-bit logical element counts and exact packed storage
lengths.
- Correct Slice lowering for non-unit strides by deriving backend end
indices from `start + extent`; this fixes both the ONNX and LiteRT
lowering paths.

## Refactors

- Introduce a private shared `GraphRecorder` used by both graph
construction and JSON loading.
- Centralize descriptor inference, operation insertion, dependency
tracking, and graph-output marking.
- Make operation insertion atomic: failed inference or validation no
longer leaves partially recorded graph state.
- Remove the historical empty-vector unknown-shape heuristic and
obsolete temporary shape-table workaround.
- Keep unresolved shapes represented through `Option` or missing
internal entries rather than public `OperandDescriptor` values.
- Update setup documentation with concise optional prerequisites for
feature-specific backends.

## Behavioral impact and compatibility

- `GraphRecorder` remains crate-private and is not a new public API.
- `MLContext::rustnn_build_graph` and `run_onnx_path_with_inputs` are
additive public entry points.
- Every `OperandDescriptor` in completed `GraphInfo` now has a known
shape:
  - `[]` is scalar.
  - Nonempty shapes may contain bounded `Dimension::Dynamic` entries.
  - Unknown shapes cannot escape graph construction.
- The experimental `.webnn` format has intentional compatibility breaks:
  - Required shapes may no longer be omitted.
  - Scalar shape arguments must be serialized explicitly.
- Direct input or constant graph outputs are rejected; an explicit
operation such as Identity is required.
- Ordinary Safetensors remain unchanged. Packed 4-bit tensors require
the RustNN metadata marker and are rejected if presented as an unmarked
or malformed extension.
- Browser WebNN still does not natively execute 4-bit tensors. The ORT
backend reconstructs an executable graph using its supported
representations.

## Validation

- Latest `cargo test --lib`: 379 tests passed.
- Default, no-default-feature, `dynamic-inputs`, ONNX Runtime, and
available backend-mock checks passed across the refactor.
- Focused round trips passed for:
  - Scalar and bounded-dynamic descriptors.
  - Reshape, Expand, Slice, Concat, and Gemm bias.
  - Multi-output inference.
  - Even- and odd-sized `Int4` and `Uint4` constants.
  - Mixed native and packed-4-bit Safetensors.
  - Packed-4-bit graph compilation and ORT execution.
  - Malformed packed archive rejection.
- Downstream onnx2webnn validation passed 743 tests and completed the
52-case skeleton and real-weight sweeps with the expected documented
blockers.
- Formatting and diff checks passed.
- LiteRT regression coverage was added, but the LiteRT feature test
could not run locally because `flatc` was unavailable.
- Clippy remained blocked by pre-existing `-D warnings` failures outside
this change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants