Skip to content
Open
22 changes: 19 additions & 3 deletions bin/nemoclaw.js
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,22 @@ 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 resolveBrevCreateConfig() {
const legacyGpu = String(process.env.NEMOCLAW_GPU || "").trim();
const legacyParts = legacyGpu.includes(":") ? legacyGpu.split(":") : [legacyGpu];
const type = String(process.env.NEMOCLAW_BREV_TYPE || legacyParts[0] || "a2-highgpu-1g").trim();
const gpuName = normalizeBrevGpuName(
process.env.NEMOCLAW_BREV_GPU_NAME || legacyParts[1] || "A100",
);
return { type, gpuName };
}

// eslint-disable-next-line complexity
async function deploy(instanceName) {
if (!instanceName) {
Expand All @@ -380,7 +396,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 @@ -403,8 +419,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
116 changes: 116 additions & 0 deletions test/deploy-brev.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// 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() {
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',
" 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 ");
});
});