code.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689
  1. import { h, Component, render } from 'https://unpkg.com/preact?module';
  2. import htm from 'https://unpkg.com/htm?module';
  3. const html = htm.bind(h);
  4. const BURNED_IN_MODEL_INFO = null;
  5. // https://stackoverflow.com/a/20732091
  6. function humanFileSize(size) {
  7. if (size == 0) { return "0 B"; }
  8. var i = Math.floor( Math.log(size) / Math.log(1024) );
  9. return (size / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i];
  10. }
  11. function caret(down) {
  12. return down ? "\u25BE" : "\u25B8";
  13. }
  14. class Blamer {
  15. constructor() {
  16. this.blame_on_click = false;
  17. this.aux_content_pane = null;
  18. }
  19. setAuxContentPane(pane) {
  20. this.aux_content_pane = pane;
  21. }
  22. readyBlame() {
  23. this.blame_on_click = true;
  24. }
  25. maybeBlame(arg) {
  26. if (!this.blame_on_click) {
  27. return;
  28. }
  29. this.blame_on_click = false;
  30. if (!this.aux_content_pane) {
  31. return;
  32. }
  33. this.aux_content_pane.doBlame(arg);
  34. }
  35. }
  36. let blame = new Blamer();
  37. class Hider extends Component {
  38. constructor() {
  39. super();
  40. this.state = { shown: null };
  41. }
  42. componentDidMount() {
  43. this.setState({ shown: this.props.shown === "true" });
  44. }
  45. render({name, children}, {shown}) {
  46. let my_caret = html`<span class=caret onClick=${() => this.click()} >${caret(shown)}</span>`;
  47. return html`<div data-hider-title=${name} data-shown=${shown}>
  48. <h2>${my_caret} ${name}</h2>
  49. <div>${shown ? this.props.children : []}</div></div>`;
  50. }
  51. click() {
  52. this.setState({shown: !this.state.shown});
  53. }
  54. }
  55. function ModelSizeSection({model: {file_size, zip_files}}) {
  56. let store_size = 0;
  57. let compr_size = 0;
  58. for (const zi of zip_files) {
  59. if (zi.compression === 0) {
  60. // TODO: Maybe check that compressed_size === file_size.
  61. store_size += zi.compressed_size;
  62. } else {
  63. compr_size += zi.compressed_size;
  64. }
  65. }
  66. let zip_overhead = file_size - store_size - compr_size;
  67. // TODO: Better formatting. Right-align this.
  68. return html`
  69. <${Hider} name="Model Size" shown=true>
  70. <pre>.
  71. Model size: ${file_size} (${humanFileSize(file_size)})
  72. Stored files: ${store_size} (${humanFileSize(store_size)})
  73. Compressed files: ${compr_size} (${humanFileSize(compr_size)})
  74. Zip overhead: ${zip_overhead} (${humanFileSize(zip_overhead)})
  75. </pre><//>`;
  76. }
  77. function StructuredDataSection({name, data, shown}) {
  78. return html`
  79. <${Hider} name=${name} shown=${shown}>
  80. <div style="font-family:monospace;">
  81. <${StructuredData} data=${data} indent="" prefix=""/>
  82. </div><//>`;
  83. }
  84. class StructuredData extends Component {
  85. constructor() {
  86. super();
  87. this.state = { shown: false };
  88. this.INLINE_TYPES = new Set(["boolean", "number", "string"])
  89. this.IGNORED_STATE_KEYS = new Set(["training", "_is_full_backward_hook"])
  90. }
  91. click() {
  92. this.setState({shown: !this.state.shown});
  93. }
  94. expando(data) {
  95. if (data === null || this.INLINE_TYPES.has(typeof(data))) {
  96. return false;
  97. }
  98. if (typeof(data) != "object") {
  99. throw new Error("Not an object");
  100. }
  101. if (Array.isArray(data)) {
  102. // TODO: Maybe show simple lists and tuples on one line.
  103. return true;
  104. }
  105. if (data.__tuple_values__) {
  106. // TODO: Maybe show simple lists and tuples on one line.
  107. return true;
  108. }
  109. if (data.__is_dict__) {
  110. // TODO: Maybe show simple (empty?) dicts on one line.
  111. return true;
  112. }
  113. if (data.__module_type__) {
  114. return true;
  115. }
  116. if (data.__tensor_v2__) {
  117. return false;
  118. }
  119. if (data.__qtensor__) {
  120. return false;
  121. }
  122. throw new Error("Can't handle data type.", data);
  123. }
  124. renderHeadline(data) {
  125. if (data === null) {
  126. return "None";
  127. }
  128. if (typeof(data) == "boolean") {
  129. const sd = String(data);
  130. return sd.charAt(0).toUpperCase() + sd.slice(1);
  131. }
  132. if (typeof(data) == "number") {
  133. return JSON.stringify(data);
  134. }
  135. if (typeof(data) == "string") {
  136. return JSON.stringify(data);
  137. }
  138. if (typeof(data) != "object") {
  139. throw new Error("Not an object");
  140. }
  141. if (Array.isArray(data)) {
  142. return "list([";
  143. }
  144. if (data.__tuple_values__) {
  145. return "tuple((";
  146. }
  147. if (data.__is_dict__) {
  148. return "dict({";
  149. }
  150. if (data.__module_type__) {
  151. return data.__module_type__ + "()";
  152. }
  153. if (data.__tensor_v2__) {
  154. const [storage, offset, size, stride, grad] = data.__tensor_v2__;
  155. const [dtype, key, device, numel] = storage;
  156. return this.renderTensor(
  157. "tensor", dtype, key, device, numel, offset, size, stride, grad, []);
  158. }
  159. if (data.__qtensor__) {
  160. const [storage, offset, size, stride, quantizer, grad] = data.__qtensor__;
  161. const [dtype, key, device, numel] = storage;
  162. let extra_parts = [];
  163. if (quantizer[0] == "per_tensor_affine") {
  164. extra_parts.push(`scale=${quantizer[1]}`);
  165. extra_parts.push(`zero_point=${quantizer[2]}`);
  166. } else {
  167. extra_parts.push(`quantizer=${quantizer[0]}`);
  168. }
  169. return this.renderTensor(
  170. "qtensor", dtype, key, device, numel, offset, size, stride, grad, extra_parts);
  171. }
  172. throw new Error("Can't handle data type.", data);
  173. }
  174. renderTensor(
  175. prefix,
  176. dtype,
  177. storage_key,
  178. device,
  179. storage_numel,
  180. offset,
  181. size,
  182. stride,
  183. grad,
  184. extra_parts) {
  185. let parts = [
  186. "(" + size.join(",") + ")",
  187. dtype,
  188. ];
  189. parts.push(...extra_parts);
  190. if (device != "cpu") {
  191. parts.push(device);
  192. }
  193. if (grad) {
  194. parts.push("grad");
  195. }
  196. // TODO: Check stride and indicate if the tensor is channels-last or non-contiguous
  197. // TODO: Check size, stride, offset, and numel and indicate if
  198. // the tensor doesn't use all data in storage.
  199. // TODO: Maybe show key?
  200. void(offset);
  201. void(stride);
  202. void(storage_key);
  203. void(storage_numel);
  204. return prefix + "(" + parts.join(", ") + ")";
  205. }
  206. renderBody(indent, data) {
  207. if (data === null || this.INLINE_TYPES.has(typeof(data))) {
  208. throw "Should not reach here."
  209. }
  210. if (typeof(data) != "object") {
  211. throw new Error("Not an object");
  212. }
  213. if (Array.isArray(data)) {
  214. let new_indent = indent + "\u00A0\u00A0";
  215. let parts = [];
  216. for (let idx = 0; idx < data.length; idx++) {
  217. // Does it make sense to put explicit index numbers here?
  218. parts.push(html`<br/><${StructuredData} prefix=${idx + ": "} indent=${new_indent} data=${data[idx]} />`);
  219. }
  220. return parts;
  221. }
  222. if (data.__tuple_values__) {
  223. // Handled the same as lists.
  224. return this.renderBody(indent, data.__tuple_values__);
  225. }
  226. if (data.__is_dict__) {
  227. let new_indent = indent + "\u00A0\u00A0";
  228. let parts = [];
  229. for (let idx = 0; idx < data.keys.length; idx++) {
  230. if (typeof(data.keys[idx]) != "string") {
  231. parts.push(html`<br/>${new_indent}Non-string key`);
  232. } else {
  233. parts.push(html`<br/><${StructuredData} prefix=${data.keys[idx] + ": "} indent=${new_indent} data=${data.values[idx]} />`);
  234. }
  235. }
  236. return parts;
  237. }
  238. if (data.__module_type__) {
  239. const mstate = data.state;
  240. if (mstate === null || typeof(mstate) != "object") {
  241. throw new Error("Bad module state");
  242. }
  243. let new_indent = indent + "\u00A0\u00A0";
  244. let parts = [];
  245. if (mstate.__is_dict__) {
  246. // TODO: Less copy/paste between this and normal dicts.
  247. for (let idx = 0; idx < mstate.keys.length; idx++) {
  248. if (typeof(mstate.keys[idx]) != "string") {
  249. parts.push(html`<br/>${new_indent}Non-string key`);
  250. } else if (this.IGNORED_STATE_KEYS.has(mstate.keys[idx])) {
  251. // Do nothing.
  252. } else {
  253. parts.push(html`<br/><${StructuredData} prefix=${mstate.keys[idx] + ": "} indent=${new_indent} data=${mstate.values[idx]} />`);
  254. }
  255. }
  256. } else if (mstate.__tuple_values__) {
  257. parts.push(html`<br/><${StructuredData} prefix="" indent=${new_indent} data=${mstate} />`);
  258. } else if (mstate.__module_type__) {
  259. // We normally wouldn't have the state of a module be another module,
  260. // but we use "modules" to encode special values (like Unicode decode
  261. // errors) that might be valid states. Just go with it.
  262. parts.push(html`<br/><${StructuredData} prefix="" indent=${new_indent} data=${mstate} />`);
  263. } else {
  264. throw new Error("Bad module state");
  265. }
  266. return parts;
  267. }
  268. if (data.__tensor_v2__) {
  269. throw "Should not reach here."
  270. }
  271. if (data.__qtensor__) {
  272. throw "Should not reach here."
  273. }
  274. throw new Error("Can't handle data type.", data);
  275. }
  276. render({data, indent, prefix}, {shown}) {
  277. const exp = this.expando(data) ? html`<span class=caret onClick=${() => this.click()} >${caret(shown)} </span>` : "";
  278. const headline = this.renderHeadline(data);
  279. const body = shown ? this.renderBody(indent, data) : "";
  280. return html`${indent}${exp}${prefix}${headline}${body}`;
  281. }
  282. }
  283. function ZipContentsSection({model: {zip_files}}) {
  284. // TODO: Add human-readable sizes?
  285. // TODO: Add sorting options?
  286. // TODO: Add hierarchical collapsible tree?
  287. return html`
  288. <${Hider} name="Zip Contents" shown=false>
  289. <table>
  290. <thead>
  291. <tr>
  292. <th>Mode</th>
  293. <th>Size</th>
  294. <th>Compressed</th>
  295. <th>Name</th>
  296. </tr>
  297. </thead>
  298. <tbody style="font-family:monospace;">
  299. ${zip_files.map(zf => html`<tr>
  300. <td>${{0: "store", 8: "deflate"}[zf.compression] || zf.compression}</td>
  301. <td>${zf.file_size}</td>
  302. <td>${zf.compressed_size}</td>
  303. <td>${zf.filename}</td>
  304. </tr>`)}
  305. </tbody>
  306. </table><//>`;
  307. }
  308. function CodeSection({model: {code_files}}) {
  309. return html`
  310. <${Hider} name="Code" shown=false>
  311. <div>
  312. ${Object.entries(code_files).map(([fn, code]) => html`<${OneCodeSection}
  313. filename=${fn} code=${code} />`)}
  314. </div><//>`;
  315. }
  316. class OneCodeSection extends Component {
  317. constructor() {
  318. super();
  319. this.state = { shown: false };
  320. }
  321. click() {
  322. const shown = !this.state.shown;
  323. this.setState({shown: shown});
  324. }
  325. render({filename, code}, {shown}) {
  326. const header = html`
  327. <h3 style="font-family:monospace;">
  328. <span class=caret onClick=${() => this.click()} >${caret(shown)} </span>
  329. ${filename}</h3>
  330. `;
  331. if (!shown) {
  332. return header;
  333. }
  334. return html`
  335. ${header}
  336. <pre>${code.map(c => this.renderBlock(c))}</pre>
  337. `;
  338. }
  339. renderBlock([text, ist_file, line, ist_s_text, s_start, s_end]) {
  340. return html`<span
  341. onClick=${() => blame.maybeBlame({ist_file, line, ist_s_text, s_start, s_end})}
  342. >${text}</span>`;
  343. }
  344. }
  345. function ExtraJsonSection({files}) {
  346. return html`
  347. <${Hider} name="Extra files (JSON)" shown=false>
  348. <div>
  349. <p>Use "Log Raw Model Info" for hierarchical view in browser console.</p>
  350. ${Object.entries(files).map(([fn, json]) => html`<${OneJsonSection}
  351. filename=${fn} json=${json} />`)}
  352. </div><//>`;
  353. }
  354. class OneJsonSection extends Component {
  355. constructor() {
  356. super();
  357. this.state = { shown: false };
  358. }
  359. click() {
  360. const shown = !this.state.shown;
  361. this.setState({shown: shown});
  362. }
  363. render({filename, json}, {shown}) {
  364. const header = html`
  365. <h3 style="font-family:monospace;">
  366. <span class=caret onClick=${() => this.click()} >${caret(shown)} </span>
  367. ${filename}</h3>
  368. `;
  369. if (!shown) {
  370. return header;
  371. }
  372. return html`
  373. ${header}
  374. <pre>${JSON.stringify(json, null, 2)}</pre>
  375. `;
  376. }
  377. }
  378. function ExtraPicklesSection({files}) {
  379. return html`
  380. <${Hider} name="Extra Pickles" shown=false>
  381. <div>
  382. ${Object.entries(files).map(([fn, content]) => html`<${OnePickleSection}
  383. filename=${fn} content=${content} />`)}
  384. </div><//>`;
  385. }
  386. class OnePickleSection extends Component {
  387. constructor() {
  388. super();
  389. this.state = { shown: false };
  390. }
  391. click() {
  392. const shown = !this.state.shown;
  393. this.setState({shown: shown});
  394. }
  395. render({filename, content}, {shown}) {
  396. const header = html`
  397. <h3 style="font-family:monospace;">
  398. <span class=caret onClick=${() => this.click()} >${caret(shown)} </span>
  399. ${filename}</h3>
  400. `;
  401. if (!shown) {
  402. return header;
  403. }
  404. return html`
  405. ${header}
  406. <pre>${content}</pre>
  407. `;
  408. }
  409. }
  410. function assertStorageAreEqual(key, lhs, rhs) {
  411. if (lhs.length !== rhs.length ||
  412. !lhs.every((val, idx) => val === rhs[idx])) {
  413. throw new Error("Storage mismatch for key '" + key + "'");
  414. }
  415. }
  416. function computeTensorMemory(numel, dtype) {
  417. const sizes = {
  418. "Byte": 1,
  419. "Char": 1,
  420. "Short": 2,
  421. "Int": 4,
  422. "Long": 8,
  423. "Half": 2,
  424. "Float": 4,
  425. "Double": 8,
  426. "ComplexHalf": 4,
  427. "ComplexFloat": 8,
  428. "ComplexDouble": 16,
  429. "Bool": 1,
  430. "QInt8": 1,
  431. "QUInt8": 1,
  432. "QInt32": 4,
  433. "BFloat16": 2,
  434. };
  435. let dtsize = sizes[dtype];
  436. if (!dtsize) {
  437. throw new Error("Unrecognized dtype: " + dtype);
  438. }
  439. return numel * dtsize;
  440. }
  441. // TODO: Maybe track by dtype as well.
  442. // TODO: Maybe distinguish between visible size and storage size.
  443. function getTensorStorages(data) {
  444. if (data === null) {
  445. return new Map();
  446. }
  447. if (typeof(data) == "boolean") {
  448. return new Map();
  449. }
  450. if (typeof(data) == "number") {
  451. return new Map();
  452. }
  453. if (typeof(data) == "string") {
  454. return new Map();
  455. }
  456. if (typeof(data) != "object") {
  457. throw new Error("Not an object");
  458. }
  459. if (Array.isArray(data)) {
  460. let result = new Map();
  461. for (const item of data) {
  462. const tensors = getTensorStorages(item);
  463. for (const [key, storage] of tensors.entries()) {
  464. if (!result.has(key)) {
  465. result.set(key, storage);
  466. } else {
  467. const old_storage = result.get(key);
  468. assertStorageAreEqual(key, old_storage, storage);
  469. }
  470. }
  471. }
  472. return result;
  473. }
  474. if (data.__tuple_values__) {
  475. return getTensorStorages(data.__tuple_values__);
  476. }
  477. if (data.__is_dict__) {
  478. return getTensorStorages(data.values);
  479. }
  480. if (data.__module_type__) {
  481. return getTensorStorages(data.state);
  482. }
  483. if (data.__tensor_v2__) {
  484. const [storage, offset, size, stride, grad] = data.__tensor_v2__;
  485. const [dtype, key, device, numel] = storage;
  486. return new Map([[key, storage]]);
  487. }
  488. if (data.__qtensor__) {
  489. const [storage, offset, size, stride, quantizer, grad] = data.__qtensor__;
  490. const [dtype, key, device, numel] = storage;
  491. return new Map([[key, storage]]);
  492. }
  493. throw new Error("Can't handle data type.", data);
  494. }
  495. function getTensorMemoryByDevice(pickles) {
  496. let all_tensors = [];
  497. for (const [name, pickle] of pickles) {
  498. const tensors = getTensorStorages(pickle);
  499. all_tensors.push(...tensors.values());
  500. }
  501. let result = {};
  502. for (const storage of all_tensors.values()) {
  503. const [dtype, key, device, numel] = storage;
  504. const size = computeTensorMemory(numel, dtype);
  505. result[device] = (result[device] || 0) + size;
  506. }
  507. return result;
  508. }
  509. // Make this a separate component so it is rendered lazily.
  510. class OpenTensorMemorySection extends Component {
  511. render({model: {model_data, constants}}) {
  512. let sizes = getTensorMemoryByDevice(new Map([
  513. ["data", model_data],
  514. ["constants", constants],
  515. ]));
  516. return html`
  517. <table>
  518. <thead>
  519. <tr>
  520. <th>Device</th>
  521. <th>Bytes</th>
  522. <th>Human</th>
  523. </tr>
  524. </thead>
  525. <tbody style="font-family:monospace;">
  526. ${Object.entries(sizes).map(([dev, size]) => html`<tr>
  527. <td>${dev}</td>
  528. <td>${size}</td>
  529. <td>${humanFileSize(size)}</td>
  530. </tr>`)}
  531. </tbody>
  532. </table>`;
  533. }
  534. }
  535. function TensorMemorySection({model}) {
  536. return html`
  537. <${Hider} name="Tensor Memory" shown=false>
  538. <${OpenTensorMemorySection} model=${model} /><//>`;
  539. }
  540. class AuxContentPane extends Component {
  541. constructor() {
  542. super();
  543. this.state = {
  544. blame_info: null,
  545. };
  546. }
  547. doBlame(arg) {
  548. this.setState({...this.state, blame_info: arg});
  549. }
  550. render({model: {interned_strings}}, {blame_info}) {
  551. let blame_content = "";
  552. if (blame_info) {
  553. const {ist_file, line, ist_s_text, s_start, s_end} = blame_info;
  554. let s_text = interned_strings[ist_s_text];
  555. if (s_start != 0 || s_end != s_text.length) {
  556. let prefix = s_text.slice(0, s_start);
  557. let main = s_text.slice(s_start, s_end);
  558. let suffix = s_text.slice(s_end);
  559. s_text = html`${prefix}<strong>${main}</strong>${suffix}`;
  560. }
  561. blame_content = html`
  562. <h3>${interned_strings[ist_file]}:${line}</h3>
  563. <pre>${s_start}:${s_end}</pre>
  564. <pre>${s_text}</pre><br/>
  565. `;
  566. }
  567. return html`
  568. <button onClick=${() => blame.readyBlame()}>Blame Code</button>
  569. <br/>
  570. ${blame_content}
  571. `;
  572. }
  573. }
  574. class App extends Component {
  575. constructor() {
  576. super();
  577. this.state = {
  578. err: false,
  579. model: null,
  580. };
  581. }
  582. componentDidMount() {
  583. const app = this;
  584. if (BURNED_IN_MODEL_INFO !== null) {
  585. app.setState({model: BURNED_IN_MODEL_INFO});
  586. } else {
  587. fetch("./model_info.json").then(function(response) {
  588. if (!response.ok) {
  589. throw new Error("Response not ok.");
  590. }
  591. return response.json();
  592. }).then(function(body) {
  593. app.setState({model: body});
  594. }).catch(function(error) {
  595. console.log("Top-level error: ", error);
  596. });
  597. }
  598. }
  599. componentDidCatch(error) {
  600. void(error);
  601. this.setState({...this.state, err: true});
  602. }
  603. render(_, {err}) {
  604. if (this.state.model === null) {
  605. return html`<h1>Loading...</h1>`;
  606. }
  607. const model = this.state.model.model;
  608. let error_msg = "";
  609. if (err) {
  610. error_msg = html`<h2 style="background:red">An error occurred. Check console</h2>`;
  611. }
  612. return html`
  613. ${error_msg}
  614. <div id=main_content style="position:absolute;width:99%;height:79%;overflow:scroll">
  615. <h1>TorchScript Model (version ${model.version}): ${model.title}</h1>
  616. <button onClick=${() => console.log(model)}>Log Raw Model Info</button>
  617. <${ModelSizeSection} model=${model}/>
  618. <${StructuredDataSection} name="Model Data" data=${model.model_data} shown=true/>
  619. <${StructuredDataSection} name="Constants" data=${model.constants} shown=false/>
  620. <${ZipContentsSection} model=${model}/>
  621. <${CodeSection} model=${model}/>
  622. <${ExtraJsonSection} files=${model.extra_files_jsons}/>
  623. <${ExtraPicklesSection} files=${model.extra_pickles}/>
  624. <${TensorMemorySection} model=${model}/>
  625. </div>
  626. <div id=aux_content style="position:absolute;width:99%;top:80%;height:20%;overflow:scroll">
  627. <${AuxContentPane}
  628. err=${this.state.error}
  629. model=${model}
  630. ref=${(p) => blame.setAuxContentPane(p)}/>
  631. </div>
  632. `;
  633. }
  634. }
  635. render(h(App), document.body);