123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689 |
- import { h, Component, render } from 'https://unpkg.com/preact?module';
- import htm from 'https://unpkg.com/htm?module';
- const html = htm.bind(h);
- const BURNED_IN_MODEL_INFO = null;
- // https://stackoverflow.com/a/20732091
- function humanFileSize(size) {
- if (size == 0) { return "0 B"; }
- var i = Math.floor( Math.log(size) / Math.log(1024) );
- return (size / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i];
- }
- function caret(down) {
- return down ? "\u25BE" : "\u25B8";
- }
- class Blamer {
- constructor() {
- this.blame_on_click = false;
- this.aux_content_pane = null;
- }
- setAuxContentPane(pane) {
- this.aux_content_pane = pane;
- }
- readyBlame() {
- this.blame_on_click = true;
- }
- maybeBlame(arg) {
- if (!this.blame_on_click) {
- return;
- }
- this.blame_on_click = false;
- if (!this.aux_content_pane) {
- return;
- }
- this.aux_content_pane.doBlame(arg);
- }
- }
- let blame = new Blamer();
- class Hider extends Component {
- constructor() {
- super();
- this.state = { shown: null };
- }
- componentDidMount() {
- this.setState({ shown: this.props.shown === "true" });
- }
- render({name, children}, {shown}) {
- let my_caret = html`<span class=caret onClick=${() => this.click()} >${caret(shown)}</span>`;
- return html`<div data-hider-title=${name} data-shown=${shown}>
- <h2>${my_caret} ${name}</h2>
- <div>${shown ? this.props.children : []}</div></div>`;
- }
- click() {
- this.setState({shown: !this.state.shown});
- }
- }
- function ModelSizeSection({model: {file_size, zip_files}}) {
- let store_size = 0;
- let compr_size = 0;
- for (const zi of zip_files) {
- if (zi.compression === 0) {
- // TODO: Maybe check that compressed_size === file_size.
- store_size += zi.compressed_size;
- } else {
- compr_size += zi.compressed_size;
- }
- }
- let zip_overhead = file_size - store_size - compr_size;
- // TODO: Better formatting. Right-align this.
- return html`
- <${Hider} name="Model Size" shown=true>
- <pre>.
- Model size: ${file_size} (${humanFileSize(file_size)})
- Stored files: ${store_size} (${humanFileSize(store_size)})
- Compressed files: ${compr_size} (${humanFileSize(compr_size)})
- Zip overhead: ${zip_overhead} (${humanFileSize(zip_overhead)})
- </pre><//>`;
- }
- function StructuredDataSection({name, data, shown}) {
- return html`
- <${Hider} name=${name} shown=${shown}>
- <div style="font-family:monospace;">
- <${StructuredData} data=${data} indent="" prefix=""/>
- </div><//>`;
- }
- class StructuredData extends Component {
- constructor() {
- super();
- this.state = { shown: false };
- this.INLINE_TYPES = new Set(["boolean", "number", "string"])
- this.IGNORED_STATE_KEYS = new Set(["training", "_is_full_backward_hook"])
- }
- click() {
- this.setState({shown: !this.state.shown});
- }
- expando(data) {
- if (data === null || this.INLINE_TYPES.has(typeof(data))) {
- return false;
- }
- if (typeof(data) != "object") {
- throw new Error("Not an object");
- }
- if (Array.isArray(data)) {
- // TODO: Maybe show simple lists and tuples on one line.
- return true;
- }
- if (data.__tuple_values__) {
- // TODO: Maybe show simple lists and tuples on one line.
- return true;
- }
- if (data.__is_dict__) {
- // TODO: Maybe show simple (empty?) dicts on one line.
- return true;
- }
- if (data.__module_type__) {
- return true;
- }
- if (data.__tensor_v2__) {
- return false;
- }
- if (data.__qtensor__) {
- return false;
- }
- throw new Error("Can't handle data type.", data);
- }
- renderHeadline(data) {
- if (data === null) {
- return "None";
- }
- if (typeof(data) == "boolean") {
- const sd = String(data);
- return sd.charAt(0).toUpperCase() + sd.slice(1);
- }
- if (typeof(data) == "number") {
- return JSON.stringify(data);
- }
- if (typeof(data) == "string") {
- return JSON.stringify(data);
- }
- if (typeof(data) != "object") {
- throw new Error("Not an object");
- }
- if (Array.isArray(data)) {
- return "list([";
- }
- if (data.__tuple_values__) {
- return "tuple((";
- }
- if (data.__is_dict__) {
- return "dict({";
- }
- if (data.__module_type__) {
- return data.__module_type__ + "()";
- }
- if (data.__tensor_v2__) {
- const [storage, offset, size, stride, grad] = data.__tensor_v2__;
- const [dtype, key, device, numel] = storage;
- return this.renderTensor(
- "tensor", dtype, key, device, numel, offset, size, stride, grad, []);
- }
- if (data.__qtensor__) {
- const [storage, offset, size, stride, quantizer, grad] = data.__qtensor__;
- const [dtype, key, device, numel] = storage;
- let extra_parts = [];
- if (quantizer[0] == "per_tensor_affine") {
- extra_parts.push(`scale=${quantizer[1]}`);
- extra_parts.push(`zero_point=${quantizer[2]}`);
- } else {
- extra_parts.push(`quantizer=${quantizer[0]}`);
- }
- return this.renderTensor(
- "qtensor", dtype, key, device, numel, offset, size, stride, grad, extra_parts);
- }
- throw new Error("Can't handle data type.", data);
- }
- renderTensor(
- prefix,
- dtype,
- storage_key,
- device,
- storage_numel,
- offset,
- size,
- stride,
- grad,
- extra_parts) {
- let parts = [
- "(" + size.join(",") + ")",
- dtype,
- ];
- parts.push(...extra_parts);
- if (device != "cpu") {
- parts.push(device);
- }
- if (grad) {
- parts.push("grad");
- }
- // TODO: Check stride and indicate if the tensor is channels-last or non-contiguous
- // TODO: Check size, stride, offset, and numel and indicate if
- // the tensor doesn't use all data in storage.
- // TODO: Maybe show key?
- void(offset);
- void(stride);
- void(storage_key);
- void(storage_numel);
- return prefix + "(" + parts.join(", ") + ")";
- }
- renderBody(indent, data) {
- if (data === null || this.INLINE_TYPES.has(typeof(data))) {
- throw "Should not reach here."
- }
- if (typeof(data) != "object") {
- throw new Error("Not an object");
- }
- if (Array.isArray(data)) {
- let new_indent = indent + "\u00A0\u00A0";
- let parts = [];
- for (let idx = 0; idx < data.length; idx++) {
- // Does it make sense to put explicit index numbers here?
- parts.push(html`<br/><${StructuredData} prefix=${idx + ": "} indent=${new_indent} data=${data[idx]} />`);
- }
- return parts;
- }
- if (data.__tuple_values__) {
- // Handled the same as lists.
- return this.renderBody(indent, data.__tuple_values__);
- }
- if (data.__is_dict__) {
- let new_indent = indent + "\u00A0\u00A0";
- let parts = [];
- for (let idx = 0; idx < data.keys.length; idx++) {
- if (typeof(data.keys[idx]) != "string") {
- parts.push(html`<br/>${new_indent}Non-string key`);
- } else {
- parts.push(html`<br/><${StructuredData} prefix=${data.keys[idx] + ": "} indent=${new_indent} data=${data.values[idx]} />`);
- }
- }
- return parts;
- }
- if (data.__module_type__) {
- const mstate = data.state;
- if (mstate === null || typeof(mstate) != "object") {
- throw new Error("Bad module state");
- }
- let new_indent = indent + "\u00A0\u00A0";
- let parts = [];
- if (mstate.__is_dict__) {
- // TODO: Less copy/paste between this and normal dicts.
- for (let idx = 0; idx < mstate.keys.length; idx++) {
- if (typeof(mstate.keys[idx]) != "string") {
- parts.push(html`<br/>${new_indent}Non-string key`);
- } else if (this.IGNORED_STATE_KEYS.has(mstate.keys[idx])) {
- // Do nothing.
- } else {
- parts.push(html`<br/><${StructuredData} prefix=${mstate.keys[idx] + ": "} indent=${new_indent} data=${mstate.values[idx]} />`);
- }
- }
- } else if (mstate.__tuple_values__) {
- parts.push(html`<br/><${StructuredData} prefix="" indent=${new_indent} data=${mstate} />`);
- } else if (mstate.__module_type__) {
- // We normally wouldn't have the state of a module be another module,
- // but we use "modules" to encode special values (like Unicode decode
- // errors) that might be valid states. Just go with it.
- parts.push(html`<br/><${StructuredData} prefix="" indent=${new_indent} data=${mstate} />`);
- } else {
- throw new Error("Bad module state");
- }
- return parts;
- }
- if (data.__tensor_v2__) {
- throw "Should not reach here."
- }
- if (data.__qtensor__) {
- throw "Should not reach here."
- }
- throw new Error("Can't handle data type.", data);
- }
- render({data, indent, prefix}, {shown}) {
- const exp = this.expando(data) ? html`<span class=caret onClick=${() => this.click()} >${caret(shown)} </span>` : "";
- const headline = this.renderHeadline(data);
- const body = shown ? this.renderBody(indent, data) : "";
- return html`${indent}${exp}${prefix}${headline}${body}`;
- }
- }
- function ZipContentsSection({model: {zip_files}}) {
- // TODO: Add human-readable sizes?
- // TODO: Add sorting options?
- // TODO: Add hierarchical collapsible tree?
- return html`
- <${Hider} name="Zip Contents" shown=false>
- <table>
- <thead>
- <tr>
- <th>Mode</th>
- <th>Size</th>
- <th>Compressed</th>
- <th>Name</th>
- </tr>
- </thead>
- <tbody style="font-family:monospace;">
- ${zip_files.map(zf => html`<tr>
- <td>${{0: "store", 8: "deflate"}[zf.compression] || zf.compression}</td>
- <td>${zf.file_size}</td>
- <td>${zf.compressed_size}</td>
- <td>${zf.filename}</td>
- </tr>`)}
- </tbody>
- </table><//>`;
- }
- function CodeSection({model: {code_files}}) {
- return html`
- <${Hider} name="Code" shown=false>
- <div>
- ${Object.entries(code_files).map(([fn, code]) => html`<${OneCodeSection}
- filename=${fn} code=${code} />`)}
- </div><//>`;
- }
- class OneCodeSection extends Component {
- constructor() {
- super();
- this.state = { shown: false };
- }
- click() {
- const shown = !this.state.shown;
- this.setState({shown: shown});
- }
- render({filename, code}, {shown}) {
- const header = html`
- <h3 style="font-family:monospace;">
- <span class=caret onClick=${() => this.click()} >${caret(shown)} </span>
- ${filename}</h3>
- `;
- if (!shown) {
- return header;
- }
- return html`
- ${header}
- <pre>${code.map(c => this.renderBlock(c))}</pre>
- `;
- }
- renderBlock([text, ist_file, line, ist_s_text, s_start, s_end]) {
- return html`<span
- onClick=${() => blame.maybeBlame({ist_file, line, ist_s_text, s_start, s_end})}
- >${text}</span>`;
- }
- }
- function ExtraJsonSection({files}) {
- return html`
- <${Hider} name="Extra files (JSON)" shown=false>
- <div>
- <p>Use "Log Raw Model Info" for hierarchical view in browser console.</p>
- ${Object.entries(files).map(([fn, json]) => html`<${OneJsonSection}
- filename=${fn} json=${json} />`)}
- </div><//>`;
- }
- class OneJsonSection extends Component {
- constructor() {
- super();
- this.state = { shown: false };
- }
- click() {
- const shown = !this.state.shown;
- this.setState({shown: shown});
- }
- render({filename, json}, {shown}) {
- const header = html`
- <h3 style="font-family:monospace;">
- <span class=caret onClick=${() => this.click()} >${caret(shown)} </span>
- ${filename}</h3>
- `;
- if (!shown) {
- return header;
- }
- return html`
- ${header}
- <pre>${JSON.stringify(json, null, 2)}</pre>
- `;
- }
- }
- function ExtraPicklesSection({files}) {
- return html`
- <${Hider} name="Extra Pickles" shown=false>
- <div>
- ${Object.entries(files).map(([fn, content]) => html`<${OnePickleSection}
- filename=${fn} content=${content} />`)}
- </div><//>`;
- }
- class OnePickleSection extends Component {
- constructor() {
- super();
- this.state = { shown: false };
- }
- click() {
- const shown = !this.state.shown;
- this.setState({shown: shown});
- }
- render({filename, content}, {shown}) {
- const header = html`
- <h3 style="font-family:monospace;">
- <span class=caret onClick=${() => this.click()} >${caret(shown)} </span>
- ${filename}</h3>
- `;
- if (!shown) {
- return header;
- }
- return html`
- ${header}
- <pre>${content}</pre>
- `;
- }
- }
- function assertStorageAreEqual(key, lhs, rhs) {
- if (lhs.length !== rhs.length ||
- !lhs.every((val, idx) => val === rhs[idx])) {
- throw new Error("Storage mismatch for key '" + key + "'");
- }
- }
- function computeTensorMemory(numel, dtype) {
- const sizes = {
- "Byte": 1,
- "Char": 1,
- "Short": 2,
- "Int": 4,
- "Long": 8,
- "Half": 2,
- "Float": 4,
- "Double": 8,
- "ComplexHalf": 4,
- "ComplexFloat": 8,
- "ComplexDouble": 16,
- "Bool": 1,
- "QInt8": 1,
- "QUInt8": 1,
- "QInt32": 4,
- "BFloat16": 2,
- };
- let dtsize = sizes[dtype];
- if (!dtsize) {
- throw new Error("Unrecognized dtype: " + dtype);
- }
- return numel * dtsize;
- }
- // TODO: Maybe track by dtype as well.
- // TODO: Maybe distinguish between visible size and storage size.
- function getTensorStorages(data) {
- if (data === null) {
- return new Map();
- }
- if (typeof(data) == "boolean") {
- return new Map();
- }
- if (typeof(data) == "number") {
- return new Map();
- }
- if (typeof(data) == "string") {
- return new Map();
- }
- if (typeof(data) != "object") {
- throw new Error("Not an object");
- }
- if (Array.isArray(data)) {
- let result = new Map();
- for (const item of data) {
- const tensors = getTensorStorages(item);
- for (const [key, storage] of tensors.entries()) {
- if (!result.has(key)) {
- result.set(key, storage);
- } else {
- const old_storage = result.get(key);
- assertStorageAreEqual(key, old_storage, storage);
- }
- }
- }
- return result;
- }
- if (data.__tuple_values__) {
- return getTensorStorages(data.__tuple_values__);
- }
- if (data.__is_dict__) {
- return getTensorStorages(data.values);
- }
- if (data.__module_type__) {
- return getTensorStorages(data.state);
- }
- if (data.__tensor_v2__) {
- const [storage, offset, size, stride, grad] = data.__tensor_v2__;
- const [dtype, key, device, numel] = storage;
- return new Map([[key, storage]]);
- }
- if (data.__qtensor__) {
- const [storage, offset, size, stride, quantizer, grad] = data.__qtensor__;
- const [dtype, key, device, numel] = storage;
- return new Map([[key, storage]]);
- }
- throw new Error("Can't handle data type.", data);
- }
- function getTensorMemoryByDevice(pickles) {
- let all_tensors = [];
- for (const [name, pickle] of pickles) {
- const tensors = getTensorStorages(pickle);
- all_tensors.push(...tensors.values());
- }
- let result = {};
- for (const storage of all_tensors.values()) {
- const [dtype, key, device, numel] = storage;
- const size = computeTensorMemory(numel, dtype);
- result[device] = (result[device] || 0) + size;
- }
- return result;
- }
- // Make this a separate component so it is rendered lazily.
- class OpenTensorMemorySection extends Component {
- render({model: {model_data, constants}}) {
- let sizes = getTensorMemoryByDevice(new Map([
- ["data", model_data],
- ["constants", constants],
- ]));
- return html`
- <table>
- <thead>
- <tr>
- <th>Device</th>
- <th>Bytes</th>
- <th>Human</th>
- </tr>
- </thead>
- <tbody style="font-family:monospace;">
- ${Object.entries(sizes).map(([dev, size]) => html`<tr>
- <td>${dev}</td>
- <td>${size}</td>
- <td>${humanFileSize(size)}</td>
- </tr>`)}
- </tbody>
- </table>`;
- }
- }
- function TensorMemorySection({model}) {
- return html`
- <${Hider} name="Tensor Memory" shown=false>
- <${OpenTensorMemorySection} model=${model} /><//>`;
- }
- class AuxContentPane extends Component {
- constructor() {
- super();
- this.state = {
- blame_info: null,
- };
- }
- doBlame(arg) {
- this.setState({...this.state, blame_info: arg});
- }
- render({model: {interned_strings}}, {blame_info}) {
- let blame_content = "";
- if (blame_info) {
- const {ist_file, line, ist_s_text, s_start, s_end} = blame_info;
- let s_text = interned_strings[ist_s_text];
- if (s_start != 0 || s_end != s_text.length) {
- let prefix = s_text.slice(0, s_start);
- let main = s_text.slice(s_start, s_end);
- let suffix = s_text.slice(s_end);
- s_text = html`${prefix}<strong>${main}</strong>${suffix}`;
- }
- blame_content = html`
- <h3>${interned_strings[ist_file]}:${line}</h3>
- <pre>${s_start}:${s_end}</pre>
- <pre>${s_text}</pre><br/>
- `;
- }
- return html`
- <button onClick=${() => blame.readyBlame()}>Blame Code</button>
- <br/>
- ${blame_content}
- `;
- }
- }
- class App extends Component {
- constructor() {
- super();
- this.state = {
- err: false,
- model: null,
- };
- }
- componentDidMount() {
- const app = this;
- if (BURNED_IN_MODEL_INFO !== null) {
- app.setState({model: BURNED_IN_MODEL_INFO});
- } else {
- fetch("./model_info.json").then(function(response) {
- if (!response.ok) {
- throw new Error("Response not ok.");
- }
- return response.json();
- }).then(function(body) {
- app.setState({model: body});
- }).catch(function(error) {
- console.log("Top-level error: ", error);
- });
- }
- }
- componentDidCatch(error) {
- void(error);
- this.setState({...this.state, err: true});
- }
- render(_, {err}) {
- if (this.state.model === null) {
- return html`<h1>Loading...</h1>`;
- }
- const model = this.state.model.model;
- let error_msg = "";
- if (err) {
- error_msg = html`<h2 style="background:red">An error occurred. Check console</h2>`;
- }
- return html`
- ${error_msg}
- <div id=main_content style="position:absolute;width:99%;height:79%;overflow:scroll">
- <h1>TorchScript Model (version ${model.version}): ${model.title}</h1>
- <button onClick=${() => console.log(model)}>Log Raw Model Info</button>
- <${ModelSizeSection} model=${model}/>
- <${StructuredDataSection} name="Model Data" data=${model.model_data} shown=true/>
- <${StructuredDataSection} name="Constants" data=${model.constants} shown=false/>
- <${ZipContentsSection} model=${model}/>
- <${CodeSection} model=${model}/>
- <${ExtraJsonSection} files=${model.extra_files_jsons}/>
- <${ExtraPicklesSection} files=${model.extra_pickles}/>
- <${TensorMemorySection} model=${model}/>
- </div>
- <div id=aux_content style="position:absolute;width:99%;top:80%;height:20%;overflow:scroll">
- <${AuxContentPane}
- err=${this.state.error}
- model=${model}
- ref=${(p) => blame.setAuxContentPane(p)}/>
- </div>
- `;
- }
- }
- render(h(App), document.body);
|