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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ node_modules/
dist/
.astro/
.cache/
src/content/docs/
public/fluxserve/
output/
.playwright-cli/
Expand Down
35 changes: 12 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,7 @@ npm ci
npm run dev
```

Development imports documentation from the sibling `../FluxServe` checkout, including uncommitted documentation edits. To use another checkout:

```sh
FLUXSERVE_SOURCE=/absolute/path/to/FluxServe npm run dev
```
Development reads documentation directly from `src/content/docs/` in this repository.

Search indexes are built for production, so use the static preview to test search:

Expand All @@ -24,7 +20,7 @@ npm run build:local
npm run preview
```

`build:local` is a preview only. Do not upload its output to production.
`build:local` uses the same checked-in content and validation as the production build.

## Production build and verification

Expand All @@ -36,27 +32,21 @@ npm test
npm run preview
```

Every production build fetches the latest FluxServe `main`. The resolved commit is used for source attribution and repository-file links; no revision pin needs updating. It does not need a sibling checkout and rejects `FLUXSERVE_SOURCE`. Generated pages, imported assets, source caches, and build outputs are ignored by Git.
Production builds use the documentation committed to this repository. They do not fetch documentation from the FluxServe repository and do not require a sibling checkout or network access beyond dependency installation.

The build validates page titles, descriptions, canonical URLs, local links, heading anchors, assets, sitemap, and search output. Tests cover import failures, URL rewriting, stale-page removal, and blog publication. Blog integration tests create an isolated temporary site and verify that draft articles appear in neither routes nor search.

Browser verification covers responsive navigation, theme selection and persistence, keyboard access, search, and code copying. GPU examples are checked against FluxServe's source definitions; website checks do not execute GPU inference.

## Updating documentation

Technical content belongs in **FLX-OSS/FluxServe**, not this repository.

1. Edit the Markdown in FluxServe and preview it using `npm run dev` or `npm run build:local`.
2. Merge the documentation changes into FluxServe `main`.
3. Run the website’s **Build and deploy website** workflow manually, or let the next website `main` push rebuild it.

FluxServe merges do not currently trigger a website build automatically. The deployed site remains a static snapshot until the next successful build. The initial docs cleanup must be merged into FluxServe `main` before production builds can succeed.

Every Markdown file in FluxServe’s `docs/` folder is published automatically, preserving folders and filenames: `docs/serving/llada2.1.md` becomes `/docs/serving/llada2.1/`. An `index.md` becomes its folder’s entry page; `docs/index.md` is required. Titles come from the first level-one heading and descriptions from the first paragraph. The sidebar follows the folders. Keep internal planning notes outside `docs/`; there is no website page allowlist.
Technical content is maintained directly in `src/content/docs/` in this repository.

The importer rewrites links between public docs, copies referenced image/PDF assets, and points other repository-file links to the commit resolved for that build. Missing source files, asset files, and imported-page anchors fail the build. Each imported page links to its exact source revision.
1. Edit or add the Markdown under `src/content/docs/docs/`.
2. Preview changes with `npm run dev`, or use `npm run build:local` followed by `npm run preview` to verify the production output.
3. Commit the documentation changes with the website changes. A successful push to `main` deploys that committed snapshot.

The disposable source cache is under `.cache/fluxserve/main`. The importer only resets this cache; it never resets the local FluxServe checkout.
The directory beneath the content root determines the public route. For example, `src/content/docs/docs/configuration.md` is published at `/docs/configuration/`. Each page supplies its title, description, sidebar metadata, and optional edit link in YAML frontmatter.

## Writing a blog post

Expand All @@ -81,15 +71,14 @@ Set `draft: false` when ready to publish. Drafts are excluded from the blog inde

GitHub Pages must use **GitHub Actions** as its build source. The workflow checks pull requests without deploying and publishes successful `main` builds; it can also be started manually from Actions. It uses the built-in GitHub token, with Pages write permissions limited to the deployment job.

The canonical origin is `https://flx-oss.github.io` with no repository-name prefix. No custom domain is configured. To roll back documentation, revert the relevant change in FluxServe main and rebuild the website. Website code can be rolled back by reverting its commit.
The canonical origin is `https://flx-oss.github.io` with no repository-name prefix. No custom domain is configured. To roll back documentation or website code, revert the relevant commit in this repository and rebuild the website.

## Structure

- `fluxserve-docs.json`: source repository and shared brand assets.
- `scripts/`: Markdown importer and built-site link/metadata validation.
- `src/pages/`: custom homepage, blog, and 404; all documentation, including deployment guides, is imported under `/docs/`.
- `scripts/`: build validation and maintenance utilities.
- `src/pages/`: custom homepage, blog, and 404 pages.
- `src/components/`, `src/styles/`: shared navigation and FluxServe styling.
- `src/content/blog/`: authored blog Markdown.
- `src/content/docs/`, `public/fluxserve/`: generated and untracked.
- `src/content/docs/`: locally authored and tracked documentation Markdown.

