Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog and this project follows Semantic Versioning.

## [0.3.0] - to be released

### Added
- Dynamic dimension representation in AST and WG grammar via `dyn("name", maxSize)` input dims.
- ONNX conversion support for preserving unresolved dynamic input dimensions in graph metadata.
- New `convert-onnx` CLI flag: `--experimental-dynamic-inputs` (opt-in dynamic input preservation).
- ONNX converter/operator support improvements:
- `ScatterND`
- `Where`, `Equal`, comparison operators, `Cos`, `Sin`, `TriLu`, `Tile`
- `ConstantOfShape`, `Range`, and additional constant-folding evaluators
- Built-in constant folding in `webnn-graph` (`--optimize`) to reduce dynamic-shape plumbing.
- Global debug switch for converter diagnostics (`--debug`).
- Pre-commit hook setup script and make-based local checks.

### Changed
- ONNX conversion now supports static-lowering + dynamic metadata workflows in one pipeline.
- Graph parser/serializer now support richer values (including object literals in options).
- JS/HTML emitters and visualizer now render mixed static/dynamic shapes.
- Docs expanded and corrected:
- ONNX lowering behavior
- Dynamic dimension guidance
- SmolLM-135M conversion example from Hugging Face

### Fixed
- Multiple ONNX conversion correctness fixes, including:
- dynamic reshape/expand conversion edge cases
- shape inflation prevention and post-conversion shape tracking
- `Unsqueeze` v14 handling
- identifier sanitization robustness (including `$` prefixes)
- clippy/robustness cleanup across converter and shape inference

### Compatibility
- Existing static graphs remain supported.
- Validator/serializer support both graph versions `v1` and `v2`.
- Dynamic input metadata is experimental and must be enabled with
`--experimental-dynamic-inputs`.

## [0.2.1] - 2025-12-28

### Added
- ONNX shape inference and `Expand` conversion support.
- Initial ONNX lowering documentation.

### Fixed
- BERT conversion fixes.
- Identifier sanitization updates.

## [0.2.0] - 2025-12-24

### Added
- Interactive HTML visualizer and `emit-html` command.
- Drag-and-drop `.webnn` loading and parser improvements.
- Graph/weights split workflow improvements and docs refinements.

## [0.1.0] - 2025-12-24

### Added
- Initial release with core DSL parsing/serialization/validation scaffold.
- Binary weights support and foundational CLI commands.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "webnn-graph"
version = "0.2.1"
version = "0.3.0"
edition = "2021"
license = "Apache-2.0"
description = "Simple DSL for WebNN graphs"
Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,28 @@ Observed output from that run:
- `/tmp/smol_hf.weights`: ~513 MB
- `/tmp/smol_hf.manifest.json`: ~423 KB

### Example: Converting all-MiniLM-L6-v2

Download the ONNX model:

```bash
curl -L "https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/onnx/model.onnx?download=true" \
-o /tmp/minilm.onnx
```

Convert with common sentence-embedding overrides:

```bash
webnn-graph convert-onnx \
--input /tmp/minilm.onnx \
--optimize \
--override-dim batch_size=1 \
--override-dim sequence_length=128 \
--output /tmp/minilm.webnn \
--weights /tmp/minilm.weights \
--manifest /tmp/minilm.manifest.json
```

### Supported ONNX Operations

The converter focuses on NLP/Transformer operations:
Expand Down
136 changes: 127 additions & 9 deletions src/emit_js.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,46 @@ fn shape_to_js(shape: &[Dimension]) -> String {
format!("[{}]", dims.join(", "))
}

fn normalize_dtype_name(name: &str) -> Option<&'static str> {
match name.to_ascii_lowercase().as_str() {
"float32" => Some("float32"),
"float16" => Some("float16"),
"int4" => Some("int4"),
"uint4" => Some("uint4"),
"int32" => Some("int32"),
"uint32" => Some("uint32"),
"int64" => Some("int64"),
"uint64" => Some("uint64"),
"int8" => Some("int8"),
"uint8" => Some("uint8"),
_ => None,
}
}

fn normalize_options_for_js(value: &mut serde_json::Value) {
match value {
serde_json::Value::Object(obj) => {
for (k, v) in obj.iter_mut() {
if k == "dataType" || k == "to" {
if let Some(s) = v.as_str() {
if let Some(norm) = normalize_dtype_name(s) {
*v = serde_json::Value::String(norm.to_string());
continue;
}
}
}
normalize_options_for_js(v);
}
}
serde_json::Value::Array(arr) => {
for v in arr {
normalize_options_for_js(v);
}
}
_ => {}
}
}

