dtamind-nim-container-manager
Version:
DTAmind NVIDIA NIM Container Manager
993 lines (902 loc) • 34.2 kB
JavaScript
/*
Author: Timothy Carambat from Mintplex Labs
*/
const { execSync, spawnSync, spawn } = require("child_process");
const Docker = require("dockerode");
const os = require("os");
const path = require("path");
const fs = require("fs");
const { request } = require("node:http");
const { EventEmitter } = require("stream");
const WLS_CONTAINER_NAME = "NVIDIA-Workbench";
const WLS_CONTAINER_SERVICE_PORT = 11444;
process.env.LLM_PROVIDER = 'nvidia-nim';
process.env.NVIDIA_NIM_LLM_MODE = 'managed';
/**
* @class NimContainerManager
* @description Manages the container runtime for NVIDIA NIM via interfaceing with the podman API
* but is suitable for Docker as well with minor modifications. Current target is podman 5.3.1.
*/
class NimContainerManager {
/** @type {import("dockerode")|null}*/
client;
localNimCache;
ngcApiKey;
pullImageEmitter = new EventEmitter();
wslContainerService = {
ip: null,
apiVersion: null,
port: null,
};
/** @type {NimContainerManager|null} */
static _instance = null;
constructor() {
if (NimContainerManager._instance) return NimContainerManager._instance;
NimContainerManager._instance = this;
}
/**
* Preloads the container manager client if the runtime is valid
* and docker/podman is available on the current machine AND the LLM is set to use Nvidia NIM
* @returns {Promise<NimContainerManager|null>}
*/
static async preload() {
if (
process.env.LLM_PROVIDER === "nvidia-nim" &&
process.env.NVIDIA_NIM_LLM_MODE === "managed"
) {
NimContainerManager.#slog(
"Preloading container manager client for Nvidia NIM since it is the user's LLM provider & managed mode is enabled."
);
await NimContainerManager.setupContainerManager();
if (!NimContainerManager.getWSLWorkbenchContainer()) {
NimContainerManager.#slog(
"NIM Workbench wsl container not found - reverting to remote mode."
);
process.env.NVIDIA_NIM_LLM_MODE = "remote";
}
}
}
/**
* Unloads the container manager client if the runtime is valid
* and docker/podman is available on the current machine
* This is a general cleanup method for when the user is switching
* between managed and remote modes.
* @param {boolean} precheck - Whether to check the environment variables before unloading.
*/
static unload(precheck = true) {
if (precheck) {
if (process.env.LLM_PROVIDER !== "nvidia-nim") return;
if (process.env.NVIDIA_NIM_LLM_MODE !== "managed") return;
}
NimContainerManager.#slog(
"Unloading container manager client for Nvidia NIM since it is the user's LLM provider & managed mode is enabled."
);
if (!NimContainerManager.getWSLWorkbenchContainer()) return;
spawn("wsl", ["-d", WLS_CONTAINER_NAME, "--", "podman", "stop", "--all"]);
}
/**
* Sets up the container manager client if the runtime is valid
* and docker/podman is available on the current machine
* @returns {Promise<NimContainerManager|null>}
*/
static async setupContainerManager() {
const manager = new NimContainerManager();
if (
!NimContainerManager.validRuntime() ||
!(await NimContainerManager.containerRuntimeIsAvailable())
) {
manager.client = null;
return null;
}
const wslContainerService = NimContainerManager.bootWSLContainerService();
manager.client = new Docker({
host: wslContainerService.ip,
protocol: "http",
port: wslContainerService.port,
});
manager.wslContainerService = wslContainerService;
manager.setEnvs();
return manager;
}
/**
* Static log method
* @param {string} text
* @param {...any} args
*/
static #slog(text, ...args) {
console.log(`\x1b[35m[NimContainerManager]\x1b[0m ${text}`, ...args);
}
/**
* Detects if the current runtime is valid for using container manager
* @returns {boolean}
*/
static validRuntime() {
if (process.platform !== "win32") {
this.#slog(
"NIM container management is not supported on this platform - only Windows is supported at this time."
);
return false;
}
return true;
}
/**
* Gets the WSL Workbench container if it exists
* @returns {{name: string, state: string, version: string}|null}
*/
static getWSLWorkbenchContainer() {
const { stdout } = spawnSync("wsl", ["--list", "--verbose"], {
encoding: "utf16le",
stdout: "pipe",
});
const lines = stdout
.toString()
.split("\n")
.filter((line) => line.trim()) // Remove empty lines
.slice(1); // Remove header line
const wslContainers = lines.map((line) => {
const [name, state, version] = line.startsWith('*') ? line.replace('*', '').trim().split(/\s+/) : line.trim().split(/\s+/);
return {
name: name.replace("*", "").trim(), // Remove * from default distribution
state: state?.trim(),
version: version?.trim(),
};
});
return wslContainers.find(
(container) => container.name === WLS_CONTAINER_NAME
);
}
/**
* Boots the WSL container service if it is not already running
* @returns {{ip: string, apiVersion: string, port: number}|null}
*/
static bootWSLContainerService() {
NimContainerManager.#slog("Booting WSL container service...");
const version = spawnSync(
"wsl",
["-d", WLS_CONTAINER_NAME, "--", "podman", "version"],
{
encoding: "utf8",
stdio: "pipe",
}
)
.stdout.toString()
.split("\n")
.find((line) => line.includes("API Version"))
.split(":")
.pop()
.trim();
// Kill the existing podman service if it is running
spawnSync("wsl", [
"-d",
WLS_CONTAINER_NAME,
"--",
"kill",
"-9",
`$(lsof -t -i:${WLS_CONTAINER_SERVICE_PORT})`,
]);
// Start the podman service - if the port is already in use, it will fail
// but it is ignored as the container will still start and the podman service will be alive.
NimContainerManager.#slog(
"Starting podman API service in NVIDIA-Workbench WSL container. Check /home/workbench/podman-service.log in the container for podman API logs."
);
spawn("wsl", [
"-d",
WLS_CONTAINER_NAME,
"--",
"podman",
"system",
"service",
"--time=0",
`tcp://0.0.0.0:${WLS_CONTAINER_SERVICE_PORT}`,
"&>",
"/home/workbench/podman-service.log",
]);
const wslContainerIp = spawnSync(
"wsl",
[
"-d",
WLS_CONTAINER_NAME,
"--",
"bash",
"-c",
"ip addr show eth0 | grep 'inet ' | awk '{print $2}' | cut -d'/' -f1 | sed 's/inet //'",
],
{
encoding: "utf8",
stdout: "pipe",
}
)
.stdout.toString()
.trim();
NimContainerManager.#slog(
`WSL container service booted successfully. Podman API(v${version}) is available at: ${wslContainerIp}:${WLS_CONTAINER_SERVICE_PORT}`
);
return {
ip: wslContainerIp,
apiVersion: version,
port: WLS_CONTAINER_SERVICE_PORT,
};
}
/**
* Checks if container runtime is available on the current machine
* and we can interact with it
* @returns {Promise<boolean>}
*/
static async containerRuntimeIsAvailable() {
try {
if (!NimContainerManager.validRuntime()) return false;
const workbenchContainer = NimContainerManager.getWSLWorkbenchContainer();
if (!workbenchContainer)
throw new Error(`${WLS_CONTAINER_NAME} container not found`);
if (workbenchContainer.state !== "Running") {
this.#slog(
`${WLS_CONTAINER_NAME} container is not running, starting it now...`
);
spawnSync("wsl", ["-d", WLS_CONTAINER_NAME, "--", "pwd"]);
}
this.#slog(`${WLS_CONTAINER_NAME} container is running, continuing...`);
const wslContainerService = NimContainerManager.bootWSLContainerService();
const client = new Docker({
host: wslContainerService.ip,
protocol: "http",
port: wslContainerService.port,
});
const info = await client.info();
return !!info;
} catch (err) {
this.#slog(
"Container runtime is not available on this client, is not running, or is invalid.",
err.message
);
return false;
}
}
/**
* Converts some common container runtime errors into more readable messages
* @param {string} error - The error message to clean up.
* @returns {string} - The cleaned up error message.
*/
static readableError(error) {
if (error.includes("port is already allocated"))
return "Port for this container is already allocated by another container. You should fully stop other containers using the same port before starting this one.";
if (error.includes('"message": "Access Denied"'))
return "You do not have permission to access this image. Make sure the tag is correct and you have authenticated with your NGC API key.";
return error;
}
/**
* Pauses or starts a container.
* @param {string} containerId - The ID of the container to toggle.
* @param {('pause'|'unpause'|'stop'|'start'|'remove')} action - The action to perform on the container.
* @returns {Promise<boolean>} - A promise that resolves to true if the container was toggled successfully, false otherwise.
*/
static async toggleContainer(containerId, action = "stop") {
const manager = new NimContainerManager();
if (!manager.client)
throw new Error("Container runtime client not available");
const container = manager.client.getContainer(containerId);
if (!container) throw new Error("Container not found");
const containerState = await container.inspect().then((info) => info.State);
if (action === "pause") {
// Already paused or not running - nothing to do
if (containerState.Paused || !containerState.Running) return true;
manager.#log("Pausing Running container", containerId);
await container.pause();
return true;
}
if (action === "stop") {
if (!containerState.Running) return true;
manager.#log("Stopping Running container", containerId);
await container.stop();
return true;
}
if (action === "start") {
if (containerState.Running) return true;
manager.#log("Starting Stopped container", containerId);
await container.start();
return true;
}
if (action === "remove") {
manager.#log("Removing container", containerId);
await container.remove({ force: true });
return true;
}
if (action === "unpause") {
if (containerState.Paused) {
manager.#log("Unpausing Paused container", containerId);
await container.unpause();
return true;
}
if (!containerState.Running) {
manager.#log(
"Container was paused but not running - starting it now",
containerId
);
await container.start();
return true;
}
}
throw new Error("Invalid action");
}
/**
* Makes a request to the podman API directly without using the Dockerode client
* There are some incompatibilities between the Dockerode client and the podman API
* and sometimes we need to make requests to the podman API directly to get around
* these issues. This method allows us to do both.
* @param {string} path - The path to request.
* @param {string} method - The method to use for the request.
* @param {any} body - The body to send with the request.
* @returns {Promise<any>} - A promise that resolves to the response from the request.
*/
async podmanRequest(path, method = "GET", body = null) {
return new Promise((resolve, reject) => {
const options = {
hostname: this.wslContainerService.ip,
port: this.wslContainerService.port,
path: `/v${this.wslContainerService.apiVersion}${path}`,
method,
headers: {
"Content-Type": "application/json",
},
};
const req = request(options, (res) => {
let data = "";
res.on("data", (chunk) => {
data += chunk;
});
res.on("end", () => {
try {
resolve(JSON.parse(data));
} catch (e) {
resolve(data);
}
});
});
req.on("error", reject);
if (body) {
req.write(JSON.stringify(body));
}
req.end();
});
}
/**
* Downloads the NVIDIA NIM Installer (or pulls from FS if already downloaded)
* TODO: should we download the installer or bundle it with the application?
* @returns {Promise<boolean>} - A promise that resolves to true if the installer was downloaded successfully, false otherwise.
*/
static async downloadInstaller() {
const installerPath =
process.env.NODE_ENV === "development"
? path.resolve(__dirname, "NIMSetup.exe")
: // In production, the installer is bundled in the `resources` folder, which contains
// this file in resources/backend/index.js.
path.resolve(__dirname, "NIMSetup.exe");
NimContainerManager.#slog(`Expecting NIM installer at ${installerPath}`);
if (!fs.existsSync(installerPath))
throw new Error("Could not locate NIM installer on this machine");
NimContainerManager.#slog(`Running NIM installer at ${installerPath}...`);
spawnSync(installerPath, {
stdio: "inherit",
shell: true,
windowsHide: true,
});
if (!NimContainerManager.getWSLWorkbenchContainer())
throw new Error(
"WSL container not found - the installer may have failed to run or complete."
);
NimContainerManager.#slog(
"Installer downloaded successfully, setting up the container manager..."
);
const manager = await NimContainerManager.setupContainerManager();
if (!manager) throw new Error("Failed to setup NIM container manager");
return true;
}
static toUnixPath(path) {
return path
.replace(/^([A-Za-z]):/, "/mnt/$1")
.toLowerCase()
.replace(/\\/g, "/");
}
/**
* Starts a new container for the given image tag as well as sets it as the active container for NVIDIA NIM
* @param {string} imageTag - The tag of the image to start the container for.
* @returns {Promise<boolean>} - A promise that resolves to true if the container was started successfully, false otherwise.
*/
static async startContainer(imageTag, ngcApiKey = null) {
const manager = new NimContainerManager();
if (!manager.client) {
NimContainerManager.#slog(
"Container runtime client not available - booting up WSL container service..."
);
NimContainerManager.bootWSLContainerService();
}
if (!manager.client)
throw new Error("Container runtime client not available");
const image = manager.client.getImage(imageTag);
if (!image) throw new Error("Image not found");
// Authenticate with NGC before starting the container - if this is the first time the container is being started
// some files need to be downloaded from the NGC registry which requires authentication - if the image already exists
// this step is skipped as it is assumed authentication was done previously
await manager.authenticateWithKey(ngcApiKey ?? manager.ngcApiKey);
const response = await manager.podmanRequest("/info", "GET");
if (!response.ID)
throw new Error("Container runtime could not be reached via HTTP");
if (process.env.NODE_ENV === "development" || !!process.env.NGC_PATCH)
NimContainerManager.#slog(
"Will apply NGC patch for container mount to bypass GPU utilization limitation."
);
const container = await manager.podmanRequest(
"/libpod/containers/create",
"POST",
{
image: imageTag,
terminal: true,
stdin: true,
env: {
NGC_API_KEY: manager.ngcApiKey,
LOCAL_NIM_CACHE: manager.localNimCache,
},
portmappings: [
{
container_port: 8000,
host_port: 8000,
host_ip: "0.0.0.0",
protocol: "tcp",
},
],
mounts: [
{
// The local nim cache is mounted as a bind mount to the container
// however, the path needs to be converted to a unix path for this to work.
Source: NimContainerManager.toUnixPath(manager.localNimCache),
Destination: "/opt/nim/.cache",
type: "bind",
},
...(process.env.NODE_ENV === "development" || !!process.env.NGC_PATCH
? [
{
Source: NimContainerManager.toUnixPath(
path.resolve(__dirname, "ngc_profile_patch.py")
),
Destination: "/opt/nim/llm/vllm_nvext/hub/ngc_profile.py",
type: "bind",
},
]
: []),
],
shm_size: 17179869184,
devices: [
{
path: "nvidia.com/gpu=all",
type: "gpu",
},
],
}
);
if (!container.Id)
throw new Error(`Container could not be created: ${container.message}`);
const instanceContainer = manager.client.getContainer(container.Id);
await instanceContainer.start();
manager.#log("Container started", container.Id);
const startedContainer = await manager.client
.listContainers({ all: true })
.then((containers) =>
containers.find((cont) => cont.Id === container.Id)
).catch((err) => {
console.error("Error listing containers:", err);
return null;
});
if (!startedContainer) return null;
const containerInfo = {
id: startedContainer.Id,
name: startedContainer.Names[0].replace("/", ""),
status: startedContainer.State,
statusText: startedContainer.Status,
baseUrl: `http://${manager.wslContainerService.ip}:${startedContainer.Ports.find((port) => port.Type === "tcp")?.PublicPort ?? 8000}/v1`,
};
manager.#log("Container started", containerInfo);
return containerInfo;
}
/**
* Pulls an image from the NGC registry
* @param {string} imageTag - The tag of the image to pull - may include nvcr.io/nim prefix
* @param {string} ngcApiKey - The NGC API key to use for authentication.
* @returns {Promise<boolean>} - A promise that resolves to true if the image was pulled successfully, false otherwise.
*/
static async pullImage(imageTag, ngcApiKey = null) {
const manager = new NimContainerManager();
if (!manager.client) {
NimContainerManager.#slog(
"Container runtime client not available - booting up WSL container service..."
);
NimContainerManager.bootWSLContainerService();
}
if (!manager.client)
throw new Error("Failed to boot WSL container service");
await manager.authenticateWithKey(ngcApiKey ?? manager.ngcApiKey);
let fullTag = imageTag.startsWith("nvcr.io/nim")
? imageTag
: `nvcr.io/nim/${imageTag}`;
if (!fullTag.endsWith(":latest")) fullTag = `${fullTag}:latest`;
this.#slog(`Pulling image ${fullTag} from NGC registry...`);
manager.pullImageEmitter.emit(
"start",
`Pulling image ${fullTag} from NGC registry...`
);
new Promise((resolve) => {
manager.client.pull(
fullTag,
{
authconfig: {
username: "$oauthtoken",
password: manager.ngcApiKey,
serveraddress: "nvcr.io",
},
},
function (err, stream) {
if (err) return resolve({ error: err.message });
stream.on("data", (chunk) => {
let data = {}
try {
data = JSON.parse(chunk.toString());
} catch (err) {
data = {};
}
let msg = data.Status ?? "";
if (data.progress) msg += ` ${data.progress}`;
manager.pullImageEmitter.emit("data", msg);
});
stream.on("end", () => {
manager.pullImageEmitter.emit(
"data",
"Pulling image from NGC registry completed successfully!"
);
resolve({ error: null });
});
}
);
}).then(({ error }) => {
if (error) throw new Error(error);
}).catch((err) => {
// Handle or log the error here so it's not unhandled
console.error("Pull error:", err);
throw new Error(error);
});
return true;
}
/**
* Streams the pull image events to the response handler
* @param {import("express").Response} responseHandler - The response handler to stream logs to.
*/
static async streamPullImageEvents(responseHandler) {
const manager = new NimContainerManager();
if (!manager.pullImageEmitter)
throw new Error("Pull image emitter not available");
// First send HTML with auto-scroll script
responseHandler.write(`
<!DOCTYPE html>
<html>
<head>
<script>
setInterval(() => { window.scrollTo(0, document.body.scrollHeight); }, 100);
</script>
<style>
body {
font-family: monospace;
background-color: #121212;
color: #e0e0e0;
}
</style>
</head>
<body>
<pre>Beginning to stream pull image events...</pre>
`);
manager.pullImageEmitter.on("data", (data) =>
responseHandler.write(`<pre>${data}</pre>`)
);
responseHandler.on("close", () => {
manager.pullImageEmitter.removeAllListeners();
responseHandler.write(`</body></html>`);
responseHandler.destroy();
});
}
/**
* Streams the logs of a container to the response handler
* @param {string} containerId - The ID of the container to stream logs for.
* @param {import("express").Response} responseHandler - The response handler to stream logs to.
*/
static async streamContainerLogs(containerId, responseHandler = null) {
if (!responseHandler) throw new Error("No response handler provided");
const manager = new NimContainerManager();
if (!manager.client)
throw new Error("Container runtime client not available");
const container = manager.client.getContainer(containerId);
if (!container) throw new Error("Container not found");
const containerState = await container.inspect().then((info) => info.State);
if (!containerState.Running || containerState.Paused) {
responseHandler.write(`==============================================\n`);
responseHandler.write(` Container runtime is not running or paused\n`);
responseHandler.write(
` Showing last 100 logs and waiting for container to start\n`
);
responseHandler.write(
`==============================================\n\n`
);
const tailLogs = await container.logs({
follow: false,
stdout: true,
stderr: true,
tail: 100,
timestamps: true,
});
responseHandler.write(`${tailLogs.toString()}\n`);
responseHandler.write(`==== Waiting for container to start ====\n\n`);
}
let streamStarted = false;
const logStream = await container.logs({
follow: true,
stdout: true,
stderr: true,
since: Number(new Date(containerState.StartedAt)) / 1000,
timestamps: true,
});
logStream.on("data", (chunk) => {
if (!streamStarted) {
responseHandler.write(
`${new Date().toISOString()} [NimContainerManager] Log stream started for container ${containerId}\n\n`
);
streamStarted = true;
}
responseHandler.write(`${chunk.toString()}`);
});
logStream.on("error", (error) => {
console.error("Error streaming logs:", error);
responseHandler.write(`Error streaming logs: ${error.message}\n`);
responseHandler.end();
});
responseHandler.on("close", () => {
responseHandler.destroy();
});
}
/****************************************************
* Instance methods
****************************************************/
/**
* Updates the NGC API key for the NimContainerManager client
* @param {string} newKey - The new NGC API key to use.
*/
updateNimAPIKey(newKey) {
this.#log(
`Updating NGC API Key for NimContainerManager client. ${newKey.slice(0, 10)}...${newKey.slice(-5)}`
);
this.ngcApiKey = newKey;
process.env.NVIDIA_NIM_LLM_NGC_API_KEY = newKey;
}
/**
* Determines the configuration mode for NVIDIA NIM
* "managed" if a runtime is available and can be used
* "remote" if a runtime is not available and we must fallback to just attaching to a BaseURL
* @returns {"managed"|"remote"}
*/
get configMode() {
return !this.client ? "remote" : "managed";
}
/**
* Sets the environment variables for the container runtime client
* that are required for NVIDIA NIM to work or other functions
*/
setEnvs() {
this.setLocalNimCache();
this.detectNGCApiKey();
}
/**
* Authenticates with the NGC registry using the provided key
* Stores the key if it is valid and different from the one already stored
* @param {string} providedKey - The NGC API key to use for authentication.
* @returns {Promise<boolean>} - A promise that resolves to true if the authentication was successful, false otherwise.
*/
async authenticateWithKey(providedKey = this.ngcApiKey) {
if (!providedKey) throw new Error("No NGC API key provided");
providedKey = providedKey === true ? this.ngcApiKey : providedKey;
this.#log(
`Authenticating with NGC API key: ${providedKey.slice(0, 10)}...${providedKey.slice(-5)}`
);
const auth = await this.client.checkAuth({
serveraddress: "nvcr.io",
username: "$oauthtoken",
password: providedKey,
});
if (auth.Status !== "Login Succeeded")
throw new Error(
"Failed to authenticate with NGC with credentials provided or found."
);
if (this.ngcApiKey !== providedKey) {
this.#log(
`Authenticated with new NGC key - storing key for future use, ${providedKey.slice(0, 10)}...${providedKey.slice(-5)}`
);
this.ngcApiKey = providedKey;
process.env.NVIDIA_NIM_LLM_NGC_API_KEY = providedKey;
}
this.#log("Authenticated with nvcr.io registry successfull!");
return true;
}
/**
* Sets the local NIM cache directory
* - Detects if set already from system ENV via echo of env variable
* - Falls back to default location in user's home directory
* Will set the instance variable `localNimCache`
* Does not create the directory automatically.
* @returns {string}
*/
setLocalNimCache() {
if (this.localNimCache) return;
// Detect if set already from system ENV
let systemNimCache;
switch (process.platform) {
case "win32":
systemNimCache = execSync(
'powershell -command "echo $env:LOCAL_NIM_CACHE"'
)
.toString()
.trim();
break;
default:
systemNimCache = execSync("echo $LOCAL_NIM_CACHE").toString().trim();
}
if (systemNimCache) {
this.#log(`Using system NIM cache: ${systemNimCache}`);
this.localNimCache = systemNimCache;
return;
}
// Assume no system NIM cache found, use default location
this.#log("No system NIM cache found, using default location");
const homeDir = os.homedir();
this.localNimCache = path.join(homeDir, ".cache", "nim");
// Create the directory if it doesn't exist
if (!fs.existsSync(this.localNimCache)) {
fs.mkdirSync(this.localNimCache, { recursive: true });
this.#log(`Created local NIM cache directory at ${this.localNimCache}`);
}
return this.localNimCache;
}
/**
* Detects the NGC API key from the system ENV
* - Detects if set already from system ENV via echo of env variable
* Will set the instance variable `ngcApiKey`
* Does not throw an error if no NGC API key is found, but logs a message
* @returns {string|null}
*/
detectNGCApiKey() {
if (this.ngcApiKey) return;
if (!!process.env.NVIDIA_NIM_LLM_NGC_API_KEY) {
this.#log(
`Using stored NGC API key: ${process.env.NVIDIA_NIM_LLM_NGC_API_KEY.slice(0, 10)}...${process.env.NVIDIA_NIM_LLM_NGC_API_KEY.slice(-5)}`
);
this.ngcApiKey = process.env.NVIDIA_NIM_LLM_NGC_API_KEY;
return;
}
// Detect if set already from system ENV
let systemNgcApiKey;
switch (process.platform) {
case "win32":
systemNgcApiKey = execSync(
'powershell -command "echo $env:NGC_API_KEY"'
)
.toString()
.trim();
break;
default:
systemNgcApiKey = execSync("echo $NGC_API_KEY").toString().trim();
}
if (systemNgcApiKey) {
this.#log(
`Using system NGC API key: ${systemNgcApiKey.slice(0, 10)}...${systemNgcApiKey.slice(-5)}`
);
this.ngcApiKey = systemNgcApiKey;
return;
}
this.#log(
"No system NGC API key found. It must be set for NVIDIA NIM to work."
);
return;
}
/**
* Gets the user's image library as well as container information if available for any images
* @returns {Promise<{name: string, id: string, tag: string, container: {id: string, name: string, status: string, statusText: string, baseUrl: string}|null, size: number}[]>}
*/
static async userImageLibrary() {
const manager = new NimContainerManager();
if (!manager.client) {
NimContainerManager.#slog(
"Container runtime client not available - booting up WSL container service..."
);
NimContainerManager.bootWSLContainerService();
}
if (!manager.client)
throw new Error("Container runtime client not available");
try {
const containers = await manager.client.listContainers({ all: true });
const foundImages = (await manager.client.listImages())
.filter(
(image) =>
image.RepoTags[0].startsWith("nvcr.io/nim") &&
image.Labels["com.nvidia.nim.type"] === "llm"
)
.map((image) => {
let container = null;
// If there are multiple containers for the same image, use the running one
const imageContainers = containers.filter(
(container) => container.ImageID === image.Id
);
if (imageContainers.length > 1)
container = imageContainers.find(
(container) => container.State === "running"
);
// Otherwise, just use the first one
if (!container) container = imageContainers?.[0];
return {
id: image.Id,
name: image.Labels["com.nvidia.nim.model"],
tag: image.RepoTags[0],
container: container
? {
id: container.Id,
name: container.Names[0].replace("/", ""),
status: container.State,
statusText: container.Status,
baseUrl: `http://${manager.wslContainerService.ip}:${container.Ports.find((port) => port.Type === "tcp")?.PublicPort ?? 8000}/v1`,
}
: null,
size: image.Size,
};
});
return foundImages;
} catch (error) {
console.error(error);
return [];
}
}
/**
* Gets the current configuration for NVIDIA NIM
* @returns {Promise<{
* images: {id: string, name: string, tag: string, container: {id: string, name: string, status: string, baseUrl: string}|null, size: number}[],
* localNimCache: string,
* ngcApiKey: boolean,
* containerRuntimeIsAvailable: boolean - this determines if the WSL Workbench container exists (but does not check if it is running)
* }>}
*/
async getConfig() {
const defaultConfig = {
images: [],
localNimCache: this.localNimCache,
ngcApiKey: !!this.ngcApiKey,
canRunManaged: process.platform === "win32" && process.arch === "x64", // Only Windows x64 can run managed mode
needsToRunInstaller: false,
containerRuntimeIsAvailable: false,
};
// Check if the container runtime is available
const wslWorkbenchContainer =
NimContainerManager.getWSLWorkbenchContainer();
if (!!wslWorkbenchContainer && !!this.client) {
return {
...defaultConfig,
images: await this.userImageLibrary(),
needsToRunInstaller: false,
containerRuntimeIsAvailable: true,
};
}
return {
...defaultConfig,
needsToRunInstaller: !wslWorkbenchContainer,
containerRuntimeIsAvailable: !!wslWorkbenchContainer,
};
}
/**
* Instance log method
* @param {string} text
* @param {...any} args
*/
#log(text, ...args) {
console.log(`\x1b[35m[NimContainerManager]\x1b[0m ${text}`, ...args);
}
}
module.exports = { NimContainerManager };