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
41 changes: 41 additions & 0 deletions website/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
<li><a href="#performance">Backends</a></li>
<li><a href="#partners">Partners</a></li>
<li><a href="https://github.com/pytorch/executorch" target="_blank">GitHub</a></li>
<li><a href="playground/" class="nav-playground">Playground</a></li>
</ul>
<form class="nav-search" action="https://docs.pytorch.org/executorch/1.0/search.html" method="get">
<input type="text" placeholder="Search docs…" name="q" style="padding: 0.5rem 1rem; border-radius: 6px; border: 1px solid #e2e8f0;">
Expand Down Expand Up @@ -86,6 +87,46 @@
</div>
</section>

<!-- Playground call-to-action -->
<section class="playground-cta" aria-labelledby="playground-cta-title">
<div class="container playground-cta__inner">
<div class="playground-cta__copy">
<p class="playground-cta__eyebrow">Experimental · ExecuTorch Playground</p>
<h2 id="playground-cta-title" class="playground-cta__title">Run PyTorch models locally in your browser</h2>
<p class="playground-cta__text">Chat with a language model, transcribe your voice, segment objects, and see depth through your camera. Powered by ExecuTorch on WebGPU: models run on your GPU, with nothing to install and nothing sent to a server.</p>
<ul class="playground-cta__chips">
<li>On-device chat</li><li>Speech to text</li><li>Live camera vision</li><li>Segment anything</li>
</ul>
<a href="playground/" class="btn btn-primary playground-cta__btn">Open the Playground &rarr;</a>
</div>
<svg class="playground-cta__art" viewBox="0 0 320 220" role="img" aria-label="" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="pgcta-bg" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#1f1360"/><stop offset=".55" stop-color="#7a2bb8"/><stop offset="1" stop-color="#df3411"/></linearGradient>
<radialGradient id="pgcta-glow" cx=".8" cy=".2" r=".6"><stop offset="0" stop-color="#fff" stop-opacity=".45"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></radialGradient>
</defs>
<rect width="320" height="220" rx="22" fill="url(#pgcta-bg)"/>
<rect width="320" height="220" rx="22" fill="url(#pgcta-glow)"/>
<rect x="28" y="30" width="180" height="120" rx="12" fill="#fff" fill-opacity=".95"/>
<rect x="28" y="30" width="180" height="22" rx="12" fill="#ececf3"/>
<circle cx="44" cy="41" r="4" fill="#ff6159"/><circle cx="58" cy="41" r="4" fill="#ffbd2e"/><circle cx="72" cy="41" r="4" fill="#28c941"/>
<rect x="44" y="66" width="104" height="14" rx="7" fill="#7a2bb8" fill-opacity=".25"/>
<rect x="96" y="90" width="98" height="14" rx="7" fill="#df3411"/>
<rect x="44" y="114" width="80" height="14" rx="7" fill="#7a2bb8" fill-opacity=".25"/>
<rect x="128" y="114" width="6" height="14" rx="2" fill="#7a2bb8"><animate attributeName="opacity" values="1;0;1" dur="1.1s" repeatCount="indefinite"/></rect>
<g transform="translate(222 92)">
<rect width="72" height="72" rx="14" fill="#fff" fill-opacity=".14" stroke="#fff" stroke-opacity=".6"/>
<rect x="18" y="18" width="36" height="36" rx="6" fill="#fff" fill-opacity=".9"/>
<path d="M26 -8v10M36 -8v10M46 -8v10M26 70v10M36 70v10M46 70v10M-8 26h10M-8 36h10M-8 46h10M70 26h10M70 36h10M70 46h10" stroke="#fff" stroke-opacity=".7" stroke-width="3" stroke-linecap="round"/>
<text x="36" y="41" text-anchor="middle" font-family="Open Sans, Helvetica, sans-serif" font-size="11" font-weight="700" fill="#7a2bb8">GPU</text>
</g>
<path d="M214 120 C 222 120, 224 128, 222 128" stroke="#fff" stroke-opacity=".7" stroke-width="2" stroke-dasharray="3 4" fill="none"/>
<path d="M270 40l3 8 8 3-8 3-3 8-3-8-8-3 8-3z" fill="#fff" fill-opacity=".9"/>
<path d="M292 70l2 5 5 2-5 2-2 5-2-5-5-2 5-2z" fill="#fff" fill-opacity=".7"/>
<text x="28" y="190" font-family="ui-monospace, Menlo, monospace" font-size="12" fill="#fff" fill-opacity=".8">navigator.gpu · ExecuTorch</text>
</svg>
</div>
</section>

