Spaces:
Sleeping
Sleeping
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); | |