/// Emit the WeightsFile helper class for loading weights
pub fn emit_weights_loader_js() -> &'static str {
r#"/**
Expand Down Expand Up @@ -174,31 +214,64 @@ pub fn emit_builder_js(g: &GraphJson) -> String {
s.push('\n');

for n in &g.nodes {
if n.op == "constant" {
let mut opts = serde_json::Value::Object(n.options.clone());
normalize_options_for_js(&mut opts);
let dtype = opts
.get("dataType")
.and_then(|v| v.as_str())
.and_then(normalize_dtype_name)
.unwrap_or("float32");
let shape = opts
.get("shape")
.cloned()
.unwrap_or_else(|| serde_json::json!([]))
.to_string();
let data = opts
.get("data")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
s.push_str(&format!(
" {{\n const b64 = {data:?};\n const bytes = Uint8Array.from(atob(b64), c => c.charCodeAt(0));\n env.set({id:?}, builder.constant({{ dataType: {dtype:?}, shape: {shape} }}, bytes.buffer));\n }}\n",
id = n.id,
data = data,
dtype = dtype,
shape = shape
));
continue;
}

let ins = n
.inputs
.iter()
.map(|x| format!("env.get({:?})", x))
.collect::<Vec<_>>()
.join(", ");
let opts = serde_json::Value::Object(n.options.clone()).to_string();
if let Some(outs) = &n.outputs {
s.push_str(&format!(
" {{\n const tmp = builder[{op:?}]({ins}, {opts});\n",
let mut opts_val = serde_json::Value::Object(n.options.clone());
normalize_options_for_js(&mut opts_val);
let opts = opts_val.to_string();
let call = if ins.is_empty() {
format!("builder[{op:?}]({opts})", op = n.op, opts = opts)
} else {
format!(
"builder[{op:?}]({ins}, {opts})",
op = n.op,
ins = ins,
opts = opts
));
)
};
if let Some(outs) = &n.outputs {
s.push_str(&format!(" {{\n const tmp = {call};\n", call = call));
for (i, o) in outs.iter().enumerate() {
s.push_str(&format!(" env.set({o:?}, tmp[{i}]);\n", o = o, i = i));
}
s.push_str(" }\n");
} else {
s.push_str(&format!(
" env.set({id:?}, builder[{op:?}]({ins}, {opts}));\n",
" env.set({id:?}, {call});\n",
id = n.id,
op = n.op,
ins = ins,
opts = opts
call = call
));
}
}
Expand Down Expand Up @@ -383,6 +456,51 @@ mod tests {
assert!(js.contains("\"axis\":1"));
}

#[test]
fn test_emit_cast_normalizes_dtype_option() {
let mut g = new_graph_json();
g.inputs.insert(
"x".to_string(),
OperandDesc {
data_type: DataType::Float32,
shape: to_dimension_vector(&[1]),
},
);
let mut options = serde_json::Map::new();
options.insert("to".to_string(), serde_json::json!("Int32"));
g.nodes.push(Node {
id: "y".to_string(),
op: "cast".to_string(),
inputs: vec!["x".to_string()],
options,
outputs: None,
});
g.outputs.insert("y".to_string(), "y".to_string());
let js = emit_builder_js(&g);
assert!(js.contains("\"to\":\"int32\""));
}

#[test]
fn test_emit_constant_node_uses_atob_decode() {
let mut g = new_graph_json();
let mut options = serde_json::Map::new();
options.insert("dataType".to_string(), serde_json::json!("Float32"));
options.insert("shape".to_string(), serde_json::json!([1]));
options.insert("data".to_string(), serde_json::json!("AAAAAA=="));
g.nodes.push(Node {
id: "c0".to_string(),
op: "constant".to_string(),
inputs: vec![],
options,
outputs: None,
});
g.outputs.insert("c0".to_string(), "c0".to_string());
let js = emit_builder_js(&g);
assert!(js.contains("atob(b64)"));
assert!(js.contains("dataType: \"float32\""));
assert!(js.contains("builder.constant"));
}

#[test]
fn test_emit_with_multi_outputs() {
let mut g = new_graph_json();
Expand Down
Loading