Skip to content
Closed
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
Empty file modified bin/nemoclaw.js
100755 → 100644
Empty file.
15 changes: 12 additions & 3 deletions docs/deployment/deploy-to-remote-gpu.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,21 @@ Changing the proxy after onboarding requires re-running `nemoclaw onboard`.

## GPU Configuration

The deploy script uses the `NEMOCLAW_GPU` environment variable to select the GPU type.
The deploy script uses the `NEMOCLAW_GPU` environment variable to select the Brev instance type and GPU name.
The default value is `a2-highgpu-1g:nvidia-tesla-a100:1`.
Set this variable before running `nemoclaw deploy` to use a different GPU configuration:
Legacy `NEMOCLAW_GPU` usage:

```console
$ export NEMOCLAW_GPU="a2-highgpu-1g:nvidia-tesla-a100:2"
$ export NEMOCLAW_GPU="a2-highgpu-1g:nvidia-tesla-a100:1"
$ nemoclaw deploy <instance-name>
```

For direct overrides, set `NEMOCLAW_BREV_TYPE` and `NEMOCLAW_BREV_GPU_NAME` instead.
These take precedence over `NEMOCLAW_GPU` when both are set:

```console
$ export NEMOCLAW_BREV_TYPE="a2-highgpu-1g"
$ export NEMOCLAW_BREV_GPU_NAME="A100"
$ nemoclaw deploy <instance-name>
```

Expand Down
24 changes: 21 additions & 3 deletions src/lib/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,22 @@ export function inferDeployProvider(
return null;
}

function normalizeBrevGpuName(value: string | undefined): string {
const trimmed = String(value || "").trim();
if (!trimmed) return "";
return trimmed.replace(/^nvidia-tesla-/i, "").toUpperCase();
}

function resolveBrevCreateConfig(env: NodeJS.ProcessEnv): { type: string; gpuName: string } {
const legacyGpu = String(env.NEMOCLAW_GPU || "").trim();
const legacyParts = legacyGpu.includes(":") ? legacyGpu.split(":") : [legacyGpu];
const type = String(env.NEMOCLAW_BREV_TYPE || legacyParts[0] || "a2-highgpu-1g").trim();
const gpuName = normalizeBrevGpuName(
env.NEMOCLAW_BREV_GPU_NAME || legacyParts[1] || "A100",
);
return { type, gpuName };
}

