Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
38 changes: 35 additions & 3 deletions bin/nemoclaw.js
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,38 @@ async function setupSpark() {
run(`sudo bash "${SCRIPTS}/setup-spark.sh"`);
}

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

function firstNonEmptyValue(...values) {
for (const value of values) {
const trimmed = String(value ?? "").trim();
if (trimmed) return trimmed;
}
return "";
}

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

// eslint-disable-next-line complexity
async function deploy(instanceName) {
if (!instanceName) {
Expand All @@ -669,7 +701,7 @@ async function deploy(instanceName) {
validateName(instanceName, "instance name");
const name = instanceName;
const qname = shellQuote(name);
const gpu = process.env.NEMOCLAW_GPU || "a2-highgpu-1g:nvidia-tesla-a100:1";
const { type, gpuName } = resolveBrevCreateConfig();

console.log("");
console.log(` Deploying NemoClaw to Brev instance: ${name}`);
Expand All @@ -692,8 +724,8 @@ async function deploy(instanceName) {
}

if (!exists) {
console.log(` Creating Brev instance '${name}' (${gpu})...`);
run(`brev create ${qname} --gpu ${shellQuote(gpu)}`);
console.log(` Creating Brev instance '${name}' (${type}, ${gpuName})...`);
run(`brev create ${qname} --type ${shellQuote(type)} --gpu-name ${shellQuote(gpuName)}`);
} else {
console.log(` Brev instance '${name}' already exists.`);
}
Expand Down
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 @@ -90,12 +90,21 @@ $ openclaw agent --agent main --local -m "Hello from the remote sandbox" --sessi

## 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:
If you want to keep using the legacy combined setting, export `NEMOCLAW_GPU` before running `nemoclaw deploy`:

```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
169 changes: 169 additions & 0 deletions test/deploy-brev.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it } from "vitest";
import { execSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";

const CLI = path.join(import.meta.dirname, "..", "bin", "nemoclaw.js");

function runWithEnv(args: string, env: Record<string, string> = {}, timeout = 25_000) {
try {
const out = execSync(`node "${CLI}" ${args}`, {
encoding: "utf-8",
timeout,
env: {
...process.env,
HOME: fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-cli-brev-home-")),
NEMOCLAW_HEALTH_POLL_COUNT: "1",
NEMOCLAW_HEALTH_POLL_INTERVAL: "0",
...env,
},
});
return { code: 0, out };
} catch (err: any) {
return { code: err.status, out: (err.stdout || "") + (err.stderr || "") };
}
}

function writeStub(binDir: string, name: string, lines: string[]) {
fs.writeFileSync(path.join(binDir, name), lines.join("\n"), { mode: 0o755 });
}

function readBrevCalls(markerFile: string) {
return fs.readFileSync(markerFile, "utf8").trim().split("\n").filter(Boolean);
}

function setupDeployStubs({ brevLsOutput = "" }: { brevLsOutput?: string } = {}) {
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 });

writeStub(localBin, "brev", [
"#!/usr/bin/env bash",
`marker_file=${JSON.stringify(markerFile)}`,
'printf \'%s\\n\' "$*" >> "$marker_file"',
'if [ "$1" = "ls" ]; then',
` printf '%s' ${JSON.stringify(brevLsOutput)}`,
" exit 0",
"fi",
"exit 0",
]);
writeStub(localBin, "ssh", ["#!/usr/bin/env bash", "exit 0"]);
writeStub(localBin, "rsync", ["#!/usr/bin/env bash", "exit 0"]);
writeStub(localBin, "scp", ["#!/usr/bin/env bash", "exit 0"]);

return {
home,
localBin,
markerFile,
};
}

describe("deploy brev compatibility", () => {
it("uses --type and --gpu-name for legacy combined NEMOCLAW_GPU", () => {
const { home, localBin, markerFile } = setupDeployStubs();

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

expect(result.code).toBe(0);
const calls = readBrevCalls(markerFile);
expect(calls).toContain("ls");
expect(calls).toContain("create pr-1377-legacy --type a2-highgpu-1g --gpu-name A100");
expect(calls.join("\n")).not.toContain("--gpu ");
});

it("prefers explicit Brev override env vars over the legacy combined value", () => {
const { home, localBin, markerFile } = setupDeployStubs();

const result = runWithEnv("deploy pr-1377-overrides", {
HOME: home,
PATH: `${localBin}:${process.env.PATH || ""}`,
NVIDIA_API_KEY: "nvapi-test",
NEMOCLAW_GPU: "a2-highgpu-1g:nvidia-tesla-a100:1",
NEMOCLAW_BREV_TYPE: "a3-highgpu-1g",
NEMOCLAW_BREV_GPU_NAME: "h100",
});

expect(result.code).toBe(0);
const calls = readBrevCalls(markerFile);
expect(calls).toContain("create pr-1377-overrides --type a3-highgpu-1g --gpu-name H100");
expect(calls.join("\n")).not.toContain("--gpu ");
});

it("falls back to default Brev type and GPU name when no env vars are set", () => {
const { home, localBin, markerFile } = setupDeployStubs();

const result = runWithEnv("deploy pr-1377-defaults", {
HOME: home,
PATH: `${localBin}:${process.env.PATH || ""}`,
NVIDIA_API_KEY: "nvapi-test",
});

expect(result.code).toBe(0);
const calls = readBrevCalls(markerFile);
expect(calls).toContain("create pr-1377-defaults --type a2-highgpu-1g --gpu-name A100");
expect(calls.join("\n")).not.toContain("--gpu ");
});

it("treats whitespace-only overrides as unset and normalizes generic nvidia GPU names", () => {
const { home, localBin, markerFile } = setupDeployStubs();

const result = runWithEnv("deploy pr-1377-l40s", {
HOME: home,
PATH: `${localBin}:${process.env.PATH || ""}`,
NVIDIA_API_KEY: "nvapi-test",
NEMOCLAW_GPU: "a3-highgpu-1g:nvidia-l40s:1",
NEMOCLAW_BREV_TYPE: " ",
NEMOCLAW_BREV_GPU_NAME: " ",
});

expect(result.code).toBe(0);
const calls = readBrevCalls(markerFile);
expect(calls).toContain("create pr-1377-l40s --type a3-highgpu-1g --gpu-name L40S");
expect(calls.join("\n")).not.toContain("--gpu ");
});

it("skips brev create when the instance already exists", () => {
const { home, localBin, markerFile } = setupDeployStubs({
brevLsOutput: "pr-1377-existing\n",
});

const result = runWithEnv("deploy pr-1377-existing", {
HOME: home,
PATH: `${localBin}:${process.env.PATH || ""}`,
NVIDIA_API_KEY: "nvapi-test",
});

expect(result.code).toBe(0);
const calls = readBrevCalls(markerFile);
expect(calls).toContain("ls");
expect(calls).toContain("refresh");
expect(calls.some((call) => call.startsWith("create "))).toBe(false);
});

it("falls back to the default GPU name when normalization produces an empty value", () => {
const { home, localBin, markerFile } = setupDeployStubs();

const result = runWithEnv("deploy pr-1377-empty-gpu-name", {
HOME: home,
PATH: `${localBin}:${process.env.PATH || ""}`,
NVIDIA_API_KEY: "nvapi-test",
NEMOCLAW_BREV_TYPE: "a3-highgpu-1g",
NEMOCLAW_BREV_GPU_NAME: "nvidia-",
});

expect(result.code).toBe(0);
const calls = readBrevCalls(markerFile);
expect(calls).toContain("create pr-1377-empty-gpu-name --type a3-highgpu-1g --gpu-name A100");
});
});