The site is English-only and publishes one documentation revision at a time. There is no server, CMS, analytics, account system, or runtime dependency on a running FluxServe engine.
14 changes: 12 additions & 2 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ export default defineConfig({
site: 'https://flx-oss.github.io',
trailingSlash: 'always',
output: 'static',
redirects: {
'/docs/': '/docs/getting_started/',
'/docs/llada2.0-flash/': '/docs/model-recipes/',
'/docs/model-recipes/llada2.0-flash/': '/docs/model-recipes/',
},
cacheDir: './.astro/cache',
integrations: [starlight({
title: 'FluxServe',
description: 'A serving engine for diffusion language models.',
favicon: '/fluxserve-icon.png',
favicon: '/new_logo.png',
customCss: ['./src/styles/site.css', './src/styles/controls.css'],
credits: false,
disable404Route: true,
Expand All @@ -18,6 +23,11 @@ export default defineConfig({
PageTitle: './src/components/PageTitle.astro',
Footer: './src/components/Footer.astro',
},
sidebar: [{ autogenerate: { directory: 'docs' } }],
sidebar: [
{ label: 'Getting Started', slug: 'docs/getting_started' },
{ label: 'Configuration', slug: 'docs/configuration' },
{ label: 'Model Recipes', slug: 'docs/model-recipes' },
{ label: 'Benchmark Guide', slug: 'docs/benchmark-guide' },
],
})],
});
7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@
"npm": ">=10.8.2"
},
"scripts": {
"docs:import": "node scripts/import-docs.mjs",
"dev": "npm run docs:import -- --local && astro dev --host 127.0.0.1",
"build": "npm run docs:import && astro build && node scripts/check-site.mjs",
"build:local": "npm run docs:import -- --local && astro build && node scripts/check-site.mjs",
"dev": "astro dev --host 127.0.0.1",
"build": "astro build && node scripts/check-site.mjs",
"build:local": "npm run build",
"preview": "astro preview --host 127.0.0.1",
"check": "astro check",
"test": "node --test tests/*.test.mjs"
Expand Down
Binary file added public/fluxserve-favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/huggingface-icon.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/new_logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env bash

set -euo pipefail

cd -- "$(dirname -- "${BASH_SOURCE[0]}")"

# Stop a previous static preview, rebuild local content, and serve the new output.
npm run preview -- stop || true
npm exec -- astro build
npm run preview
3 changes: 3 additions & 0 deletions scripts/check-site.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@ for (const file of files.filter(file => file.endsWith('.html'))) {
const failures = [];
for (const [file, $] of html) {
const pathname = '/' + path.relative(root, file).replace(/index\.html$/, '');
const isRedirect = $('meta[http-equiv="refresh"]').length > 0;
if (!isRedirect) {
if ($('h1').length !== 1) failures.push(pathname + ': expected one h1');
if (!$('title').text() || !$('meta[name="description"]').attr('content'))
failures.push(pathname + ': missing title or description');
if (!$('link[rel="canonical"]').attr('href')?.startsWith('https://flx-oss.github.io/'))
failures.push(pathname + ': missing canonical URL');
}
for (const element of $('a[href],img[src],script[src],link[rel="stylesheet"][href]').toArray()) {
const url = $(element).attr('href') ?? $(element).attr('src');
if (!url || /^(https?:|mailto:|tel:|data:|\/\/)/i.test(url)) continue;
Expand Down
43 changes: 43 additions & 0 deletions scripts/import-docs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export async function importDocs({ sourceDir, outputDir, publicDir, config, loca
});
}
// Validate everything before replacing the generated output.
consolidateModelRecipes(rendered);
for (const asset of assets) await stat(within(sourceDir, asset));
await rm(outputDir, { recursive: true, force: true });
await rm(publicDir, { recursive: true, force: true });
Expand All @@ -133,6 +134,48 @@ export async function importDocs({ sourceDir, outputDir, publicDir, config, loca
return { pages: rendered.length, assets: assets.size };
}

export function consolidateModelRecipes(rendered) {
const recipes = [
['llada2-mini', 'LLaDA2.0 Mini'],
['llada2-flash', 'LLaDA2.0 Flash'],
['llada2.1', 'LLaDA2.1'],
];
const selected = recipes.map(([slug]) => rendered.find(page => page.file === `docs/serving/${slug}.md`));
if (selected.some(page => !page)) return;
const destination = '/docs/serving/model-recipes/';
const links = new Map();
const slugger = new GithubSlugger();
const sections = selected.map((page, i) => {
const [slug, title] = recipes[i];
const route = `/docs/serving/${slug}/`;
const sectionId = slugger.slug(title);
links.set(route, destination + '#' + sectionId);
links.set(route + '#_top', destination + '#' + sectionId);
const body = page.content.replace(/^---\n[\s\S]*?\n---\n/, '');
const tree = processor.parse(body);
const oldSlugger = new GithubSlugger();
visit(tree, 'heading', node => {
const heading = textOf(node);
links.set(route + '#' + oldSlugger.slug(heading), destination + '#' + slugger.slug(heading));
node.depth = Math.min(6, node.depth + 1);
});
return '## ' + title + '\n\n' + processor.stringify(tree);
});
for (const page of selected) rendered.splice(rendered.indexOf(page), 1);
rendered.push({
file: 'docs/serving/model-recipes.md',
content: '---\ntitle: Model Recipes\ndescription: Serving and benchmarking recipes for LLaDA2.0 Mini, LLaDA2.0 Flash, and LLaDA2.1.\neditUrl: false\n---\n\n' + sections.join('\n'),
});
for (const page of rendered) {
const frontmatter = page.content.match(/^---\n[\s\S]*?\n---\n/)[0];
const tree = processor.parse(page.content.slice(frontmatter.length));
visit(tree, node => {
if (['link', 'definition'].includes(node.type) && links.has(node.url)) node.url = links.get(node.url);
});
page.content = frontmatter + '\n' + processor.stringify(tree);
}
}

export async function fetchMain(sourceDir, repositoryURL) {
await mkdir(sourceDir, { recursive: true });
const git = (...args) => execFileSync('git', args, { cwd: sourceDir, stdio: 'pipe' }).toString().trim();
Expand Down
89 changes: 3 additions & 86 deletions src/components/BenchmarkChart.astro
Original file line number Diff line number Diff line change
@@ -1,88 +1,5 @@
---
// Approximate points read from the original published figure. Keep its axes and series.
const rates = [2, 4, 8, 12, 16];
const charts = [
{ model: 'Mini', dataset: 'BigCodeBench', config: 'TP=1 · EP=1', min: 690, max: 1340, ticks: [700, 800, 900, 1000, 1100, 1200, 1300], flux: [766, 1252, 1255, 1263, 1264], sglang: [830, 998, 1003, 997, 1005] },
{ model: 'Flash', dataset: 'BigCodeBench', config: 'TP=4 · EP=4', min: 604, max: 746, ticks: [620, 640, 660, 680, 700, 720, 740], flux: [709, 718, 720, 714, 720], sglang: [629, 632, 629, 633, 632] },
{ model: 'Mini', dataset: 'GSM8K', config: 'TP=1 · EP=1', min: 640, max: 935, ticks: [650, 700, 750, 800, 850, 900], flux: [675, 893, 897, 897, 903], sglang: [716, 836, 859, 860, 862] },
{ model: 'Flash', dataset: 'GSM8K', config: 'TP=4 · EP=4', min: 482, max: 548, ticks: [490, 500, 510, 520, 530, 540], flux: [513, 508, 520, 517, 523], sglang: [511, 509, 514, 514, 509] },
];
const x = (rate: number) => 60 + (rate - 1) / 16 * 420;
import SpeedBenchmarkChart from './SpeedBenchmarkChart.astro';
---
<section class="benchmark-tabs" aria-label="FluxServe benchmarks">
<div class="tab-list" role="tablist" aria-label="Benchmark">
{charts.map((chart, i) => <button type="button" role="tab" id={`benchmark-tab-${i}`} aria-controls={`benchmark-panel-${i}`} aria-selected={i === 0} tabindex={i === 0 ? 0 : -1}>
<span>{chart.model}</span><small>{chart.dataset}</small>
</button>)}
</div>
{charts.map((chart, i) => {
const y = (value: number) => 290 - (value - chart.min) / (chart.max - chart.min) * 255;
return <div role="tabpanel" id={`benchmark-panel-${i}`} aria-labelledby={`benchmark-tab-${i}`} hidden={i !== 0} tabindex="0">
<div class="legend"><span><i class="flux"></i>FluxServe</span><span><i class="sglang"></i>SGLang</span></div>
<svg viewBox="-25 0 535 345" role="img" aria-labelledby={`chart-title-${i} chart-desc-${i}`}>
<title id={`chart-title-${i}`}>{chart.model} · {chart.dataset}</title>
<desc id={`chart-desc-${i}`}>Decode throughput versus request rate, reconstructed from approximate points in the published figure. The vertical axis starts at {chart.min}, matching the original chart's range.</desc>
<text class="axis-label" transform="translate(-10 163) rotate(-90)" text-anchor="middle">Decode throughput (tokens/s)</text>
{chart.ticks.map(tick => <g><line class="grid" x1="60" x2="480" y1={y(tick)} y2={y(tick)} /><text x="48" y={y(tick) + 4} text-anchor="end">{tick}</text></g>)}
{[4, 8, 12, 16].map(rate => <g><line class="grid" x1={x(rate)} x2={x(rate)} y1="35" y2="290" /><text x={x(rate)} y="313" text-anchor="middle">{rate}</text></g>)}
<text x="270" y="339" text-anchor="middle">Request rate (req/s)</text>
<defs><clipPath id={`line-reveal-${i}`}><rect class="reveal" x="60" y="30" width="420" height="265" /></clipPath></defs>
<g clip-path={`url(#line-reveal-${i})`}>
{(['flux', 'sglang'] as const).map(engine => <g class={engine}>
<polyline points={chart[engine].map((value, index) => `${x(rates[index])},${y(value)}`).join(' ')} fill="none" stroke-width="3" stroke-dasharray={engine === 'sglang' ? '8 6' : undefined} />
{chart[engine].map((value, index) => <circle cx={x(rates[index])} cy={y(value)} r="3.5" />)}
</g>)}
</g>
</svg>
<p class="chart-note">LLaDA2.0-{chart.model.toLowerCase()} · {chart.config}</p>
</div>;
})}
<footer><a href="/docs/guides/benchmark/#published-performance-figure">Benchmark details ↗</a></footer>
</section>
<script>
document.querySelectorAll<HTMLElement>('.benchmark-tabs').forEach(card => {
const tabs = [...card.querySelectorAll<HTMLButtonElement>('[role="tab"]')];
const panels = [...card.querySelectorAll<HTMLElement>('[role="tabpanel"]')];
function select(index: number) {
tabs.forEach((tab, i) => { tab.setAttribute('aria-selected', String(i === index)); tab.tabIndex = i === index ? 0 : -1; panels[i].hidden = i !== index; });
tabs[index].focus();
}
tabs.forEach((tab, i) => {
tab.addEventListener('click', () => select(i));
tab.addEventListener('keydown', event => {
const next = event.key === 'ArrowRight' ? (i + 1) % tabs.length : event.key === 'ArrowLeft' ? (i + tabs.length - 1) % tabs.length : event.key === 'Home' ? 0 : event.key === 'End' ? tabs.length - 1 : null;
if (next !== null) { event.preventDefault(); select(next); }
});
});
});
</script>
<style>
.benchmark-tabs { min-width: 0; border: 1px solid var(--line); background: var(--sl-color-bg); }
.tab-list { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); border-bottom: 1px solid var(--line); }
button { font: inherit; cursor: pointer; color: var(--muted); background: transparent; border: 0; }
[role='tab'] { padding: 1rem .2rem; border-right: 1px solid var(--line); }
[role='tab']:last-child { border-right: 0; }
[role='tab'][aria-selected='true'] { color: var(--sl-color-white); background: var(--surface); box-shadow: inset 0 -2px var(--brand); }
[role='tab'] span { display: block; font-size: 1.1rem; font-weight: 650; }
[role='tab'] small { display: block; font-size: .8rem; margin-top: .2rem; }
[role='tabpanel'] { padding: 1.6rem 1.3rem .8rem; }
.chart-note { margin: .65rem 0 0; color: var(--muted); font-size: .75rem; font-weight: 400; }
.legend { display: flex; gap: 1.2rem; margin-top: 0; font-size: .7rem; color: var(--muted); }
.legend span { display: flex; align-items: center; gap: .4rem; }
.legend i { width: 20px; height: 0; border-top: 2px solid #c72635; }
.legend .sglang { border-top: 2px dashed #f58a16; }
svg { display: block; width: 100%; height: auto; overflow: visible; }
svg text { fill: var(--muted); font: 13px var(--sl-font-mono); }
.grid { stroke: var(--line); stroke-dasharray: 3 3; }
svg .flux { stroke: #c72635; fill: #c72635; }
svg .sglang { stroke: #f58a16; fill: #f58a16; }
:global([data-theme='dark']) svg .flux { stroke: #fa6173; fill: #fa6173; }
:global([data-theme='dark']) .legend .flux { border-color: #fa6173; }
.reveal { transform-box: fill-box; transform-origin: left center; animation: draw 1.6s ease-in-out both; }
footer { margin: 0 1.3rem; padding: .85rem 0; border-top: 1px solid var(--line); text-align: right; }
footer a { color: var(--muted); font-size: .7rem; }
button:focus-visible, [role='tabpanel']:focus-visible { outline: 2px solid var(--brand); outline-offset: -3px; }
@keyframes draw { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@media(prefers-reduced-motion: reduce) { .reveal { animation: none; } }
@media(max-width: 400px) { [role='tab'] span { font-size: 1rem; } [role='tab'] small { font-size: .7rem; } [role='tabpanel'] { padding: 1.2rem .65rem .5rem; } }
</style>

<SpeedBenchmarkChart />
Loading
Loading