File size: 16,876 Bytes
d3e420f
210e29b
 
09eb6df
 
 
 
 
 
f4104eb
4f2e40d
 
f4104eb
d3e420f
210e29b
8b2d95e
 
 
07c358f
09eb6df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
07c358f
09eb6df
 
 
 
 
 
 
07c358f
09eb6df
 
 
 
 
07c358f
 
59069d8
 
07c358f
 
59069d8
 
 
07c358f
59069d8
 
09eb6df
d3e420f
 
 
 
 
09eb6df
d3e420f
 
 
4f66a58
 
 
 
 
210e29b
4f66a58
 
f4104eb
210e29b
4f2e40d
 
4f66a58
5ae7606
210e29b
 
 
 
 
 
5ae7606
 
 
 
 
210e29b
 
 
 
 
09eb6df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4f66a58
09eb6df
 
4f66a58
 
09eb6df
 
 
 
 
 
 
 
 
 
 
4f66a58
09eb6df
 
 
 
 
 
 
 
 
 
 
f4104eb
 
 
 
 
4f2e40d
 
f4104eb
09eb6df
 
210e29b
09eb6df
4f2e40d
09eb6df
4f66a58
 
 
 
 
 
 
 
 
 
 
 
 
09eb6df
f4104eb
 
 
 
 
4f2e40d
f4104eb
 
 
4f66a58
 
4f2e40d
09eb6df
 
4f2e40d
09eb6df
59069d8
07c358f
 
4f2e40d
 
59069d8
4f2e40d
 
09eb6df
 
 
 
 
 
d3e420f
 
4f2e40d
 
 
d3e420f
4f2e40d
 
 
 
210e29b
 
 
4f2e40d
 
210e29b
 
 
4f66a58
4f2e40d
 
d3e420f
210e29b
8b2d95e
 
 
59069d8
8b2d95e
 
59069d8
d3e420f
 
4f66a58
d3e420f
210e29b
d3e420f
4f2e40d
d3e420f
 
 
 
 
210e29b
4f66a58
4f2e40d
4f66a58
b1f5be9
 
210e29b
 
 
 
 
 
 
 
 
 
 
09eb6df
210e29b
 
 
 
09eb6df
 
210e29b
09eb6df
 
 
 
8b2d95e
07c358f
09eb6df
 
 
210e29b
d3e420f
8b2d95e
210e29b
 
09eb6df
4f2e40d
 
 
 
 
 
 
09eb6df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
07c358f
 
09eb6df
 
 
 
 
 
 
 
 
 
07c358f
 
09eb6df
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
// Lists the support results of the aws-neuron/torch-neuronx-model-support
// dataset, one tab per library, 100 models per page: for the models the latest
// sweep selected, or for the Hub's models by trending score or downloads.
import { parquetReadObjects } from "https://cdn.jsdelivr.net/npm/hyparquet@1.31.2/src/index.js";

const HUB = "https://huggingface.co";
const DATASET = "aws-neuron/torch-neuronx-model-support";
const NEFF_CACHE = "hf://buckets/aws-neuron/torch-neuronx-neff-cache";
const LIBRARIES = ["transformers", "diffusers", "sentence-transformers"];
const VIEWS = { selected: "Selected", trending: "Trending", downloads: "Downloads" };
// The task Trending and Downloads open on: all tasks would open on models the sweep never tests.
const DEFAULT_TASK = "text-generation";
// The Hub's sort, and model field, of each view but "selected".
const HUB_SORTS = { trending: "trendingScore", downloads: "downloads" };
const PAGE = 100;
// The test instance: 4 Trainium2 devices of 96 GiB of HBM each.
const DEVICES = 4;
const DEVICE_HBM = 96 * 1024 ** 3;
const COLUMNS = ["model_id", "cores", "result", "wall (s)", "Memory (GB)", "notes"];

// An element with attributes and children. Strings become text nodes, never HTML.
function h(tag, attrs = {}, ...children) {
  const el = document.createElement(tag);
  for (const [key, value] of Object.entries(attrs)) {
    if (key.startsWith("on")) el.addEventListener(key.slice(2), value);
    else if (value === true) el.setAttribute(key, "");
    else if (value !== false && value != null) el.setAttribute(key, value);
  }
  el.append(...children.flat(Infinity).filter((c) => c != null && c !== false));
  return el;
}

