Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[submodule "targets/tiny-gpu"]
path = targets/tiny-gpu
url = https://github.com/deaneeth/tiny-gpu
url = https://github.com/tinycomputers/tiny-gpu
ignore = untracked
[submodule "targets/tiny-tpu"]
path = targets/tiny-tpu
url = https://github.com/deaneeth/tiny-tpu
url = https://github.com/tinycomputers/tiny-tpu
2 changes: 1 addition & 1 deletion targets/tiny-gpu
Submodule tiny-gpu updated 1 files
+2 −0 README.md
2 changes: 1 addition & 1 deletion targets/tiny-tpu
Submodule tiny-tpu updated 1 files
+4 −0 README.md
19 changes: 17 additions & 2 deletions web/e2e/smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,32 @@ test.describe("Compiler Explorer smoke", () => {
await expect(page.getByText("bit-exact").first()).toBeVisible();
});

test("the tiny-tpu tab shows the array, cycle by cycle, from the RTL trace", async ({ page }) => {
test("the tiny-tpu array plays by itself, cycle by cycle, from the RTL trace", async ({ page }) => {
await page.goto("/app");
await page.getByRole("button", { name: "Compile & run" }).click();
await expect(page.getByText("bit-exact").first()).toBeVisible({ timeout: 150_000 });
await page.getByRole("tab", { name: "tiny-tpu" }).click();
await expect(page.getByRole("img", { name: /^PE\[0\]\[0\] / }).first()).toBeVisible();
await expect(page.getByText(/cycle 0 \/ \d+ · IDLE|cycle 0 \/ \d+ · LOAD_WEIGHTS/)).toBeVisible();
// Playing on arrival: the cycle counter moves without any click.
await expect(page.getByText(/cycle [1-9]\d* \/ \d+/)).toBeVisible({ timeout: 10_000 });
await page.getByRole("button", { name: "Pause" }).click();
await page.getByRole("button", { name: "pass 2" }).click();
await expect(page.getByText(/cycle 0 \/ \d+/)).toBeVisible();
});

test("the pass replay steps through the compiler stages", async ({ page }) => {
await page.goto("/app");
await page.getByRole("button", { name: "Compile & run" }).click();
await expect(page.getByText("bit-exact").first()).toBeVisible({ timeout: 150_000 });
await page.getByRole("tab", { name: "Passes" }).click();
await page.getByRole("button", { name: "Replay the passes" }).click();
// Each stage lasts under a second, so wait for the replay to finish rather than catch one stage.
await expect(page.getByRole("button", { name: "Replay the passes" })).toBeVisible({ timeout: 60_000 });
const stage = await page.getByText(/stage \d+ of \d+/).innerText();
const [, last] = stage.match(/of (\d+)/) ?? [];
expect(stage).toBe(`stage ${last} of ${last}`);
});

test("choosing an example replaces the editor's source", async ({ page }) => {
await page.goto("/app");
await page.getByLabel("example").selectOption("mlp_gpu");
Expand Down
30 changes: 27 additions & 3 deletions web/src/components/Explorer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { analyze } from "../lib/compiler";
import { EXAMPLES } from "../lib/examples.generated";
import { runTpu, type TpuRun } from "../lib/tpu";
Expand Down Expand Up @@ -67,6 +67,22 @@ export default function Explorer() {
}

const passes = analysis?.gpu && target === "gpu" ? analysis.gpu.passes : analysis?.tpu?.passes ?? [];
const [replaying, setReplaying] = useState(false);
const passCount = passes.length;
// Replay: step through the stages of the chosen chip's pipeline, one every 900 ms.
useEffect(() => {
if (!replaying) return;
const id = window.setInterval(() => {
setPassIndex((i) => {
if (i >= passCount - 1) {
setReplaying(false);
return i;
}
return i + 1;
});
}, 900);
return () => window.clearInterval(id);
}, [replaying, passCount]);
const exactTpu = useMemo(
() => (analysis?.tpu?.ok && tpuRun && analysis.oracle ? tpuExact(tpuRun, analysis.tpu, analysis.oracle) : null),
[analysis, tpuRun],
Expand Down Expand Up @@ -141,12 +157,20 @@ export default function Explorer() {
<>
<div className="tabs">
{passes.map((p, i) => (
<button key={i} aria-selected={i === passIndex} onClick={() => setPassIndex(i)}>
<button key={i} aria-selected={i === passIndex} onClick={() => { setReplaying(false); setPassIndex(i); }}>
{p.name}
</button>
))}
</div>
<Code>{passes[passIndex]?.ir ?? ""}</Code>
<div style={{ display: "flex", gap: 8, alignItems: "center", marginBottom: 8 }}>
<button onClick={() => { if (replaying) setReplaying(false); else { setPassIndex(0); setReplaying(true); } }}>
{replaying ? "Pause" : "Replay the passes"}
</button>
<span className="muted">stage {passIndex + 1} of {passCount}</span>
</div>
<div key={passIndex} className="fade-in">
<Code>{passes[passIndex]?.ir ?? ""}</Code>
</div>
<p className="muted">Each stage is the IR after that pass. Lowering to kernels and tiles is shown as its own stage.</p>
</>
)}
Expand Down
33 changes: 22 additions & 11 deletions web/src/components/TpuSchedule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,28 @@ import type { TpuRun } from "../lib/tpu";
export default function TpuSchedule({ run }: { run: TpuRun }) {
const [pass, setPass] = useState(0);
const [cycle, setCycle] = useState(0);
const [playing, setPlaying] = useState(false);
// Play on arrival, unless the reader asked for reduced motion.
const [playing, setPlaying] = useState(() => !window.matchMedia("(prefers-reduced-motion: reduce)").matches);

const current = run.passes[pass];
const last = current ? current.states.length - 1 : 0;
const lastPass = run.passes.length - 1;

// Advance one cycle per tick while playing; stop at the end of the pass.
// One cycle per tick. At the end of a pass, the next pass starts; the replay stops after the last.
useEffect(() => {
if (!playing) return;
const id = window.setInterval(() => {
setCycle((c) => {
if (c >= last) {
setPlaying(false);
return c;
}
return c + 1;
});
if (cycle < last) {
setCycle(cycle + 1);
} else if (pass < lastPass) {
setPass(pass + 1);
setCycle(0);
} else {
setPlaying(false);
}
}, 250);
return () => window.clearInterval(id);
}, [playing, last]);
}, [playing, cycle, last, pass, lastPass]);

if (!current) return <p className="muted">This program has no chip passes.</p>;
const state = current.states[cycle];
Expand All @@ -35,6 +38,12 @@ export default function TpuSchedule({ run }: { run: TpuRun }) {
setPlaying(false);
}

function replay() {
setPass(0);
setCycle(0);
setPlaying(true);
}

return (
<div>
<div className="tabs">
Expand All @@ -45,7 +54,9 @@ export default function TpuSchedule({ run }: { run: TpuRun }) {
))}
</div>
<div style={{ display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }}>
<button onClick={() => setPlaying((p) => !p)}>{playing ? "Pause" : "Play"}</button>
<button onClick={() => (playing ? setPlaying(false) : replay())}>
{playing ? "Pause" : cycle === last && pass === lastPass ? "Replay" : "Play"}
</button>
<input
type="range"
min={0}
Expand Down
10 changes: 10 additions & 0 deletions web/src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,13 @@ th { color: var(--muted); font-weight: 600; }
.panel { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 12px; }
.tabs { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 10px; }
.tabs button[aria-selected="true"] { background: var(--accent); color: #fff; border-color: var(--accent); }

/* Short fade when a stage or a pass changes; off for readers who ask for reduced motion. */
.fade-in { animation: fade-in 320ms ease-out; }
@keyframes fade-in {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
.fade-in { animation: none; }
}
Loading