Initial import: grid-bot — grid trading bot for BTC-USDT on Cifra Markets
This commit is contained in:
@@ -0,0 +1,450 @@
|
||||
#!/usr/bin/env python3
|
||||
# mypy: allow-untyped-defs
|
||||
"""
|
||||
model_dump: a one-stop shop for TorchScript model inspection.
|
||||
|
||||
The goal of this tool is to provide a simple way to extract lots of
|
||||
useful information from a TorchScript model and make it easy for humans
|
||||
to consume. It (mostly) replaces zipinfo, common uses of show_pickle,
|
||||
and various ad-hoc analysis notebooks.
|
||||
|
||||
The tool extracts information from the model and serializes it as JSON.
|
||||
That JSON can then be rendered by an HTML+JS page, either by
|
||||
loading the JSON over HTTP or producing a fully self-contained page
|
||||
with all of the code and data burned-in.
|
||||
"""
|
||||
|
||||
# Maintainer notes follow.
|
||||
"""
|
||||
The implementation strategy has tension between 3 goals:
|
||||
- Small file size.
|
||||
- Fully self-contained.
|
||||
- Easy, modern JS environment.
|
||||
Using Preact and HTM achieves 1 and 2 with a decent result for 3.
|
||||
However, the models I tested with result in ~1MB JSON output,
|
||||
so even using something heavier like full React might be tolerable
|
||||
if the build process can be worked out.
|
||||
|
||||
One principle I have followed that I think is very beneficial
|
||||
is to keep the JSON data as close as possible to the model
|
||||
and do most of the rendering logic on the client.
|
||||
This makes for easier development (just refresh, usually),
|
||||
allows for more laziness and dynamism, and lets us add more
|
||||
views of the same data without bloating the HTML file.
|
||||
|
||||
Currently, this code doesn't actually load the model or even
|
||||
depend on any part of PyTorch. I don't know if that's an important
|
||||
feature to maintain, but it's probably worth preserving the ability
|
||||
to run at least basic analysis on models that cannot be loaded.
|
||||
|
||||
I think the easiest way to develop this code is to cd into model_dump and
|
||||
run "python -m http.server", then load http://localhost:8000/skeleton.html
|
||||
in the browser. In another terminal, run
|
||||
"python -m torch.utils.model_dump --style=json FILE > \
|
||||
torch/utils/model_dump/model_info.json"
|
||||
every time you update the Python code or model.
|
||||
When you update JS, just refresh.
|
||||
|
||||
Possible improvements:
|
||||
- Fix various TODO comments in this file and the JS.
|
||||
- Make the HTML much less janky, especially the auxiliary data panel.
|
||||
- Make the auxiliary data panel start small, expand when
|
||||
data is available, and have a button to clear/contract.
|
||||
- Clean up the JS. There's a lot of copypasta because
|
||||
I don't really know how to use Preact.
|
||||
- Make the HTML render and work nicely inside a Jupyter notebook.
|
||||
- Add the ability for JS to choose the URL to load the JSON based
|
||||
on the page URL (query or hash). That way we could publish the
|
||||
inlined skeleton once and have it load various JSON blobs.
|
||||
- Add a button to expand all expandable sections so ctrl-F works well.
|
||||
- Add hyperlinking from data to code, and code to code.
|
||||
- Add hyperlinking from debug info to Diffusion.
|
||||
- Make small tensor contents available.
|
||||
- Do something nice for quantized models
|
||||
(they probably don't work at all right now).
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import io
|
||||
import itertools
|
||||
import json
|
||||
import os
|
||||
import pickle
|
||||
import pprint
|
||||
import re
|
||||
import sys
|
||||
import urllib.parse
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
import warnings
|
||||
|
||||
import torch.utils.show_pickle
|
||||
|
||||
|
||||
DEFAULT_EXTRA_FILE_SIZE_LIMIT = 16 * 1024
|
||||
|
||||
__all__ = ['get_storage_info', 'hierarchical_pickle', 'get_model_info', 'get_inline_skeleton',
|
||||
'burn_in_info', 'get_info_and_burn_skeleton']
|
||||
|
||||
def get_storage_info(storage):
|
||||
if not isinstance(storage, torch.utils.show_pickle.FakeObject):
|
||||
raise AssertionError(f"storage is not FakeObject: {type(storage)}")
|
||||
if storage.module != "pers":
|
||||
raise AssertionError(f"storage.module is not 'pers': {storage.module!r}")
|
||||
if storage.name != "obj":
|
||||
raise AssertionError(f"storage.name is not 'obj': {storage.name!r}")
|
||||
if storage.state is not None:
|
||||
raise AssertionError(f"storage.state is not None: {storage.state!r}")
|
||||
if not isinstance(storage.args, tuple):
|
||||
raise AssertionError(f"storage.args is not a tuple: {type(storage.args)}")
|
||||
if len(storage.args) != 1:
|
||||
raise AssertionError(f"len(storage.args) is not 1: {len(storage.args)}")
|
||||
sa = storage.args[0]
|
||||
if not isinstance(sa, tuple):
|
||||
raise AssertionError(f"sa is not a tuple: {type(sa)}")
|
||||
if len(sa) != 5:
|
||||
raise AssertionError(f"len(sa) is not 5: {len(sa)}")
|
||||
if sa[0] != "storage":
|
||||
raise AssertionError(f"sa[0] is not 'storage': {sa[0]!r}")
|
||||
if not isinstance(sa[1], torch.utils.show_pickle.FakeClass):
|
||||
raise AssertionError(f"sa[1] is not FakeClass: {type(sa[1])}")
|
||||
if sa[1].module != "torch":
|
||||
raise AssertionError(f"sa[1].module is not 'torch': {sa[1].module!r}")
|
||||
if not sa[1].name.endswith("Storage"):
|
||||
raise AssertionError(f"sa[1].name does not end with 'Storage': {sa[1].name!r}")
|
||||
storage_info = [sa[1].name.replace("Storage", "")] + list(sa[2:])
|
||||
return storage_info
|
||||
|
||||
|
||||
def hierarchical_pickle(data):
|
||||
if isinstance(data, (bool, int, float, str, type(None))):
|
||||
return data
|
||||
if isinstance(data, list):
|
||||
return [hierarchical_pickle(d) for d in data]
|
||||
if isinstance(data, tuple):
|
||||
return {
|
||||
"__tuple_values__": hierarchical_pickle(list(data)),
|
||||
}
|
||||
if isinstance(data, dict):
|
||||
return {
|
||||
"__is_dict__": True,
|
||||
"keys": hierarchical_pickle(list(data.keys())),
|
||||
"values": hierarchical_pickle(list(data.values())),
|
||||
}
|
||||
if isinstance(data, torch.utils.show_pickle.FakeObject):
|
||||
typename = f"{data.module}.{data.name}"
|
||||
if (
|
||||
typename.startswith(('__torch__.', 'torch.jit.LoweredWrapper.', 'torch.jit.LoweredModule.'))
|
||||
):
|
||||
if data.args != ():
|
||||
raise AssertionError("data.args is not ()")
|
||||
return {
|
||||
"__module_type__": typename,
|
||||
"state": hierarchical_pickle(data.state),
|
||||
}
|
||||
if typename == "torch._utils._rebuild_tensor_v2":
|
||||
if data.state is not None:
|
||||
raise AssertionError("data.state is not None")
|
||||
storage, offset, size, stride, requires_grad, *_ = data.args
|
||||
storage_info = get_storage_info(storage)
|
||||
return {"__tensor_v2__": [storage_info, offset, size, stride, requires_grad]}
|
||||
if typename == "torch._utils._rebuild_qtensor":
|
||||
if data.state is not None:
|
||||
raise AssertionError("data.state is not None")
|
||||
storage, offset, size, stride, quantizer, requires_grad, *_ = data.args
|
||||
storage_info = get_storage_info(storage)
|
||||
if not isinstance(quantizer, tuple):
|
||||
raise AssertionError("quantizer is not a tuple")
|
||||
if not isinstance(quantizer[0], torch.utils.show_pickle.FakeClass):
|
||||
raise AssertionError("quantizer[0] is not a FakeClass")
|
||||
if quantizer[0].module != "torch":
|
||||
raise AssertionError("quantizer[0].module is not torch")
|
||||
if quantizer[0].name == "per_tensor_affine":
|
||||
if len(quantizer) != 3:
|
||||
raise AssertionError("len(quantizer) is not 3")
|
||||
if not isinstance(quantizer[1], float):
|
||||
raise AssertionError("quantizer[1] is not a float")
|
||||
if not isinstance(quantizer[2], int):
|
||||
raise AssertionError("quantizer[2] is not an int")
|
||||
quantizer_extra = list(quantizer[1:3])
|
||||
else:
|
||||
quantizer_extra = []
|
||||
quantizer_json = [quantizer[0].name] + quantizer_extra
|
||||
return {"__qtensor__": [storage_info, offset, size, stride, quantizer_json, requires_grad]}
|
||||
if typename == "torch.jit._pickle.restore_type_tag":
|
||||
if data.state is not None:
|
||||
raise AssertionError("data.state is not None")
|
||||
obj, typ = data.args
|
||||
if not isinstance(typ, str):
|
||||
raise AssertionError("typ is not a string")
|
||||
return hierarchical_pickle(obj)
|
||||
if re.fullmatch(r"torch\.jit\._pickle\.build_[a-z]+list", typename):
|
||||
if data.state is not None:
|
||||
raise AssertionError("data.state is not None")
|
||||
ls, = data.args
|
||||
if not isinstance(ls, list):
|
||||
raise AssertionError("ls is not a list")
|
||||
return hierarchical_pickle(ls)
|
||||
if typename == "torch.device":
|
||||
if data.state is not None:
|
||||
raise AssertionError("data.state is not None")
|
||||
name, = data.args
|
||||
if not isinstance(name, str):
|
||||
raise AssertionError("name is not a string")
|
||||
# Just forget that it was a device and return the name.
|
||||
return name
|
||||
if typename == "builtin.UnicodeDecodeError":
|
||||
if data.state is not None:
|
||||
raise AssertionError("data.state is not None")
|
||||
msg, = data.args
|
||||
if not isinstance(msg, str):
|
||||
raise AssertionError("msg is not a string")
|
||||
# Hack: Pretend this is a module so we don't need custom serialization.
|
||||
# Hack: Wrap the message in a tuple so it looks like a nice state object.
|
||||
# TODO: Undo at least that second hack. We should support string states.
|
||||
return {
|
||||
"__module_type__": typename,
|
||||
"state": hierarchical_pickle((msg,)),
|
||||
}
|
||||
raise Exception(f"Can't prepare fake object of type for JS: {typename}") # noqa: TRY002
|
||||
raise Exception(f"Can't prepare data of type for JS: {type(data)}") # noqa: TRY002
|
||||
|
||||
|
||||
def get_model_info(
|
||||
path_or_file,
|
||||
title=None,
|
||||
extra_file_size_limit=DEFAULT_EXTRA_FILE_SIZE_LIMIT):
|
||||
"""Get JSON-friendly information about a model.
|
||||
|
||||
The result is suitable for being saved as model_info.json,
|
||||
or passed to burn_in_info.
|
||||
"""
|
||||
|
||||
if isinstance(path_or_file, os.PathLike):
|
||||
default_title = os.fspath(path_or_file)
|
||||
file_size = path_or_file.stat().st_size # type: ignore[attr-defined]
|
||||
elif isinstance(path_or_file, str):
|
||||
default_title = path_or_file
|
||||
file_size = Path(path_or_file).stat().st_size
|
||||
else:
|
||||
default_title = "buffer"
|
||||
path_or_file.seek(0, io.SEEK_END)
|
||||
file_size = path_or_file.tell()
|
||||
path_or_file.seek(0)
|
||||
|
||||
title = title or default_title
|
||||
|
||||
with zipfile.ZipFile(path_or_file) as zf:
|
||||
path_prefix = None
|
||||
zip_files = []
|
||||
# pyrefly: ignore [bad-assignment]
|
||||
for zi in zf.infolist():
|
||||
prefix = re.sub("/.*", "", zi.filename)
|
||||
if path_prefix is None:
|
||||
path_prefix = prefix
|
||||
elif prefix != path_prefix:
|
||||
raise Exception(f"Mismatched prefixes: {path_prefix} != {prefix}") # noqa: TRY002
|
||||
zip_files.append(
|
||||
{
|
||||
"filename": zi.filename,
|
||||
"compression": zi.compress_type,
|
||||
"compressed_size": zi.compress_size,
|
||||
"file_size": zi.file_size,
|
||||
}
|
||||
)
|
||||
if path_prefix is None:
|
||||
raise AssertionError("path_prefix is None")
|
||||
version = zf.read(path_prefix + "/version").decode("utf-8").strip()
|
||||
|
||||
def get_pickle(name):
|
||||
if path_prefix is None:
|
||||
raise AssertionError("path_prefix is None")
|
||||
with zf.open(path_prefix + f"/{name}.pkl") as handle:
|
||||
raw = torch.utils.show_pickle.DumpUnpickler(handle, catch_invalid_utf8=True).load()
|
||||
return hierarchical_pickle(raw)
|
||||
|
||||
model_data = get_pickle("data")
|
||||
constants = get_pickle("constants")
|
||||
|
||||
# Intern strings that are likely to be reused.
|
||||
# Pickle automatically detects shared structure,
|
||||
# so reused strings are stored efficiently.
|
||||
# However, JSON has no way of representing this,
|
||||
# so we have to do it manually.
|
||||
interned_strings : dict[str, int] = {}
|
||||
|
||||
def intern(s):
|
||||
if s not in interned_strings:
|
||||
interned_strings[s] = len(interned_strings)
|
||||
return interned_strings[s]
|
||||
|
||||
code_files = {}
|
||||
for zi in zf.infolist():
|
||||
if not zi.filename.endswith(".py"):
|
||||
continue
|
||||
with zf.open(zi) as handle:
|
||||
raw_code = handle.read()
|
||||
with zf.open(zi.filename + ".debug_pkl") as handle:
|
||||
raw_debug = handle.read()
|
||||
|
||||
# Parse debug info and add begin/end markers if not present
|
||||
# to ensure that we cover the entire source code.
|
||||
debug_info_t = pickle.loads(raw_debug)
|
||||
text_table = None
|
||||
|
||||
if (len(debug_info_t) == 3 and
|
||||
isinstance(debug_info_t[0], str) and
|
||||
debug_info_t[0] == 'FORMAT_WITH_STRING_TABLE'):
|
||||
_, text_table, content = debug_info_t
|
||||
|
||||
def parse_new_format(line):
|
||||
# (0, (('', '', 0), 0, 0))
|
||||
num, ((text_indexes, fname_idx, offset), start, end), tag = line
|
||||
text = ''.join(text_table[x] for x in text_indexes) # type: ignore[index]
|
||||
fname = text_table[fname_idx] # type: ignore[index]
|
||||
return num, ((text, fname, offset), start, end), tag
|
||||
|
||||
debug_info_t = map(parse_new_format, content)
|
||||
|
||||
debug_info = list(debug_info_t)
|
||||
if not debug_info:
|
||||
debug_info.append((0, (('', '', 0), 0, 0)))
|
||||
if debug_info[-1][0] != len(raw_code):
|
||||
debug_info.append((len(raw_code), (('', '', 0), 0, 0)))
|
||||
|
||||
code_parts = []
|
||||
for di, di_next in itertools.pairwise(debug_info):
|
||||
start, source_range, *_ = di
|
||||
end = di_next[0]
|
||||
if end <= start:
|
||||
raise AssertionError("end is not greater than start")
|
||||
source, s_start, s_end = source_range
|
||||
s_text, s_file, s_line = source
|
||||
# TODO: Handle this case better. TorchScript ranges are in bytes,
|
||||
# but JS doesn't really handle byte strings.
|
||||
# if bytes and chars are not equivalent for this string,
|
||||
# zero out the ranges so we don't highlight the wrong thing.
|
||||
if len(s_text) != len(s_text.encode("utf-8")):
|
||||
s_start = 0
|
||||
s_end = 0
|
||||
text = raw_code[start:end]
|
||||
code_parts.append([text.decode("utf-8"), intern(s_file), s_line, intern(s_text), s_start, s_end])
|
||||
code_files[zi.filename] = code_parts
|
||||
|
||||
extra_files_json_pattern = re.compile(re.escape(path_prefix) + "/extra/.*\\.json")
|
||||
extra_files_jsons = {}
|
||||
for zi in zf.infolist():
|
||||
if not extra_files_json_pattern.fullmatch(zi.filename):
|
||||
continue
|
||||
if zi.file_size > extra_file_size_limit:
|
||||
continue
|
||||
with zf.open(zi) as handle:
|
||||
try:
|
||||
json_content = json.load(handle)
|
||||
extra_files_jsons[zi.filename] = json_content
|
||||
except json.JSONDecodeError:
|
||||
extra_files_jsons[zi.filename] = "INVALID JSON"
|
||||
|
||||
always_render_pickles = {
|
||||
"bytecode.pkl",
|
||||
}
|
||||
extra_pickles = {}
|
||||
for zi in zf.infolist():
|
||||
if not zi.filename.endswith(".pkl"):
|
||||
continue
|
||||
with zf.open(zi) as handle:
|
||||
# TODO: handle errors here and just ignore the file?
|
||||
# NOTE: For a lot of these files (like bytecode),
|
||||
# we could get away with just unpickling, but this should be safer.
|
||||
obj = torch.utils.show_pickle.DumpUnpickler(handle, catch_invalid_utf8=True).load()
|
||||
buf = io.StringIO()
|
||||
pprint.pprint(obj, buf)
|
||||
contents = buf.getvalue()
|
||||
# Checked the rendered length instead of the file size
|
||||
# because pickles with shared structure can explode in size during rendering.
|
||||
if os.path.basename(zi.filename) not in always_render_pickles and \
|
||||
len(contents) > extra_file_size_limit:
|
||||
continue
|
||||
extra_pickles[zi.filename] = contents
|
||||
|
||||
return {
|
||||
"model": {
|
||||
"title": title,
|
||||
"file_size": file_size,
|
||||
"version": version,
|
||||
"zip_files": zip_files,
|
||||
"interned_strings": list(interned_strings),
|
||||
"code_files": code_files,
|
||||
"model_data": model_data,
|
||||
"constants": constants,
|
||||
"extra_files_jsons": extra_files_jsons,
|
||||
"extra_pickles": extra_pickles,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def get_inline_skeleton():
|
||||
"""Get a fully-inlined skeleton of the frontend.
|
||||
|
||||
The returned HTML page has no external network dependencies for code.
|
||||
It can load model_info.json over HTTP, or be passed to burn_in_info.
|
||||
"""
|
||||
|
||||
import importlib.resources
|
||||
|
||||
# pyrefly: ignore [bad-argument-type]
|
||||
skeleton = importlib.resources.read_text(__package__, "skeleton.html")
|
||||
# pyrefly: ignore [bad-argument-type]
|
||||
js_code = importlib.resources.read_text(__package__, "code.js")
|
||||
for js_module in ["preact", "htm"]:
|
||||
# pyrefly: ignore [bad-argument-type]
|
||||
js_lib = importlib.resources.read_binary(__package__, f"{js_module}.mjs")
|
||||
js_url = "data:application/javascript," + urllib.parse.quote(js_lib)
|
||||
js_code = js_code.replace(f"https://unpkg.com/{js_module}?module", js_url)
|
||||
skeleton = skeleton.replace(' src="./code.js">', ">\n" + js_code)
|
||||
return skeleton
|
||||
|
||||
|
||||
def burn_in_info(skeleton, info):
|
||||
"""Burn model info into the HTML skeleton.
|
||||
|
||||
The result will render the hard-coded model info and
|
||||
have no external network dependencies for code or data.
|
||||
"""
|
||||
|
||||
# Note that Python's json serializer does not escape slashes in strings.
|
||||
# Since we're inlining this JSON directly into a script tag, a string
|
||||
# containing "</script>" would end the script prematurely and
|
||||
# mess up our page. Unconditionally escape fixes that.
|
||||
return skeleton.replace(
|
||||
"BURNED_IN_MODEL_INFO = null",
|
||||
"BURNED_IN_MODEL_INFO = " + json.dumps(info, sort_keys=True).replace("/", "\\/"))
|
||||
|
||||
|
||||
def get_info_and_burn_skeleton(path_or_bytesio, **kwargs):
|
||||
model_info = get_model_info(path_or_bytesio, **kwargs)
|
||||
skeleton = get_inline_skeleton()
|
||||
page = burn_in_info(skeleton, model_info)
|
||||
return page
|
||||
|
||||
|
||||
def main(argv, *, stdout=None) -> None:
|
||||
warnings.warn("torch.utils.model_dump is deprecated and will be removed in a future PyTorch release.", stacklevel=2)
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--style", choices=["json", "html"])
|
||||
parser.add_argument("--title")
|
||||
parser.add_argument("model")
|
||||
args = parser.parse_args(argv[1:])
|
||||
|
||||
info = get_model_info(args.model, title=args.title)
|
||||
|
||||
output = stdout or sys.stdout
|
||||
|
||||
if args.style == "json":
|
||||
output.write(json.dumps(info, sort_keys=True) + "\n")
|
||||
elif args.style == "html":
|
||||
skeleton = get_inline_skeleton()
|
||||
page = burn_in_info(skeleton, info)
|
||||
output.write(page)
|
||||
else:
|
||||
raise Exception("Invalid style") # noqa: TRY002
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
from . import main
|
||||
|
||||
sys.exit(main(sys.argv))
|
||||
@@ -0,0 +1,689 @@
|
||||
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);
|
||||
@@ -0,0 +1,2 @@
|
||||
// HTM, Apache License
|
||||
var n=function(t,s,r,e){var u;s[0]=0;for(var h=1;h<s.length;h++){var p=s[h++],a=s[h]?(s[0]|=p?1:2,r[s[h++]]):s[++h];3===p?e[0]=a:4===p?e[1]=Object.assign(e[1]||{},a):5===p?(e[1]=e[1]||{})[s[++h]]=a:6===p?e[1][s[++h]]+=a+"":p?(u=t.apply(a,n(t,a,r,["",null])),e.push(u),a[0]?s[0]|=2:(s[h-2]=0,s[h]=u)):e.push(a)}return e},t=new Map;export default function(s){var r=t.get(this);return r||(r=new Map,t.set(this,r)),(r=n(this,r.get(s)||(r.set(s,r=function(n){for(var t,s,r=1,e="",u="",h=[0],p=function(n){1===r&&(n||(e=e.replace(/^\s*\n\s*|\s*\n\s*$/g,"")))?h.push(0,n,e):3===r&&(n||e)?(h.push(3,n,e),r=2):2===r&&"..."===e&&n?h.push(4,n,0):2===r&&e&&!n?h.push(5,0,!0,e):r>=5&&((e||!n&&5===r)&&(h.push(r,0,e,s),r=6),n&&(h.push(r,n,0,s),r=6)),e=""},a=0;a<n.length;a++){a&&(1===r&&p(),p(a));for(var l=0;l<n[a].length;l++)t=n[a][l],1===r?"<"===t?(p(),h=[h],r=3):e+=t:4===r?"--"===e&&">"===t?(r=1,e=""):e=t+e[0]:u?t===u?u="":e+=t:'"'===t||"'"===t?u=t:">"===t?(p(),r=1):r&&("="===t?(r=5,s=e,e=""):"/"===t&&(r<5||">"===n[a][l+1])?(p(),3===r&&(h=h[0]),r=h,(h=h[0]).push(2,0,r),r=0):" "===t||"\t"===t||"\n"===t||"\r"===t?(p(),r=2):e+=t),3===r&&"!--"===e&&(r=4,h=h[0])}return p(),h}(s)),r),arguments,[])).length>1?r:r[0]}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>TorchScript Model</title>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
table, th, td {
|
||||
border: 1px solid black;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.caret {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
<script type="module" src="./code.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user