export function buildDeployEnvLines(opts: {
env: NodeJS.ProcessEnv;
sandboxName: string;
Expand Down Expand Up @@ -240,7 +256,7 @@ export async function executeDeploy(opts: DeployExecutionOptions): Promise<void>

const name = validateName(instanceName, "instance name");
const qname = shellQuote(name);
const gpu = env.NEMOCLAW_GPU || "a2-highgpu-1g:nvidia-tesla-a100:1";
const { type, gpuName } = resolveBrevCreateConfig(env);
const brevProvider = String(env.NEMOCLAW_BREV_PROVIDER || "gcp").trim().toLowerCase();
const skipConnect = ["1", "true"].includes(
String(env.NEMOCLAW_DEPLOY_NO_CONNECT || "").toLowerCase(),
Expand Down Expand Up @@ -296,8 +312,10 @@ export async function executeDeploy(opts: DeployExecutionOptions): Promise<void>
}

if (!exists) {
log(` Creating Brev instance '${name}' (${gpu}, provider=${brevProvider})...`);
run(`brev create ${qname} --type ${shellQuote(gpu)} --provider ${shellQuote(brevProvider)}`);
log(` Creating Brev instance '${name}' (${type}, ${gpuName}, provider=${brevProvider})...`);
run(
`brev create ${qname} --type ${shellQuote(type)} --gpu-name ${shellQuote(gpuName)} --provider ${shellQuote(brevProvider)}`,
);
} else {
log(` Brev instance '${name}' already exists.`);
}
Expand Down
188 changes: 188 additions & 0 deletions test/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ function runWithEnv(args, env = {}, timeout = 10000) {
}
}

function readCommandTokens(markerFile) {
return fs
.readFileSync(markerFile, "utf8")
.trim()
.split(/\s+/)
.filter(Boolean);
}

describe("CLI dispatch", () => {
it("help exits 0 and shows sections", () => {
const r = run("help");
Expand Down Expand Up @@ -253,6 +261,7 @@ describe("CLI dispatch", () => {
expect(r.code).toBe(0);
expect(fs.readFileSync(markerFile, "utf8")).toContain("logs alpha --tail");
expect(fs.readFileSync(markerFile, "utf8")).not.toContain("--follow");
expect(readCommandTokens(markerFile)).toEqual(["logs", "alpha", "--tail"]);
});

it("destroys the gateway runtime when the last sandbox is removed", () => {
Expand Down Expand Up @@ -738,6 +747,185 @@ describe("CLI dispatch", () => {
expect(r.out.includes("Upgrade OpenShell by rerunning `nemoclaw onboard`")).toBeTruthy();
});

it("deploy uses brev --type and --gpu-name instead of legacy --gpu", () => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-cli-deploy-brev-"));
const localBin = path.join(home, "bin");
const markerFile = path.join(home, "brev-args");
fs.mkdirSync(localBin, { recursive: true });

fs.writeFileSync(
path.join(localBin, "brev"),
[
"#!/usr/bin/env bash",
`marker_file=${JSON.stringify(markerFile)}`,
"printf '%s\\n' \"$*\" >> \"$marker_file\"",
"if [ \"$1\" = \"ls\" ] && [ \"$2\" = \"--json\" ]; then",
" echo '[{\"name\":\"pr-998-test\",\"status\":\"RUNNING\",\"build_status\":\"COMPLETED\",\"shell_status\":\"READY\"}]'",
" exit 0",
"fi",
"if [ \"$1\" = \"ls\" ]; then",
" exit 0",
"fi",
"exit 0",
].join("\n"),
{ mode: 0o755 }
);
fs.writeFileSync(
path.join(localBin, "ssh"),
[
"#!/usr/bin/env bash",
'if [ "$1" = "-o" ] || [ "$1" = "-F" ] || [ "$1" = "-q" ] || [ "$1" = "-t" ]; then',
" if printf '%s\\n' \"$*\" | grep -q 'echo \\$HOME'; then",
` echo ${JSON.stringify(home)}`,
" fi",
" exit 0",
"fi",
"exit 0",
].join("\n"),
{ mode: 0o755 }
);
fs.writeFileSync(path.join(localBin, "rsync"), "#!/usr/bin/env bash\nexit 0\n", { mode: 0o755 });
fs.writeFileSync(path.join(localBin, "scp"), "#!/usr/bin/env bash\nexit 0\n", { mode: 0o755 });
fs.writeFileSync(path.join(localBin, "ssh-keyscan"), "#!/usr/bin/env bash\necho 'test-host-key'\n", {
mode: 0o755,
});

const r = runWithEnv("deploy pr-998-test", {
HOME: home,
PATH: `${localBin}:${process.env.PATH || ""}`,
NVIDIA_API_KEY: "nvapi-test",
NEMOCLAW_GPU: "a2-highgpu-1g:nvidia-tesla-a100:1",
NEMOCLAW_DEPLOY_NO_CONNECT: "1",
NEMOCLAW_DEPLOY_NO_START_SERVICES: "1",
}, 25000);

expect(r.code).toBe(0);
const calls = fs.readFileSync(markerFile, "utf8").trim().split("\n");
expect(
calls.some((call) =>
call.includes("create pr-998-test --type a2-highgpu-1g --gpu-name A100 --provider gcp"),
),
).toBe(true);
expect(calls.some((call) => call.includes("--gpu "))).toBe(false);
}, 25000);

it("deploy prefers direct Brev overrides when provided", () => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-cli-deploy-brev-override-"));
const localBin = path.join(home, "bin");
const markerFile = path.join(home, "brev-args");
fs.mkdirSync(localBin, { recursive: true });

fs.writeFileSync(
path.join(localBin, "brev"),
[
"#!/usr/bin/env bash",
`marker_file=${JSON.stringify(markerFile)}`,
"printf '%s\\n' \"$*\" >> \"$marker_file\"",
"if [ \"$1\" = \"ls\" ] && [ \"$2\" = \"--json\" ]; then",
" echo '[{\"name\":\"pr-998-override\",\"status\":\"RUNNING\",\"build_status\":\"COMPLETED\",\"shell_status\":\"READY\"}]'",
" exit 0",
"fi",
"if [ \"$1\" = \"ls\" ]; then",
" exit 0",
"fi",
"exit 0",
].join("\n"),
{ mode: 0o755 }
);
fs.writeFileSync(
path.join(localBin, "ssh"),
[
"#!/usr/bin/env bash",
"if printf '%s\\n' \"$*\" | grep -q 'echo \\$HOME'; then",
` echo ${JSON.stringify(home)}`,
"fi",
"exit 0",
].join("\n"),
{ mode: 0o755 }
);
fs.writeFileSync(path.join(localBin, "rsync"), "#!/usr/bin/env bash\nexit 0\n", { mode: 0o755 });
fs.writeFileSync(path.join(localBin, "scp"), "#!/usr/bin/env bash\nexit 0\n", { mode: 0o755 });
fs.writeFileSync(path.join(localBin, "ssh-keyscan"), "#!/usr/bin/env bash\necho 'test-host-key'\n", {
mode: 0o755,
});

const r = runWithEnv("deploy pr-998-override", {
HOME: home,
PATH: `${localBin}:${process.env.PATH || ""}`,
NVIDIA_API_KEY: "nvapi-test",
NEMOCLAW_GPU: "a2-highgpu-1g:nvidia-tesla-a100:1",
NEMOCLAW_BREV_TYPE: "l4-standard",
NEMOCLAW_BREV_GPU_NAME: "l4",
NEMOCLAW_DEPLOY_NO_CONNECT: "1",
NEMOCLAW_DEPLOY_NO_START_SERVICES: "1",
}, 25000);

expect(r.code).toBe(0);
const calls = fs.readFileSync(markerFile, "utf8").trim().split("\n");
expect(
calls.some((call) =>
call.includes("create pr-998-override --type l4-standard --gpu-name L4 --provider gcp"),
),
).toBe(true);
}, 25000);

it("deploy falls back to the default Brev values when no GPU env is set", () => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-cli-deploy-brev-default-"));
const localBin = path.join(home, "bin");
const markerFile = path.join(home, "brev-args");
fs.mkdirSync(localBin, { recursive: true });

fs.writeFileSync(
path.join(localBin, "brev"),
[
"#!/usr/bin/env bash",
`marker_file=${JSON.stringify(markerFile)}`,
"printf '%s\\n' \"$*\" >> \"$marker_file\"",
"if [ \"$1\" = \"ls\" ] && [ \"$2\" = \"--json\" ]; then",
" echo '[{\"name\":\"pr-998-default\",\"status\":\"RUNNING\",\"build_status\":\"COMPLETED\",\"shell_status\":\"READY\"}]'",
" exit 0",
"fi",
"if [ \"$1\" = \"ls\" ]; then",
" exit 0",
"fi",
"exit 0",
].join("\n"),
{ mode: 0o755 }
);
fs.writeFileSync(
path.join(localBin, "ssh"),
[
"#!/usr/bin/env bash",
"if printf '%s\\n' \"$*\" | grep -q 'echo \\$HOME'; then",
` echo ${JSON.stringify(home)}`,
"fi",
"exit 0",
].join("\n"),
{ mode: 0o755 }
);
fs.writeFileSync(path.join(localBin, "rsync"), "#!/usr/bin/env bash\nexit 0\n", { mode: 0o755 });
fs.writeFileSync(path.join(localBin, "scp"), "#!/usr/bin/env bash\nexit 0\n", { mode: 0o755 });
fs.writeFileSync(path.join(localBin, "ssh-keyscan"), "#!/usr/bin/env bash\necho 'test-host-key'\n", {
mode: 0o755,
});

const r = runWithEnv("deploy pr-998-default", {
HOME: home,
PATH: `${localBin}:${process.env.PATH || ""}`,
NVIDIA_API_KEY: "nvapi-test",
NEMOCLAW_DEPLOY_NO_CONNECT: "1",
NEMOCLAW_DEPLOY_NO_START_SERVICES: "1",
}, 25000);

expect(r.code).toBe(0);
const calls = fs.readFileSync(markerFile, "utf8").trim().split("\n");
expect(
calls.some((call) =>
call.includes("create pr-998-default --type a2-highgpu-1g --gpu-name A100 --provider gcp"),
),
).toBe(true);
}, 25000);

it("connect does not pre-start a duplicate port forward", () => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-cli-connect-forward-"));
const localBin = path.join(home, "bin");
Expand Down