const encodePath = (path) => path.split("/").map(encodeURIComponent).join("/");

async function fetchOk(url) {
  const response = await fetch(url);
  if (!response.ok) throw new Error(`${url}: HTTP ${response.status}`);
  return response;
}

// Promises by key, dropped on failure so that the next render retries.
const cache = new Map();
function cached(key, load) {
  if (!cache.has(key)) cache.set(key, load().catch((e) => { cache.delete(key); throw e; }));
  return cache.get(key);
}

// The dataset's files at its latest commit: one per (neuronx-cc, library version)
// pair.
async function loadIndex() {
  const info = await (await fetchOk(`${HUB}/api/datasets/${DATASET}`)).json();
  const pairs = [];
  for (const { rfilename: path } of info.siblings) {
    const pair = path.match(/^data\/neuronxcc-([^/]+)\/(.+)-([^-/]+)\.parquet$/);
    if (pair) pairs.push({ sdk: pair[1], library: pair[2], version: pair[3], path });
  }
  return { sha: info.sha, pairs };
}

const index = await loadIndex().catch((e) => e);
const fileUrl = (path) => `${HUB}/datasets/${DATASET}/resolve/${index.sha}/${encodePath(path)}`;

// A pair's rows: its runs, the models its sweep left out, and each model's rank
// in the selection of its last sweep, best-ranked first.
const loadRows = (path) => cached(path, async () => {
  const rows = await parquetReadObjects({ file: await (await fetchOk(fileUrl(path))).arrayBuffer() });
  const ranked = rows.filter((row) => row.selection_rank != null)
    .sort((a, b) => a.selection_rank - b.selection_rank);
  return {
    runs: rows.filter((row) => row.result !== "EXCLUDED"),
    exclusions: new Map(rows.filter((row) => row.result === "EXCLUDED").map((row) => [row.model_id, row])),
    selection: new Map(ranked.map((row) => [row.model_id, row.selection_rank])),
  };
});

// A model's task on the Hub, or "unknown" if the Hub no longer has it.
const loadTask = (id) => cached(`task:${id}`, async () => {
  const query = new URLSearchParams({ "expand[]": "pipeline_tag" });
  const response = await fetch(`${HUB}/api/models/${encodePath(id)}?${query}`);
  if (response.status === 404) return "unknown";
  if (!response.ok) throw new Error(`${id}: HTTP ${response.status}`);
  return (await response.json()).pipeline_tag ?? "unknown";
}).catch(() => "unknown");

// The models of a library, of a task unless it is null, and whose id contains
// `name` unless it is empty, in the order of the Hub's model list, with their
// parameter count when the Hub knows it: at least `count` of them, or all if it
// has fewer. `next` is the Hub's link to the models after them, if any. The list
// grows one Hub page at a time, as deep as it was asked for.
const hubLists = new Map();
function hubList(library, task, name, view, count) {
  const key = `${library}:${task}:${name}:${view}`;
  const sort = HUB_SORTS[view];
  if (!hubLists.has(key)) {
    const query = new URLSearchParams({ library, sort, direction: "-1", limit: PAGE });
    if (task) query.set("pipeline_tag", task);
    if (name) query.set("search", name);
    for (const field of [sort, "safetensors", "library_name"]) query.append("expand[]", field);
    hubLists.set(key, { models: [], next: `${HUB}/api/models?${query}`, growing: Promise.resolve() });
  }
  const list = hubLists.get(key);
  list.growing = list.growing.catch(() => {}).then(async () => {
    while (list.models.length < count && list.next) {
      const response = await fetchOk(list.next);
      list.models.push(...(await response.json())
        // The Hub's library filter matches a tag that models of other libraries
        // carry too, such as diffusers' FLUX or sentence-transformers' MiniLM.
        .filter((model) => model.library_name === library)
        .map((model) => ({ id: model.id, score: model[sort] ?? 0, params: model.safetensors?.total })));
      list.next = response.headers.get("Link")?.match(/<([^>]+)>;\s*rel="next"/)?.[1];
    }
  });
  return list.growing.then(() => list);
}

