From a22593e2c5a7c0fb7d0140ab60698f0a29fd9ff0 Mon Sep 17 00:00:00 2001 From: Lutz Roeder Date: Tue, 26 Nov 2024 20:25:52 -0800 Subject: [PATCH] Add DOT test files (#1368) --- source/dot.js | 653 +++++++++++++++++++++++++++++++++++++++++++++ source/graphviz.js | 36 --- source/mlir.js | 31 ++- source/onnx.js | 2 +- source/view.js | 8 +- test/models.json | 87 +++++- 6 files changed, 758 insertions(+), 59 deletions(-) create mode 100644 source/dot.js delete mode 100644 source/graphviz.js diff --git a/source/dot.js b/source/dot.js new file mode 100644 index 0000000000..49fee00e4c --- /dev/null +++ b/source/dot.js @@ -0,0 +1,653 @@ + +const dot = {}; + +dot.ModelFactory = class { + + match(context) { + const reader = context.read('text', 0x10000); + if (reader) { + try { + for (let i = 0; i < 64; i++) { + const line = reader.read('\n'); + if (line === undefined) { + return; + } + if (line.trim().startsWith('//') || line.trim().startsWith('#')) { + continue; + } + if (line.trim().match(/^(strict)?\s*digraph/)) { + context.type = 'dot'; + } + } + } catch { + // continue regardless of error + } + } + } + + async open(context) { + const decoder = context.read('text.decoder'); + const parser = new dot.Parser(decoder); + const graph = parser.parse(); + if (graph.kind !== 'digraph') { + throw new dot.Error(`Graph type '${graph.type}' is not supported.`); + } + return new dot.Model(graph); + } +}; + +dot.Model = class { + + constructor(graph) { + this.format = 'DOT'; + this.graphs = [new dot.Graph(graph)]; + } +}; + +dot.Graph = class { + + constructor(graph) { + this.name = graph.name || ''; + this.nodes = []; + this.inputs = []; + this.outputs = []; + const values = new Map(); + values.map = (name, type, tensor, metadata) => { + if (!values.has(name) || tensor) { + values.set(name, new dot.Value(name, type, tensor, metadata)); + } + return values.get(name); + }; + const nodes = new Map(); + nodes.map = (name) => { + if (!nodes.has(name)) { + const node = {}; + node.kind = 'node'; + node.name = name; + node.inputs = []; + node.outputs = []; + node.attributes = new Map(); + node.metadata = new Map(); + nodes.set(name, node); + } + return nodes.get(name); + }; + for (const node of graph.statements) { + if (node.kind === 'node') { + node.inputs = []; + node.outputs = []; + node.metadata = new Map([...node.defaults, ...node.attributes]); + node.attributes = new Map(); + delete node.defaults; + const metadata = node.metadata; + if (metadata.has('label')) { + const label = metadata.get('label'); + if (label.startsWith('{') && label.endsWith('}')) { + const lines = label.substring(1, label.length - 1).split('|'); + if (lines.length > 1 && node.name === lines[0] && lines[1].startsWith('op_code=')) { + const def = lines[1].split('\\l'); + node.op_code = def[0].split('=').pop(); + if (node.op_code === 'call_module') { + [, node.op_code] = def; + } else if (node.op_code === 'call_function') { + const vals = lines[2].split('\\l'); + [node.op_code] = vals; + } else if (node.op_code.startsWith('get_parameter')) { + node.attributes.set('type', node.op_code.substring(13, node.op_code.length).trim()); + node.op_code = 'get_parameter'; + } + if (lines.length > 2) { + const attributes = lines[2].split('\\l'); + for (const attribute of attributes) { + const parts = attribute.split(':'); + if (parts.length === 2) { + let value = parts[1].trim(); + if (value.startsWith('(') && value.endsWith(')')) { + value = JSON.parse(`[${value.substring(1, value.length - 1)}]`); + } + node.attributes.set(parts[0].trim(), value); + } + } + } + metadata.delete('label'); + } else if (lines.length === 1 && lines[0].startsWith('buffer\\l')) { + const def = lines[0].split('\\l'); + [node.op_code] = def; + if (def.length > 1) { + node.attributes.set('type', def[1]); + } + metadata.delete('label'); + } + } else { + const match = label.match(/^name:\s*([A-Za-z][A-Za-z0-9_]*)\stype:\s*([A-Za-z][A-Za-z0-9_]*)$/); + if (match && node.name === match[1]) { + [, , node.op_code] = match; + metadata.delete('label'); + } + } + + } else { + const lines = node.name.split('\\n'); + const match = lines[0].match(/^([A-Z][A-Za-z0-9_]*)\/([A-Z][A-Za-z0-9_]*)\s\(op#(\d+)\)$/); + if (match) { + [, ,node.op_code] = match; + } else { + const match = lines[0].match(/^([A-Z][A-Za-z0-9_]*)\s\(op#(\d+)\)$/); + if (match) { + [, node.op_code] = match; + } else { + // debugger; + } + } + + } + nodes.set(node.name, node); + } + } + for (const edge of graph.statements) { + if (edge.kind === 'edge') { + edge.uses = edge.uses || []; + const to = nodes.map(edge.to); + to.inputs.push(edge); + edge.uses.push(to); + edge.from = nodes.map(edge.name); + edge.from.outputs.push(edge); + } + } + for (const [key, node] of nodes) { + if ((node.op_code === 'get_parameter' || node.op_code === 'buffer') && + node.inputs.length === 0 && + node.outputs.length === 1 && node.outputs[0].uses.length === 1) { + node.outputs[0].initializer = node; + nodes.delete(key); + } + const keys = new Set(['pos', 'height', 'width', 'shape', 'label']); + if (node.metadata.get('shape') === 'octagon' && node.metadata.keys().every((key) => keys.has(key)) && + node.inputs.length === 1 && node.inputs[0].uses.length === 1 && node.inputs[0].from.outputs.length === 1 && node.inputs[0].from.outputs[0].uses.length === 1 && + node.outputs.length === 1 && node.outputs[0].uses.length === 1) { + const [e1] = node.inputs[0].from.outputs; + const [e2] = node.outputs; + const [n2] = node.outputs[0].uses; + n2.inputs = n2.inputs.map((edge) => edge === e2 ? e1 : edge); + nodes.delete(key); + } + } + for (const [, obj] of nodes) { + const node = new dot.Node(obj, values); + this.nodes.push(node); + } + for (const edge of graph.statements) { + if (edge.kind === 'edge') { + const value = values.map(edge.name); + const metadata = new Map([...edge.defaults, ...edge.attributes]); + value.metadata = Array.from(metadata).map(([key, value]) => new dot.Argument(key, value)); + } + } + } +}; + +dot.Argument = class { + + constructor(name, value, type) { + this.name = name; + this.value = value; + this.type = type; + } +}; + +dot.Value = class { + + constructor(name, type, initializer, metadata) { + this.name = name; + this.type = !type && initializer ? initializer.type : type; + this.initializer = initializer || null; + this.metadata = metadata; + } +}; + +dot.Node = class { + + constructor(node, values) { + this.name = node.name; + this.type = { name: node.op_code || node.name || 'node' }; + this.inputs = []; + this.outputs = []; + this.attributes = []; + this.metadata = []; + for (let i = 0; i < node.inputs.length; i++) { + const edge = node.inputs[i]; + const initializer = edge.initializer ? new dot.Tensor(edge.initializer) : null; + const value = values.map(edge.name, null, initializer); + const argument = new dot.Argument(i.toString(), [value]); + this.inputs.push(argument); + } + for (let i = 0; i < node.outputs.length; i++) { + const edge = node.outputs[i]; + const value = values.map(edge.name); + const argument = new dot.Argument(i.toString(), [value]); + this.outputs.push(argument); + } + for (const [name, value] of node.attributes) { + const argument = new dot.Argument(name, value, 'attribute'); + this.attributes.push(argument); + } + for (const [name, value] of node.metadata) { + const argument = new dot.Argument(name, value); + this.metadata.push(argument); + } + } +}; + +dot.TensorType = class { + + constructor(type) { + const index = type.indexOf('['); + const dtype = type.substring(0, index); + this.dataType = dtype.split('.').pop(); + const dimensions = JSON.parse(type.substring(index, type.length)); + this.shape = new dot.TensorShape(dimensions); + } +}; + +dot.TensorShape = class { + + constructor(dimensions) { + this.dimensions = dimensions; + } +}; + +dot.Tensor = class { + + constructor(stmt) { + if (stmt.attributes.has('type')) { + const type = stmt.attributes.get('type'); + this.type = new dot.TensorType(type); + } + } +}; + +dot.Parser = class { + + constructor(decoder) { + // https://graphviz.org/doc/info/lang.html + this._tokenizer = new dot.Tokenizer(decoder); + this._token = this._tokenizer.read(); + } + + parse() { + const graph = {}; + if (this._eat('id', 'strict')) { + graph.strict = true; + } + let edgeop = ''; + if (this._match('id', 'graph')) { + graph.kind = this._read(); + edgeop = '--'; + } else if (this._match('id', 'digraph')) { + graph.kind = this._read(); + edgeop = '->'; + } else { + throw new dot.Error('Invalid graph type.'); + } + if (this._match('id')) { + graph.name = this._read(); + } + const defaults = {}; + defaults.graph = new Map(); + defaults.node = new Map(); + defaults.edge = new Map(); + graph.statements = this._parseBlock(defaults, edgeop, 0); + graph.defaults = new Map(defaults.graph); + return graph; + } + + _parseBlock(defaults, edgeop) { + defaults = { + graph: new Map(defaults.graph), + node: new Map(defaults.node), + edge: new Map(defaults.edge) + }; + const list = []; + this._read('{'); + while (!this._match('}')) { + if (this._eat('id', 'subgraph')) { + const stmt = {}; + stmt.kind = 'subgraph'; + if (this._match('id')) { + stmt.name = this._read(); + } + stmt.statements = this._parseBlock(defaults, edgeop); + } else if (this._match('{')) { + const stmt = {}; + const statements = this._parseBlock(defaults, edgeop); + if (this._eat(edgeop)) { + if (!statements.every((stmt) => stmt.kind === 'node' && stmt.attributes.size === 0)) { + throw new dot.Error('Invalid edge group statement.'); + } + const sources = statements.map((stmt) => stmt.name); + list.push(...this._parseEdges(sources, edgeop, defaults.edge)); + } else { + stmt.kind = 'subgraph'; + stmt.statements = statements; + } + } else if (this._match('id')) { + const name = this._parseNodeId(); + if (this._eat('=')) { // attr + if (this._match('id')) { + const value = this._read(); + defaults.graph.set(name, value); + } else { + throw new dot.Error('Invalid attribute value.'); + } + } else if (this._eat(edgeop)) { + list.push(...this._parseEdges([name], edgeop, defaults.edge)); + } else { + const attributes = this._parseAttributes(); + if (name === 'node' || name === 'edge' || name === 'graph') { + for (const [key, value] of attributes) { + defaults[name].set(key, value); + } + } else { + list.push({ kind: 'node', name, attributes, defaults: new Map(defaults.node) }); + } + } + } + if (this._match(';') || this._match(',')) { + this._read(); + } + } + this._read('}'); + return list; + } + + _parseNodeIds() { + const list = []; + const open = this._eat('{'); + while (!this._match('}')) { + const value = this._parseNodeId(); + list.push(value); + if (this._match(',')) { + this._read(); + continue; + } else if (this._match(';')) { + this._read(); + if (!open) { + break; + } + } else if (!open) { + break; + } + } + if (open) { + this._read('}'); + } + return list; + } + + _parseNodeId(name) { + let value = name || this._read('id'); + if (this._match(':')) { + value += this._read(); + value += this._read('id'); + if (this._match(':')) { + value += this._read(); + value += this._read('id'); + } + } + return value; + } + + _parseAttributes() { + const table = new Map(); + if (this._eat('[')) { + while (this._match('id')) { + const name = this._read('id'); + this._read('='); + const value = this._read('id'); + table.set(name, value); + if (this._match(';') || this._match(',')) { + this._read(); + } + } + this._read(']'); + } + return table; + } + + _parseEdges(sources, edgeop, defaults) { + const list = []; + do { + const targets = this._parseNodeIds(); + for (const name of sources) { + for (const to of targets) { + list.push({ kind: 'edge', name, to }); + } + } + sources = targets; + } while (this._eat(edgeop)); + const attributes = this._parseAttributes(); + for (const edge of list) { + edge.attributes = attributes; + edge.defaults = new Map(defaults.edge); + } + return list; + } + + _match(kind, value) { + return (this._token.kind === kind && (!value || this._token.value === value)); + } + + _read(kind, value) { + if (kind && this._token.kind !== kind) { + throw new dot.Error(`Expected token of type '${kind}', but got '${this._token.kind}' ${this._tokenizer.location()}`); + } + if (value && this._token.value !== value) { + throw new dot.Error(`Expected token with value '${value}', but got '${this._token.value}' ${this._tokenizer.location()}`); + } + const token = this._token; + this._token = this._tokenizer.read(); + return token.value; + } + + _eat(kind, value) { + if (this._match(kind, value)) { + return this._read(); + } + return null; + } +}; + +dot.Tokenizer = class { + + constructor(decoder) { + this._decoder = decoder; + this._position = 0; + this._char = this._decoder.decode(); + } + + _seek(position) { + this._decoder.position = position; + this._char = ''; + this._read(); + } + + _read() { + if (this._char === undefined) { + this._unexpected(); + } + const char = this._char; + this._position = this._decoder.position; + this._char = this._decoder.decode(); + return char; + } + + _peek() { + const position = this._decoder.position; + const char = this._decoder.decode(); + this._decoder.position = position; + return char; + } + + read() { + while (this._char) { + switch (this._char) { + case ' ': + case '\t': + case '\n': + case '\r': + case '\f': + this._skipWhitespace(); + continue; + case '/': + case '#': + this._skipComment(); + continue; + case '{': + case '}': + case '[': + case ']': + case '=': + case ':': + case ';': + case ',': { + const value = this._read(); + return { kind: value, value }; + } + case '-': { + let value = this._read(); + if (this._char === '>' || this._char === '-') { + value += this._read(); + return { kind: value, value }; + } + throw new dot.Error(`Unexpected character '${value}' ${this.location()}`); + } + default: { + if (/[a-zA-Z0-9_$"<]/.test(this._char)) { + const value = this._identifier(); + return { kind: 'id', value }; + } + throw new dot.Error(`Unexpected character '${this._char}' ${this.location()}`); + } + } + } + return { type: 'eof' }; + } + + _skipWhitespace() { + while (this._char !== undefined && (this._char === ' ' || this._char === '\t' || this._char === '\n' || this._char === '\r' || this._char === '\f')) { + this._read(); + } + } + + _skipComment() { + if (this._char === '#' || (this._char === '/' && this._peek() === '/')) { + while (this._char && this._char !== '\n') { + this._read(); + } + this._skipWhitespace(); + if (this._char === '/') { + this._skipComment(); + } + return; + } + if (this._char === '/' && this._peek() === '*') { + while (this._char && (this._char !== '*' || this._peek() !== '/')) { + this._read(); + } + this._read(); + this._read(); + this._skipWhitespace(); + if (this._char === '/') { + this._skipComment(); + } + return; + } + throw new dot.Error('Invalid comment.'); + } + + _identifier() { + let value = ''; + if (this._char === '"') { // double quoted string + this._read(); + while (this._char && this._char !== '"') { + value += this._read(); + } + this._read('"'); + } if (this._char === '<') { // HTML String + value += this._read(); + let level = 0; + while (level > 0 || this._char !== '>') { + const c = this._read(); + value += c; + if (c === '<') { + level += 1; + } else if (c === '>') { + level -= 1; + } + } + value += this._read(); + } else { + while (/[a-zA-Z0-9_$.*]/.test(this._char)) { + value += this._read(); + } + } + return value; + } + + _unexpected() { + let c = this._char; + if (c === undefined) { + throw new dot.Error('Unexpected end of input.'); + } else if (c === '"') { + c = 'string'; + } else if ((c >= '0' && c <= '9') || c === '-') { + c = 'number'; + } else { + if (c < ' ' || c > '\x7F') { + const name = Object.keys(this._escape).filter((key) => this._escape[key] === c); + c = (name.length === 1) ? `\\${name}` : `\\u${(`000${c.charCodeAt(0).toString(16)}`).slice(-4)}`; + } + c = `token '${c}'`; + } + this._throw(`Unexpected ${c}`); + } + + _throw(message) { + message = message.replace(/\.$/, ''); + throw new dot.Error(`${message} ${this._location()}`); + } + + location() { + let line = 1; + let column = 1; + const position = this._decoder.position; + this._decoder.position = 0; + let c = ''; + do { + if (this._decoder.position === this._position) { + this._decoder.position = position; + return `at ${line}:${column}.`; + } + c = this._decoder.decode(); + if (c === '\n') { + line++; + column = 1; + } else { + column++; + } + } + while (c !== undefined); + this._decoder.position = position; + return `at ${line}:${column}.`; + } +}; + +dot.Error = class extends Error { + + constructor(message) { + super(message); + this.name = 'Error loadig DOT graph'; + } +}; + +export const ModelFactory = dot.ModelFactory; diff --git a/source/graphviz.js b/source/graphviz.js deleted file mode 100644 index b0cf0537ad..0000000000 --- a/source/graphviz.js +++ /dev/null @@ -1,36 +0,0 @@ - -const graphviz = {}; - -graphviz.ModelFactory = class { - - match(context) { - const reader = context.read('text', 0x10000); - if (reader) { - try { - const line = reader.read('\n'); - if (line === undefined) { - return; - } - if (line.indexOf('digraph') !== -1) { - context.type = 'graphviz.dot'; - } - } catch { - // continue regardless of error - } - } - } - - async open(/* context */) { - throw new graphviz.Error('Invalid file content. File contains Graphviz data.'); - } -}; - -graphviz.Error = class extends Error { - - constructor(message) { - super(message); - this.name = 'Error loading Graphviz model.'; - } -}; - -export const ModelFactory = graphviz.ModelFactory; diff --git a/source/mlir.js b/source/mlir.js index 08f9143836..8de96b9654 100644 --- a/source/mlir.js +++ b/source/mlir.js @@ -585,24 +585,31 @@ mlir.Tokenizer = class { } _skipComment() { - if (this._eat('/')) { + this._read('/'); + if (this._current === '/') { + while (this._current && this._current !== '\n') { + this._read(); + } + this._skipWhitespace(); if (this._current === '/') { - while (this._current && this._current !== '\n') { - this._read(); - } - this._skipWhitespace(); this._skipComment(); - } else if (this._current === '*') { - while (this._current) { - this._read(); - if (this._eat('*') && this._eat('/')) { - break; - } + } + return; + } + if (this._current === '*') { + while (this._current) { + this._read(); + if (this._eat('*') && this._eat('/')) { + break; } - this._skipWhitespace(); + } + this._skipWhitespace(); + if (this._current === '/') { this._skipComment(); } + return; } + throw new mlir.Error('Invalid comment.'); } _number() { diff --git a/source/onnx.js b/source/onnx.js index 7ba9702186..2f25f44cac 100644 --- a/source/onnx.js +++ b/source/onnx.js @@ -39,7 +39,7 @@ onnx.ModelFactory = class { } filter(context, type) { - return context.type !== 'onnx.proto' || (type !== 'onnx.data' && type !== 'graphviz.dot'); + return context.type !== 'onnx.proto' || (type !== 'onnx.data' && type !== 'dot'); } }; diff --git a/source/view.js b/source/view.js index 6cb4959f13..7630e98c1f 100644 --- a/source/view.js +++ b/source/view.js @@ -3436,6 +3436,12 @@ view.ConnectionSidebar = class extends view.ObjectSidebar { this.addHeader('Outputs'); this.addNodeList('to', to); } + if (Array.isArray(value.metadata) && value.metadata.length > 0) { + this.addHeader('Metadata'); + for (const metadata of value.metadata) { + this.addProperty(metadata.name, metadata.value); + } + } } addNodeList(name, list) { @@ -5812,7 +5818,7 @@ view.ModelFactoryService = class { this.register('./nnc', ['.nnc','.tflite']); this.register('./safetensors', ['.safetensors', '.safetensors.index.json']); this.register('./tvm', ['.json', '.params']); - this.register('./graphviz', ['.dot']); + this.register('./dot', ['.dot']); this.register('./catboost', ['.cbm']); this.register('./weka', ['.model']); this.register('./qnn', ['.json', '.bin', '.serialized']); diff --git a/test/models.json b/test/models.json index bd7e752984..f11266bf63 100644 --- a/test/models.json +++ b/test/models.json @@ -2098,27 +2098,96 @@ "link": "https://github.com/lutzroeder/netron/issues/334" }, { - "type": "graphviz", + "type": "dot", + "target": "chart_fr1.dot", + "source": "https://github.com/user-attachments/files/17928647/chart_fr1.dot.zip[chart_fr1.dot]", + "format": "DOT", + "link": "https://github.com/lutzroeder/netron/issues/1368" + }, + { + "type": "dot", + "target": "dataflow.dot", + "source": "https://github.com/user-attachments/files/17928508/dataflow.dot.zip[dataflow.dot]", + "format": "DOT", + "link": "https://github.com/lutzroeder/netron/issues/1368" + }, + { + "type": "dot", + "target": "edges.dot", + "source": "https://github.com/user-attachments/files/17928569/edges.dot.zip[edges.dot]", + "format": "DOT", + "link": "https://github.com/lutzroeder/netron/issues/1368" + }, + { + "type": "dot", "target": "EfficientDet-d0.onnx.dot", "source": "https://github.com/user-attachments/files/17180084/EfficientDet-d0.onnx.dot.zip[EfficientDet-d0.onnx.dot]", - "format": "Graphviz", - "error": "Invalid file content. File contains Graphviz data.", + "format": "DOT", + "tags": "skip-render", + "link": "https://github.com/lutzroeder/netron/issues/1368" + }, + { + "type": "dot", + "target": "fg.dot", + "source": "https://github.com/user-attachments/files/17928899/fg.dot.zip[fg.dot]", + "format": "DOT", "link": "https://github.com/lutzroeder/netron/issues/1368" }, { - "type": "graphviz", + "type": "dot", "target": "gpt2-10.onnx.dot", "source": "https://github.com/user-attachments/files/17180085/gpt2-10.onnx.dot.zip[gpt2-10.onnx.dot]", - "format": "Graphviz", - "error": "Invalid file content. File contains Graphviz data.", + "format": "DOT", + "link": "https://github.com/lutzroeder/netron/issues/1368" + }, + { + "type": "dot", + "target": "homekey.dot", + "source": "https://github.com/user-attachments/files/17928509/homekey.dot.zip[homekey.dot]", + "format": "DOT", "link": "https://github.com/lutzroeder/netron/issues/1368" }, { - "type": "graphviz", + "type": "dot", + "target": "mb_v1_ssd_with_flag.dot", + "source": "https://github.com/user-attachments/files/17928515/mb_v1_ssd_with_flag.dot.zip[mb_v1_ssd_with_flag.dot]", + "format": "DOT", + "link": "https://github.com/lutzroeder/netron/issues/1368" + }, + { + "type": "dot", + "target": "mobilenetv1.dot", + "source": "https://github.com/user-attachments/files/17928510/mobilenetv1.dot.zip[mobilenetv1.dot]", + "format": "DOT", + "link": "https://github.com/lutzroeder/netron/issues/1368" + }, + { + "type": "dot", + "target": "nth.dot", + "source": "https://github.com/user-attachments/files/17928645/nth.dot.zip[nth.dot]", + "format": "DOT", + "link": "https://github.com/lutzroeder/netron/issues/1368" + }, + { + "type": "dot", + "target": "pilot.dot", + "source": "https://github.com/user-attachments/files/17928648/pilot.dot.zip[pilot.dot]", + "format": "DOT", + "link": "https://github.com/lutzroeder/netron/issues/1368" + }, + { + "type": "dot", "target": "resnet18.dot", "source": "https://github.com/user-attachments/files/17175694/resnet18.dot.zip[resnet18.dot]", - "format": "Graphviz", - "error": "Invalid file content. File contains Graphviz data.", + "format": "DOT", + "assert": "model.graphs[0].nodes.length == 71", + "link": "https://github.com/lutzroeder/netron/issues/1368" + }, + { + "type": "dot", + "target": "squeezenet.dot", + "source": "https://github.com/user-attachments/files/17928516/squeezenet.dot.zip[squeezenet.dot]", + "format": "DOT", "link": "https://github.com/lutzroeder/netron/issues/1368" }, {