|
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; |
|
|
|
|
|
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) { |
|
|
|
store_size += zi.compressed_size; |
|
} else { |
|
compr_size += zi.compressed_size; |
|
} |
|
} |
|
let zip_overhead = file_size - store_size - compr_size; |
|
|
|
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)) { |
|
|
|
return true; |
|
} |
|
if (data.__tuple_values__) { |
|
|
|
return true; |
|
} |
|
if (data.__is_dict__) { |
|
|
|
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"); |
|
} |
|
|
|
|
|
|
|
|
|
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++) { |
|
|
|
parts.push(html`<br/><${StructuredData} prefix=${idx + ": "} indent=${new_indent} data=${data[idx]} />`); |
|
} |
|
return parts; |
|
} |
|
if (data.__tuple_values__) { |
|
|
|
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__) { |
|
|
|
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])) { |
|
|
|
} 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__) { |
|
|
|
|
|
|
|
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}}) { |
|
|
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
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); |
|
|