// Newest first. A version that is not a release, such as a git commit pin,
// comes before the releases.
function newestFirst(a, b) {
  const release = (v) => v.match(/^\d+(?:\.\d+)+/)?.[0].split(".").map(Number);
  const [ra, rb] = [release(a), release(b)];
  if (!ra || !rb) return Boolean(ra) - Boolean(rb) || a.localeCompare(b);
  for (let i = 0; i < Math.max(ra.length, rb.length); i++) {
    const d = (rb[i] ?? 0) - (ra[i] ?? 0);
    if (d) return d;
  }
  // A pre-release, such as 5.21.0rc1, is older than its release.
  const pre = (v) => /^\d+(?:\.\d+)+(?:a|b|rc|\.?dev)/.test(v);
  return pre(a) - pre(b);
}

const badge = (result) => `${result === "PASS" ? "✅" : "❌"} ${result}`;
const memory = (run) => run.hbm_peak_gb == null ? ""
  : `${run.hbm_peak_gb.toFixed(2)} / ${Number(run.hbm_capacity_gb.toFixed(1))}`;
const day = (date) => new Date(date).toISOString().slice(0, 10);

const tabs = document.getElementById("tabs");
const panel = document.getElementById("panel");
const dialog = document.getElementById("run");
const state = new Map(LIBRARIES.map((library) =>
  [library, { view: "selected", sdk: null, version: null, tasks: {}, names: {}, page: 1 }]));
let active = LIBRARIES[0];
let rendering = 0;
// The pending list render of the name being typed.
let typing;

function renderTabs() {
  tabs.replaceChildren(...LIBRARIES.map((library) => h("button", {
    role: "tab",
    "aria-selected": String(library === active),
    onclick: () => { active = library; renderTabs(); renderPanel(); },
  }, library)));
}

