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
13 changes: 11 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,10 @@ question worth answering is which combinations actually work. Below is every
combination of the two MLP policies and the two algorithms on four tasks, and
pi0.5 on LIBERO. The border and its label say what the experiments found. The
line under each clip is the training return of all three seeds, drawn on one
scale per column, so a flat line really is flat. Hover over a cell, or tap
it, to play it.
scale per column, so a flat line really is flat. Hover over a cell to play
it; click or tap it to play it and see the two commands that trained it.
From one cell to the next, only the words that name the policy, the
algorithm and the task change.

<div class="cov" data-src="/media/coverage/coverage.json"></div>

Expand All @@ -103,6 +105,13 @@ defect of ours we are still tracking down, not a finding about FPO. The
scripts that made all of this are in
[figures/coverage](https://github.com/PlugRL/plugrl-server/tree/main/figures/coverage).

The two sides do not even share a Python environment. None of the servers
that trained these cells has MuJoCo, robosuite or gymnasium installed. The
env clients ran in three separate environments: gymnasium with MuJoCo 3 for
the MuJoCo tasks, and robosuite 1.4.1 with MuJoCo 2.3.7 for robomimic and for
LIBERO, because robosuite 1.4.1 does not run on MuJoCo 3. One server
codebase trained all of them.

## A real VLA, end to end

<video src="/media/libero-base.mp4" autoplay loop muted playsinline controls
Expand Down
8 changes: 7 additions & 1 deletion docs/index.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ dummy 算法的 `learn` 是一个 sleep,不会移动任何权重。它用来
PlugRL 把策略、算法和环境拆开,所以真正值得回答的问题是:哪些组合真的能跑。下面是
两个 MLP 策略和两个算法在四个任务上的全部组合,外加 pi0.5 在 LIBERO 上的情况。边框
和上面的标签写的是实验得出的结论。每段视频下面那条线是三个种子的训练回报,同一列用同
一个纵轴,所以平的线就是真的没动。鼠标悬停或者点一下格子就能播放。
一个纵轴,所以平的线就是真的没动。鼠标悬停就能播放;点一下格子,除了播放,还会在下面显示训练
它的那两条命令。从一格换到另一格,变的只有指定策略、算法和任务的那几个词。

<div class="cov" data-src="/media/coverage/coverage.json"></div>

Expand All @@ -88,6 +89,11 @@ PlugRL 把策略、算法和环境拆开,所以真正值得回答的问题是
零,是我们平台的问题,还在排查,不是关于 FPO 的结论。生成这些内容的脚本在
[figures/coverage](https://github.com/PlugRL/plugrl-server/tree/main/figures/coverage)。

训练端和环境端连 Python 环境都不共用。训练这些格子的服务端,没有一台装了 MuJoCo、robosuite
或 gymnasium。环境客户端分别跑在三套独立的环境里:MuJoCo 那几个任务用的是 gymnasium 加
MuJoCo 3;robomimic 和 LIBERO 用的是 robosuite 1.4.1 加 MuJoCo 2.3.7,因为 robosuite 1.4.1
在 MuJoCo 3 上跑不起来。训练它们的是同一个服务端代码库。

## 真实 VLA,端到端

<video src="/media/libero-base.mp4" autoplay loop muted playsinline controls
Expand Down
128 changes: 123 additions & 5 deletions docs/javascripts/coverage.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@
* Builds every <div class="cov" data-src="..."> from the JSON that
* plugrl-server's figures/coverage/data.py writes. Each cell shows a still;
* its clip loads on first use and plays on hover (mouse) or on tap / Enter.
* With reduced motion requested, hovering plays nothing.
* With reduced motion requested, hovering plays nothing. Clicking a cell
* also shows the two commands that trained it in the panel under its block,
* marking the words that differ from the cell shown before.
*/
(function () {
"use strict";

const EXPERIMENTS = "https://github.com/PlugRL/plugrl-server/tree/main/experiments/";
// The cells whose commands show before anyone clicks.
const DEFAULT_CELL = "fpo-fpo-hopper";
const DEFAULT_VLA = "pi0-fpo";

const TEXT = {
en: {
Expand Down Expand Up @@ -36,6 +41,19 @@
of: "of",
successes: "successes in 50 episodes",
missing: "clip not recorded",
cmdTitle: (row, col) => `${row} on ${col}`,
cmdSource: "as its experiment ran it",
server: "Training server",
serverNote: "no MuJoCo, robosuite or gymnasium installed",
client: "Env client",
clientNote: (e) => `uv sync --extra ${e.extra}: ${e.packages}`,
cmdHint:
"Click another cell: the highlighted words are what changed. Shown with seed 0 " +
"(the experiments ran seeds 0-2); the client's episode count is only an upper bound, " +
"the server ends the run.",
vlaHint: "Cluster paths are placeholders; the linked script has the rest.",
copy: "Copy",
copied: "Copied",
},
zh: {
status: {
Expand All @@ -62,6 +80,18 @@
of: "/",
successes: "50 个回合里的成功次数",
missing: "没有录像",
cmdTitle: (row, col) => `${row},${col}`,
cmdSource: "实验里实际跑的脚本",
server: "训练服务端",
serverNote: "没装 MuJoCo、robosuite 或 gymnasium",
client: "环境客户端",
clientNote: (e) => `uv sync --extra ${e.extra}:${e.packages}`,
cmdHint:
"点另一格:高亮的就是变了的词。这里统一写成种子 0(实验跑的是 0 到 2 三个种子);" +
"客户端的回合数只是个上限,什么时候停由服务端决定。",
vlaHint: "集群上的路径用占位符代替,完整内容见链接的脚本。",
copy: "复制",
copied: "已复制",
},
};

Expand Down Expand Up @@ -153,6 +183,66 @@
return p;
}

const SOURCES = "https://github.com/PlugRL/plugrl-server/blob/main/";
const ENTRY = { server: "plugrl-run-server", client: "plugrl-run-env-client" };

// One command as a code block. Words absent from `before` - the previously
// shown cell's commands - are marked, so a click shows what a swap changes.
function command(label, note, entry, lines, before, t) {
const all = [`${entry} ${lines[0]}`, ...lines.slice(1)];
const code = el("code");
all.forEach((line, i) => {
if (i) code.append("\n ");
line.split(" ").forEach((word, j) => {
if (j) code.append(" ");
code.append(before && !before.has(word) ? el("mark", { text: word }) : word);
});
if (i < all.length - 1) code.append(" \\");
});
const text = all.join(" \\\n ");
const copy = el("button", { type: "button", class: "cov-copy", text: t.copy });
copy.addEventListener("click", () => {
if (!navigator.clipboard) return;
navigator.clipboard.writeText(text).then(() => {
copy.textContent = t.copied;
setTimeout(() => (copy.textContent = t.copy), 1500);
}, () => {});
});
return el("div", { class: "cov-cmd-block" },
el("p", { class: "cov-cmd-label" }, el("strong", { text: label }), el("span", { text: note })),
el("div", { class: "cov-cmd-code" }, el("pre", {}, code), copy));
}

// The panel under a block: the selected cell's server and client commands.
function panel(data, t, hint) {
const root = el("div", { class: "cov-cmd", "aria-live": "polite" });
let previous = null;
let selected = null;
const words = (tr) =>
new Set([ENTRY.server, ENTRY.client, ...tr.server, ...tr.client].join(" ").split(/\s+/));
const show = (c, node, title) => {
if (selected) selected.classList.remove("is-selected");
selected = node;
node.classList.add("is-selected");
const tr = c.train;
const before = previous && previous !== tr ? words(previous) : null;
root.replaceChildren(
el("p", { class: "cov-cmd-title" },
el("strong", { text: title }), " · ", el("a", { href: SOURCES + tr.source, text: t.cmdSource })),
command(t.server, t.serverNote, ENTRY.server, tr.server, before, t),
command(t.client, t.clientNote(data.envs[tr.env]), ENTRY.client, tr.client, before, t),
el("p", { class: "cov-cmd-hint", text: hint }),
);
previous = tr;
};
return { root, show };
}

function selectable(node, onSelect) {
node.addEventListener("click", (e) => !e.target.closest("a") && onSelect());
node.addEventListener("keydown", (e) => (e.key === "Enter" || e.key === " ") && onSelect());
}

function cell(c, data, base, t, zh) {
const node = el("figure", { class: "cov-cell", "data-status": c.status, tabindex: 0 });
const m = media(c.id, c.clip, base, t, t.clip);
Expand Down Expand Up @@ -193,29 +283,57 @@
const t = zh ? TEXT.zh : TEXT.en;
const base = root.dataset.src.replace(/\/[^/]*$/, "");

const mlp = panel(data, t, t.cmdHint);
let first = null;
// A link to #cov-<cell id> opens with that cell selected after the default
// one, so the words that differ between the two are already marked.
const wanted = (location.hash.match(/^#cov-([\w-]+)$/) || [])[1];
let linked = null;
const grid = el("div", { class: "cov-grid" });
data.columns.forEach((col) => grid.append(el("div", { class: "cov-colhead", text: col.label })));
data.rows.forEach((row) => {
grid.append(el("div", { class: "cov-rowhead", text: row.label }));
data.columns.forEach((col) => {
const c = data.cells.find((x) => x.row === row.id && x.column === col.id);
grid.append(c ? cell(c, data, base, t, zh) : el("div"));
if (!c) return grid.append(el("div"));
const node = cell(c, data, base, t, zh);
const pick = () => mlp.show(c, node, t.cmdTitle(row.label, col.label));
selectable(node, pick);
if (c.id === DEFAULT_CELL) first = pick;
if (c.id === wanted) linked = { pick, node };
grid.append(node);
});
});
root.append(el("div", { class: "cov-scroll" }, grid));

const legend = el("p", { class: "cov-legend" });
Object.entries(t.legend).forEach(([s, label]) => legend.append(el("span", { "data-status": s, text: label })));
root.append(legend);
root.append(legend, mlp.root);
if (first) first();

if (data.vla) {
const vla = panel(data, t, t.vlaHint);
let firstVla = null;
const row = el("div", { class: "cov-vla-grid" });
data.vla.cells.forEach((c) => row.append(vlaCell(c, data, base, t, zh)));
data.vla.cells.forEach((c) => {
const node = vlaCell(c, data, base, t, zh);
const pick = () => vla.show(c, node, zh ? c.label_zh : c.label);
selectable(node, pick);
if (c.id === DEFAULT_VLA) firstVla = pick;
if (c.id === wanted) linked = { pick, node };
row.append(node);
});
root.append(
el("div", { class: "cov-vla" },
el("p", { class: "cov-vla-title", text: zh ? data.vla.task_zh : data.vla.task }),
el("div", { class: "cov-scroll" }, row)),
el("div", { class: "cov-scroll" }, row),
vla.root),
);
if (firstVla) firstVla();
}
if (linked) {
linked.pick();
linked.node.scrollIntoView({ block: "center" });
}
}

Expand Down
Binary file added docs/media/coverage-grid.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/media/coverage/coverage.json

Large diffs are not rendered by default.

64 changes: 64 additions & 0 deletions docs/stylesheets/coverage.css
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,70 @@
.cov-legend [data-status="flat"] { --cov-c: var(--cov-flat); }
.cov-legend [data-status="collapses"] { --cov-c: var(--cov-collapses); }

/* The selected cell, and the panel with its two commands. */
.md-typeset .cov-cell.is-selected {
box-shadow: 0 0 0 2px var(--cov-ground), 0 0 0 4px var(--cov-c);
}

.cov-cmd {
margin-top: 0.8rem;
padding: 0.55rem 0.7rem 0.6rem;
border: 1px solid var(--cov-rule);
border-radius: 6px;
}

.md-typeset .cov-cmd p { margin: 0; }

.cov-cmd-title { font-size: 0.7rem; }

.md-typeset .cov-cmd-label {
display: flex;
flex-wrap: wrap;
gap: 0.2rem 0.6rem;
align-items: baseline;
margin: 0.55rem 0 0.25rem;
}

.cov-cmd-label span {
color: var(--cov-ink-soft);
font-family: var(--md-code-font-family, monospace);
font-size: 0.6rem;
}

.cov-cmd-code { position: relative; }

.md-typeset .cov-cmd-code pre {
margin: 0;
font-size: 0.64rem;
}

.md-typeset .cov-cmd-code pre > code { padding: 0.55rem 3.6rem 0.55rem 0.7rem; }

.md-typeset .cov-cmd mark { padding: 0 0.1em; }

.cov-copy {
position: absolute;
top: 0.35rem;
right: 0.35rem;
padding: 0.1rem 0.45rem;
border: 1px solid var(--cov-rule);
border-radius: 3px;
background: var(--cov-ground);
color: var(--cov-ink-soft);
font-size: 0.6rem;
cursor: pointer;
}

.cov-copy:hover,
.cov-copy:focus-visible { color: var(--cov-ink); }

.md-typeset .cov-cmd-hint {
margin-top: 0.45rem;
color: var(--cov-ink-soft);
font-size: 0.64rem;
line-height: 1.45;
}

.cov-vla {
margin-top: 1.4rem;
}
Expand Down
Loading