<!-- Why On-Device -->

<!-- Why On-Device Cards -->
Expand Down
40 changes: 40 additions & 0 deletions website/playground/hub.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/* Playground hub: one card per playground. Tokens and the site bar come from
webgpu/styles/app.css. */
.hub { max-width: 1120px; margin: 0 auto; padding: 56px 32px 80px; }
.hub__head { display: flex; flex-direction: column; gap: 10px; margin-bottom: 32px; }
.hub__title { font-size: clamp(26px, 2.6vw, 34px); line-height: 1.2; font-weight: 700; letter-spacing: -0.01em; }
.hub__lede { font-size: 16px; color: var(--text-2); max-width: 68ch; }

.track {
display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr); overflow: hidden;
border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); color: var(--text);
box-shadow: var(--shadow-sm); transition: transform 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease;
}
.track:hover { text-decoration: none; transform: translateY(-4px); box-shadow: var(--shadow-md); border-color: var(--accent); }
.track__art { display: grid; place-items: center; min-height: 300px; background: #e6f4ea; overflow: hidden; }
:root[data-theme="dark"] .track__art { background: #1e3329; }
.track__art .art { width: 100%; height: auto; }
.track__body { display: flex; flex-direction: column; gap: 12px; padding: 32px 36px; justify-content: center; }
.track__tags { display: flex; gap: 8px; }
.track__tag { font-size: 11px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; padding: 3px 9px; border-radius: 999px; background: var(--accent-soft); color: var(--accent-ink); }
.track__tag--exp { background: var(--ph-soft); color: var(--ph-ink); border: 1px solid color-mix(in srgb, var(--ph-line) 45%, transparent); }
.track__title { font-size: 24px; font-weight: 700; line-height: 1.25; }
.track__text { font-size: 15.5px; color: var(--text-2); }
.track__demos { list-style: none; margin: 4px 0 0; padding: 0; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px 16px; }
.track__demo { display: flex; align-items: center; gap: 9px; font-size: 14px; font-weight: 600; color: var(--text); }
.track__demo svg { flex-shrink: 0; }
.track__privacy { font-size: 13px; color: var(--text-3); }
.track__cta { align-self: flex-start; margin-top: 4px; }

.soon { margin-top: 28px; padding: 26px 30px; border: 1px dashed var(--border-strong); border-radius: var(--radius); background: transparent; }
.soon__title { font-size: 18px; font-weight: 700; margin-bottom: 6px; }
.soon__text { font-size: 15px; color: var(--text-2); }
.soon__text a { font-weight: 600; }

@media (max-width: 900px) {
.hub { padding: 32px 18px 56px; }
.track { grid-template-columns: minmax(0, 1fr); }
.track__art { min-height: 0; aspect-ratio: 16 / 9; }
.track__body { padding: 22px; }
.track__title { font-size: 21px; }
}
18 changes: 18 additions & 0 deletions website/playground/hub.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { DEMOS } from './webgpu/src/config/catalog.js';
import { h } from './webgpu/src/lib/dom.js';
import { appIcon } from './webgpu/src/ui/app-icons.js';
import { illustration } from './webgpu/src/ui/illustrations.js';

// The card is taller than the 16:9 art, so show the whole scene instead of cropping it.
for (const el of document.querySelectorAll('[data-art]')) {
const art = illustration(el.dataset.art);
art.setAttribute('preserveAspectRatio', 'xMidYMid meet');
el.append(art);
}

for (const list of document.querySelectorAll('[data-demos]')) {
for (const id of list.dataset.demos.split(' ')) {
const demo = DEMOS.find((d) => d.id === id);
if (demo) list.append(h('li', { class: 'track__demo' }, appIcon(id, 22), demo.title));
}
}
74 changes: 74 additions & 0 deletions website/playground/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Playground · ExecuTorch</title>
<meta name="description" content="Try ExecuTorch hands-on, right in your browser.">
<meta name="color-scheme" content="light dark">
<link rel="icon" type="image/svg+xml" href="./webgpu/assets/executorch-chip-logo.svg">
<script>
// Same theme resolution as the WebGPU playground, so moving between them never flashes.
(function () {
var saved = null;
try { saved = localStorage.getItem('etwg-theme'); } catch (e) {}
var dark = saved ? saved === 'dark' : matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.dataset.theme = dark ? 'dark' : 'light';
})();
</script>
<link rel="stylesheet" href="./webgpu/styles/app.css">
<link rel="stylesheet" href="./hub.css">
</head>
<body>
<header class="sitebar">
<div class="sitebar__inner">
<a class="sitebar__brand" href="../index.html">
<img class="sitebar__logo" src="./webgpu/assets/executorch-chip-logo.svg" alt="" width="40" height="40">
<span class="sitebar__name">ExecuTorch</span>
</a>
<nav class="sitebar__links" aria-label="executorch.ai">
<a class="sitebar__link sitebar__link--wide" href="../index.html#why-ondevice">Why On-Device</a>
<a class="sitebar__link sitebar__link--wide" href="../index.html#challenges">Challenges</a>
<a class="sitebar__link sitebar__link--wide" href="../index.html#features">Solution</a>
<a class="sitebar__link sitebar__link--wide" href="../index.html#performance">Backends</a>
<a class="sitebar__link sitebar__link--wide" href="../index.html#partners">Partners</a>
<a class="sitebar__link" href="https://github.com/pytorch/executorch">GitHub</a>
<a class="sitebar__pill" href="./" aria-current="page">Playground</a>
</nav>
<form class="sitebar__search" action="https://docs.pytorch.org/executorch/1.0/search.html" method="get">
<input class="sitebar__search-input" type="text" name="q" placeholder="Search docs…" aria-label="Search docs">
</form>
</div>
</header>

<main class="hub">
<header class="hub__head">
<p class="eyebrow">ExecuTorch Playground</p>
<h1 class="hub__title">Try ExecuTorch hands-on</h1>
<p class="hub__lede">Interactive experiments that run ExecuTorch right where you are: no install and no server. Pick a playground to start.</p>
</header>

<a class="track" href="./webgpu/">
<div class="track__art" data-art="effects"></div>
<div class="track__body">
<div class="track__tags">
<span class="track__tag">WebGPU</span>
<span class="track__tag track__tag--exp">Experimental</span>
</div>
<h2 class="track__title">Run PyTorch models locally in your browser</h2>
<p class="track__text">Chat with a language model, transcribe your voice, segment objects, and see depth through your camera. Real ExecuTorch models running on your GPU with the WebGPU backend.</p>
<ul class="track__demos" data-demos="effects chat speech-to-text segment vlm depth"></ul>
<p class="track__privacy">Private by design: models run on your GPU, and nothing you type, say, or show leaves the tab.</p>
<span class="btn btn--primary track__cta">Open the WebGPU playground <span aria-hidden="true">&rarr;</span></span>
</div>
</a>

<section class="soon" aria-labelledby="soon-title">
<h2 class="soon__title" id="soon-title">More playgrounds are on the way</h2>
<p class="soon__text">What would you like to try next? Tell us on <a href="https://github.com/pytorch/executorch/issues" target="_blank" rel="noopener">GitHub</a> or in the <a href="https://discord.gg/GbraDsuqCW" target="_blank" rel="noopener">PyTorch Edge Discord</a>.</p>
</section>
</main>

<script type="module" src="./hub.js"></script>
</body>
</html>
85 changes: 85 additions & 0 deletions website/playground/webgpu/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# ExecuTorch WebGPU Playground

The playground for executorch.ai, served at `/playground/webgpu/`. Visitors run real
ExecuTorch models in their own browser on WebGPU: nothing to install, and
prompts, photos, and audio never leave the tab.

Zero dependencies and no build step: plain ES modules, one design system, hash
routing. The folder drops into the site as-is; the homepage links to it from the
nav ("Playground") and a call-to-action band under the hero.

## Run it locally

```bash
node scripts/serve.mjs # http://localhost:5173/playground/webgpu/ (+ LAN URLs for the no-WebGPU fallback)
node --test "tests/*.test.mjs" # licensing gate, internal-reference scan, runtime, loader, wiring
node scripts/check-wiring.mjs # static wiring check on its own
```

### Local-assets dev mode (real models before hosting is cleared)

```bash
node scripts/serve.mjs --dev-assets <dir> --dev-config <file-or-dir>
```

The server binds loopback only and adds two routes the deployed site never has:
`/__dev__/config.json` (model id → an `assets` block shaped like the one in
`models.js`, with `baseUrl` relative to `<dir>`) and `/__dev_assets__/*` (files
from `<dir>`). A dev entry takes precedence over the hosted one, so a new export
can be tried before it is published. Pages only look for the config when
they are themselves on a loopback host. Shipped model entries keep
`baseUrl: null` and `license.status: 'todo'`, so a deployed copy shows
"not available in this build" until a model is cleared.

To try it on a laptop GPU from a remote dev machine, forward the port
(`ssh -L 5173:localhost:5173 <host>`) and open `http://localhost:5173/playground/webgpu/`;
`localhost` is a secure context, so WebGPU is available.

## What runs

Every tile's badge comes from the `compute` field in `src/config/catalog.js`.

| Demo | Badge | What runs |
|---|---|---|
| On-device Chat | ExecuTorch | Qwen 3 0.6B, Qwen 2.5 0.5B, Llama 3.2 1B (4-bit `.pte`) with batched prefill, KV cache reused across turns, and JS sampling. Voice input through Whisper. |
| Segment Anything | ExecuTorch | SAM 2 Hiera Tiny: encode once, decode a mask per click or box. |
| VLM Q&A | ExecuTorch | SmolVLM-500M on a photo or camera snapshot. |
| Depth, Object Detection | ExecuTorch | Depth Anything V2 Small, YOLO26s, on photos or the live camera. |
| Background Removal, Super-Resolution | ExecuTorch | MODNet (photo or live camera), Real-ESRGAN ×4. |
| Speech to Text | ExecuTorch | Whisper base on the microphone or an audio file. |

## Model licensing gate

All checkpoints are referenced from one module, `src/config/models.js`.
`resolveWeights()` in `src/loader/resolve.js` returns remote weights only when
the license is `cleared` **and** an HTTPS URL is set; `tests/licensing.test.mjs`
asserts both halves. To enable a model after review: set
`license.status: 'cleared'`, record the decision, and set `assets.baseUrl` (a
commit-pinned URL). The download path (`src/loader/fetch-progress.js`) streams
with byte progress and caches in Cache Storage.

## Runtime layer

- `runtime/`: the ExecuTorch Wasm runtime with its WebGPU backend, shared by
every demo. `runtime/BUILD_INFO.md` says how it was built and when to rebuild it.
- `src/runtime/executorch.js`: starts that runtime once (one WebGPU device),
loads `.pte` programs, and runs one call into it at a time.
- `src/runtime/sources.js`: where a model's files come from (cleared remote URL,
or the loopback dev config).
- `src/runtime/llm.js`: ExecuTorch LLM driver (batched prefill, decode, sampling).
- `src/runtime/forward.js`, `whisper.js`, `sam.js`: the other model drivers.
- `src/runtime/emscripten.js`: loads an Emscripten runtime script and its `.wasm`.
- `src/runtime/vlm.js`: the unlisted VLM demo, which still targets its own
runtime build.

## Shipping to executorch.ai

1. Settle model licensing and host the cleared `.pte` files at commit-pinned
URLs with CORS; fill in `models.js`. The runtime ships with the site
(`runtime/`); serve `.wasm` as `application/wasm`.
2. Copy `site/` changes upstream: `playground/` (hub + `webgpu/`), `examples/` (redirect), the homepage nav item and CTA,
and the `style.css` additions. `scripts/` and `tests/` can stay or be excluded.
3. HTTPS is required (WebGPU needs a secure context). Asset names are not
content-hashed, so keep HTML/JS/CSS on a short cache or revalidation policy.
4. The runtime is single-threaded, so cross-origin isolation (COOP/COEP) is
not required.
Loading
Loading