async function renderPanel() {
  const render = ++rendering;
  clearTimeout(typing);
  if (index instanceof Error) {
    panel.replaceChildren(h("p", { class: "status" }, `Could not read the dataset: ${index.message}`));
    return;
  }
  const pairs = index.pairs.filter((p) => p.library === active);
  if (!pairs.length) {
    panel.replaceChildren(h("p", { class: "status" }, "No records yet."));
    return;
  }
  const s = state.get(active);
  const sdks = [...new Set(pairs.map((p) => p.sdk))].sort(newestFirst);
  const versionsOf = (sdk) => pairs.filter((p) => p.sdk === sdk).map((p) => p.version).sort(newestFirst);
  // The latest selection was swept with the newest library version of the newest compiler.
  const latest = { sdk: sdks[0], version: versionsOf(sdks[0])[0] };
  if (!sdks.includes(s.sdk)) s.sdk = latest.sdk;
  if (!versionsOf(s.sdk).includes(s.version)) s.version = versionsOf(s.sdk)[0];
  const selected = s.view === "selected";
  const shown = selected ? latest : { sdk: s.sdk, version: s.version };
  const pathOf = ({ sdk, version }) => pairs.find((p) => p.sdk === sdk && p.version === version).path;

  const select = (label, key, options, onchange) => h("label", {}, label, h("select", {
    onchange: (e) => { s[key] = e.target.value; s.page = 1; onchange?.(); renderPanel(); },
  }, options.map((o) => h("option", { value: o, selected: o === s[key] }, o))));
  const tasks = h("div", { class: "tasks", role: "group", "aria-label": "Task" }, h("span", {}, "Task"));
  const results = h("div", { class: "results" }, h("p", { class: "status" }, "Loading…"));
  // Typing renders the list again, not the panel, which would take the focus away.
  let renderResults;
  const nameField = h("label", {}, "Name", h("input", {
    type: "search",
    placeholder: "Filter by name",
    value: s.names[s.view] ?? "",
    oninput: (e) => {
      s.names[s.view] = e.target.value.trim();
      s.page = 1;
      clearTimeout(typing);
      typing = setTimeout(() => renderResults?.(), 300);
    },
  }));
  panel.replaceChildren(
    h("div", { class: "views", role: "tablist" }, Object.entries(VIEWS).map(([view, label]) => h("button", {
      role: "tab",
      "aria-selected": String(view === s.view),
      onclick: () => { s.view = view; s.page = 1; renderPanel(); },
    }, label))),
    selected
      ? h("p", { class: "pair" }, `neuronx-cc ${latest.sdk} · ${active} ${latest.version}`)
      : h("div", { class: "controls" },
        select("SDK (neuronx-cc)", "sdk", sdks, () => { s.version = null; }),
        select(active, "version", versionsOf(s.sdk)),
        nameField),
    ...(selected ? [] : [tasks]),
    results);

  let runs, rank, exclusions, taskList;
  try {
    let latestRows;
    [{ runs, exclusions }, latestRows] = await Promise.all([loadRows(pathOf(shown)), loadRows(pathOf(latest))]);
    rank = latestRows.selection;
    if (!selected) {
      // The tasks the sweep covers: those of the latest pair's and the selection's models.
      const covered = new Set([...latestRows.runs.map((run) => run.model_id), ...rank.keys()]);
      taskList = [...new Set(await Promise.all([...covered].map(loadTask)))].sort();
    }
  } catch (e) {
    if (render === rendering) results.replaceChildren(h("p", { class: "status" }, `Could not load the results: ${e.message}`));
    return;
  }
  if (render !== rendering) return;

  const runsOf = new Map();
  for (const run of runs) runsOf.set(run.model_id, [...(runsOf.get(run.model_id) ?? []), run]);
  // The task Trending or Downloads shows, or null for all of them.
  if (!selected && !(s.view in s.tasks)) s.tasks[s.view] = taskList.includes(DEFAULT_TASK) ? DEFAULT_TASK : null;
  const task = s.tasks[s.view] ?? null;

  // The current page of the selection, or of the Hub's list of the chosen task or
  // of all tasks: its models, each with its runs on the shown pair, the position
  // of the first, and whether more follow.
  async function listed() {
    const start = (s.page - 1) * PAGE;
    const end = start + PAGE;
    let models, more;
    if (selected) {
      const ids = [...rank.keys()];
      models = ids.slice(start, end).map((id) => ({ id }));
      more = ids.length > end;
    } else {
      const list = await hubList(active, task, s.names[s.view] ?? "", s.view, end);
      models = list.models.slice(start, end);
      more = list.models.length > end || Boolean(list.next);
    }
    return { start, more, models: models.map((model) => ({ ...model, runs: runsOf.get(model.id) ?? [] })) };
  }

  // Why a model was not tested: weights larger than the test instance's HBM, or
  // the reason the sweep's selection left it out.
  function untestedReason({ id, params }) {
    if (params * 2 > DEVICES * DEVICE_HBM) return `Requires more than ${DEVICES} Trainium2 devices`;
    return exclusions.get(id)?.notes ?? "";
  }

  renderResults = async () => {
    const render = ++rendering;
    let page;
    try {
      page = await listed();
    } catch (e) {
      if (render === rendering) results.replaceChildren(h("p", { class: "status" }, `Could not load the Hub's list: ${e.message}`));
      return;
    }
    if (render !== rendering) return;
    if (!page.models.length) {
      const name = s.names[s.view];
      results.replaceChildren(h("p", { class: "status" }, selected ? "No selection yet."
        : `No model of ${task ?? "any task"}${name ? ` matching "${name}"` : ""} is listed.`));
      return;
    }
    // A model passes if one of its runs passed.
    const passed = page.models.filter((m) => m.runs.some((run) => run.result === "PASS")).length;
    const untested = page.models.filter((m) => !m.runs.length).length;
    const go = async (pages) => {
      s.page += pages;
      await renderResults();
      if (results.getBoundingClientRect().top < 0) results.scrollIntoView();
    };
    const pager = () => h("div", { class: "pager" },
      h("button", { disabled: s.page === 1, onclick: () => go(-1) }, "‹ Previous"),
      h("button", { disabled: !page.more, onclick: () => go(1) }, "Next ›"));
    results.replaceChildren(
      h("div", { class: "summary" },
        h("p", {}, h("strong", {}, `${page.start + 1}-${page.start + page.models.length}`),
          ` · ✅ Pass ${passed} · ❌ Fail ${page.models.length - passed - untested} · Untested ${untested}`),
        pager()),
      h("div", { class: "scroll" }, h("table", {},
        h("thead", {}, h("tr", {}, COLUMNS.map((c) => h("th", {}, c)))),
        h("tbody", {}, page.models.map((model) => model.runs.length ? model.runs.map((run) => h("tr", {
          tabindex: 0,
          onclick: () => showRun(run),
          onkeydown: (e) => { if (e.key === "Enter") showRun(run); },
        },
        h("td", {}, h("code", {}, model.id)),
        h("td", { class: "num" }, run.neuron_cores),
        h("td", { class: "result" }, badge(run.result)),
        h("td", { class: "num" }, run.wall_s ?? ""),
        h("td", { class: "num" }, memory(run)),
        h("td", { class: "notes" }, run.notes ?? "")))
          : h("tr", { class: "untested" },
            h("td", {}, h("code", {}, model.id)), h("td"), h("td", { class: "result" }, "not tested"),
            h("td"), h("td"), h("td", { class: "notes" }, untestedReason(model))))))),
      pager());
  };
  // One task at a time, as on the Hub: clicking the chosen task again shows all of them.
  if (!selected) {
    tasks.append(...[null, ...taskList].map((t) => h("button", {
      "aria-pressed": String(task === t),
      onclick: () => { s.tasks[s.view] = task === t ? null : t; s.page = 1; renderPanel(); },
    }, t ?? "All")));
  }
  renderResults();
}

let scriptUrl;
function showRun(run) {
  URL.revokeObjectURL(scriptUrl);
  scriptUrl = URL.createObjectURL(new Blob([run.run_py], { type: "text/x-python" }));
  const pip = run.run_py.match(/^#   (pip install .*)$/m)?.[1];
  const stack = [
    ["neuronx-cc", run.neuronx_cc], ["torch-neuronx", run.torch_neuronx], ["torch", run.torch],
    ["nki", run.nki], ["Python", run.python], ["Neuron driver", run.neuron_driver],
  ].filter(([, version]) => version);
  dialog.replaceChildren(h("div", { class: "dialog" },
    h("form", { method: "dialog" }, h("button", { class: "close", "aria-label": "Close" }, "×")),
    h("h2", {}, h("a", { href: `${HUB}/${run.model_id}`, target: "_blank", rel: "noopener" }, run.model_id)),
    h("p", {}, `${badge(run.result)} · ${run.neuron_cores} NeuronCore${run.neuron_cores > 1 ? "s" : ""}`
      + ` · ${run.runner} · ${run.library} ${run.library_version}`
      + ` · tested ${day(run.tested_at)}`),
    h("h3", {}, "Run it"),
    h("p", {}, "On a Trainium2 (trn2) instance with this Neuron stack, for example in the AWS Neuron"
      + " Deep Learning Container:"),
    h("dl", {}, stack.map(([name, version]) => [h("dt", {}, name), h("dd", {}, h("code", {}, version))])),
    h("ol", {},
      pip && h("li", {}, "Install the packages the script adds:", h("pre", {}, pip)),
      h("li", {}, "Optionally, skip compilation with the graphs this compiler already built:",
        h("pre", {}, `hf buckets sync ${NEFF_CACHE}/neuronxcc-${run.neuronx_cc} /tmp/neff_cache`)),
      h("li", {}, "Download ", h("a", { href: scriptUrl, download: "run.py" }, "run.py"), " and run it",
        run.neuron_cores > 1 ? ". It launches its own torch.distributed ranks:" : ":",
        h("pre", {}, "python3 run.py"))),
    run.error && [h("h3", {}, "Error"), h("pre", { class: "error" }, run.error)],
    h("details", {}, h("summary", {}, "run.py"), h("pre", {}, run.run_py))));
  dialog.showModal();
}
dialog.addEventListener("click", (e) => { if (e.target === dialog) dialog.close(); });

renderTabs();
renderPanel();