badgr-cli 1.0.37 → 1.0.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/badgr.js +15 -1
- package/src/catalog.js +287 -0
- package/src/commands/run.js +23 -0
- package/src/commands/serve.js +100 -10
- package/src/commands/template.js +119 -0
- package/tests/serve-lifecycle.test.js +165 -0
- package/tests/template.test.js +549 -0
package/package.json
CHANGED
package/src/badgr.js
CHANGED
|
@@ -17,6 +17,7 @@ import { comfyuiCommand } from './commands/comfyui.js';
|
|
|
17
17
|
import { trainCommand } from './commands/train.js';
|
|
18
18
|
import { transcribeCommand } from './commands/transcribe.js';
|
|
19
19
|
import { embedCommand } from './commands/embed.js';
|
|
20
|
+
import { templateCommand } from './commands/template.js';
|
|
20
21
|
|
|
21
22
|
const HELP = `
|
|
22
23
|
${chalk.bold('badgr')} — run or serve GPU workloads from one command
|
|
@@ -38,6 +39,9 @@ ${chalk.bold('SHORTCUTS')} ${chalk.dim('(wrappers around run / serve for common
|
|
|
38
39
|
${chalk.cyan('badgr train <config.yaml>')} LoRA / fine-tuning job, stream logs
|
|
39
40
|
${chalk.cyan('badgr transcribe <audio>')} Whisper transcription, print transcript
|
|
40
41
|
${chalk.cyan('badgr embed <model> <input>')} Text embeddings, output JSONL
|
|
42
|
+
${chalk.cyan('badgr serve template <name>')} Launch an endpoint template (vllm, invokeai, comfyui, …)
|
|
43
|
+
${chalk.cyan('badgr run template <name>')} Launch a job template (axolotl, unsloth)
|
|
44
|
+
${chalk.cyan('badgr template list')} Browse all pre-built templates
|
|
41
45
|
|
|
42
46
|
${chalk.bold('EXAMPLES')}
|
|
43
47
|
${chalk.dim('# Verify the stack works end-to-end:')}
|
|
@@ -46,6 +50,12 @@ ${chalk.bold('EXAMPLES')}
|
|
|
46
50
|
${chalk.dim('# Serve a model (OpenAI-compatible):')}
|
|
47
51
|
badgr serve meta-llama/Llama-3.1-8B-Instruct --max-cost 10
|
|
48
52
|
|
|
53
|
+
${chalk.dim('# Serve a Hugging Face GGUF file via llama.cpp:')}
|
|
54
|
+
badgr serve --runtime llama.cpp \\
|
|
55
|
+
--hf-repo org/model-repo \\
|
|
56
|
+
--hf-file model.gguf \\
|
|
57
|
+
--max-cost 10
|
|
58
|
+
|
|
49
59
|
${chalk.dim('# Launch ComfyUI with a workflow:')}
|
|
50
60
|
badgr comfyui run workflow.json --max-cost 5
|
|
51
61
|
|
|
@@ -94,7 +104,10 @@ ${chalk.bold('badgr serve OPTIONS')}
|
|
|
94
104
|
--count <n> Number of GPUs (default: 1)
|
|
95
105
|
--region US|EU|AU Region preference
|
|
96
106
|
--max-price <$/hr> Hard spend cap per GPU-hour
|
|
97
|
-
--
|
|
107
|
+
--runtime llama.cpp Serve a HF GGUF file via llama.cpp instead of vLLM
|
|
108
|
+
--hf-repo <repo> Hugging Face repo (use with --runtime llama.cpp), e.g. org/model-repo
|
|
109
|
+
--hf-file <file> GGUF filename within that repo, e.g. model.gguf
|
|
110
|
+
--health-path <path> Readiness path to poll (auto-detected: comfyui → /system_stats, llama.cpp → /health)
|
|
98
111
|
--no-wait Skip endpoint health check
|
|
99
112
|
|
|
100
113
|
${chalk.bold('AFTER SERVING')}
|
|
@@ -133,6 +146,7 @@ async function main() {
|
|
|
133
146
|
case 'train': return trainCommand(config, rest, chalk);
|
|
134
147
|
case 'transcribe': return transcribeCommand(config, rest, chalk);
|
|
135
148
|
case 'embed': return embedCommand(config, rest, chalk);
|
|
149
|
+
case 'template': return templateCommand(config, rest, chalk);
|
|
136
150
|
// legacy aliases kept for compatibility
|
|
137
151
|
case 'up': return upCommand(config, rest, chalk);
|
|
138
152
|
case 'config': {
|
package/src/catalog.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template catalog — shared by template.js, serve.js, and run.js.
|
|
3
|
+
* No imports from other badgr commands to avoid circular dependencies.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const TEMPLATES = [
|
|
7
|
+
{
|
|
8
|
+
name: 'comfyui',
|
|
9
|
+
title: 'ComfyUI',
|
|
10
|
+
description: 'Stable Diffusion image generation with ComfyUI node editor',
|
|
11
|
+
type: 'endpoint',
|
|
12
|
+
image: 'yanwk/comfyui-boot:latest',
|
|
13
|
+
gpu: 'RTX_4090',
|
|
14
|
+
gpu_count: 1,
|
|
15
|
+
port: 8188,
|
|
16
|
+
health_path: '/system_stats',
|
|
17
|
+
min_vram_gb: 16,
|
|
18
|
+
env: {},
|
|
19
|
+
notes: [
|
|
20
|
+
'Access the ComfyUI UI at the returned endpoint URL.',
|
|
21
|
+
'Tip: use `badgr comfyui run workflow.json` to also queue a workflow at boot.',
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'axolotl',
|
|
26
|
+
title: 'Axolotl Fine-Tuning',
|
|
27
|
+
description: 'LLM LoRA/QLoRA fine-tuning with the Axolotl training framework',
|
|
28
|
+
type: 'job',
|
|
29
|
+
image: 'axolotlai/axolotl-cloud-uv:main-latest',
|
|
30
|
+
gpu: 'A100',
|
|
31
|
+
gpu_count: 1,
|
|
32
|
+
min_vram_gb: 40,
|
|
33
|
+
env: {
|
|
34
|
+
HF_TOKEN: '<your-hf-token>',
|
|
35
|
+
WANDB_API_KEY: '<optional>',
|
|
36
|
+
HF_HUB_CACHE: '/workspace/hf-cache',
|
|
37
|
+
},
|
|
38
|
+
notes: [
|
|
39
|
+
'Preferred: `badgr train config.yaml` auto-detects Axolotl configs.',
|
|
40
|
+
'Pass HF_TOKEN via --env or BADGR_HF_TOKEN env var.',
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'unsloth',
|
|
45
|
+
title: 'Unsloth Fine-Tuning',
|
|
46
|
+
description: 'Fast LoRA/QLoRA fine-tuning — 2x faster, 60% less VRAM than baseline',
|
|
47
|
+
type: 'job',
|
|
48
|
+
image: 'unslothai/unsloth:latest',
|
|
49
|
+
gpu: 'RTX_4090',
|
|
50
|
+
gpu_count: 1,
|
|
51
|
+
min_vram_gb: 16,
|
|
52
|
+
env: {
|
|
53
|
+
HF_TOKEN: '<your-hf-token>',
|
|
54
|
+
},
|
|
55
|
+
notes: [
|
|
56
|
+
'Use `badgr train config.yaml --framework unsloth` for config-driven runs.',
|
|
57
|
+
'Unsloth detects the GPU environment automatically.',
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'vllm',
|
|
62
|
+
title: 'vLLM OpenAI Server',
|
|
63
|
+
description: 'OpenAI-compatible LLM inference endpoint powered by vLLM',
|
|
64
|
+
type: 'endpoint',
|
|
65
|
+
image: 'vllm/vllm-openai:latest',
|
|
66
|
+
gpu: 'RTX_4090',
|
|
67
|
+
gpu_count: 1,
|
|
68
|
+
port: 8000,
|
|
69
|
+
health_path: '/v1/models',
|
|
70
|
+
min_vram_gb: 24,
|
|
71
|
+
env: {
|
|
72
|
+
MODEL: 'meta-llama/Llama-3.1-8B-Instruct',
|
|
73
|
+
HF_TOKEN: '<for-gated-models>',
|
|
74
|
+
MAX_MODEL_LEN: '8192',
|
|
75
|
+
TENSOR_PARALLEL_SIZE: '1',
|
|
76
|
+
},
|
|
77
|
+
command: [
|
|
78
|
+
'python', '-m', 'vllm.entrypoints.openai.api_server',
|
|
79
|
+
'--model', '${MODEL}',
|
|
80
|
+
'--host', '0.0.0.0',
|
|
81
|
+
'--port', '8000',
|
|
82
|
+
'--tensor-parallel-size', '${TENSOR_PARALLEL_SIZE}',
|
|
83
|
+
],
|
|
84
|
+
notes: [
|
|
85
|
+
'Override MODEL with any HuggingFace model ID via --env MODEL=<id>.',
|
|
86
|
+
'For multi-GPU: increase gpu_count and TENSOR_PARALLEL_SIZE to match.',
|
|
87
|
+
'HF_TOKEN required for gated models (Llama, Gemma, Mistral).',
|
|
88
|
+
'OpenAI client: OpenAI(base_url="<endpoint>/v1", api_key="<key>")',
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'llama-cpp',
|
|
93
|
+
title: 'llama.cpp Server',
|
|
94
|
+
description: 'Fast quantised (GGUF) model inference via llama.cpp HTTP server',
|
|
95
|
+
type: 'endpoint',
|
|
96
|
+
image: 'michaelmanleyx/llama-cpp:server-cuda',
|
|
97
|
+
gpu: 'RTX_4090',
|
|
98
|
+
gpu_count: 1,
|
|
99
|
+
port: 8080,
|
|
100
|
+
health_path: '/health',
|
|
101
|
+
min_vram_gb: 8,
|
|
102
|
+
env: {
|
|
103
|
+
LLAMA_ARG_HF_REPO: 'bartowski/Meta-Llama-3.1-8B-Instruct-GGUF',
|
|
104
|
+
LLAMA_ARG_HF_FILE: 'Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf',
|
|
105
|
+
},
|
|
106
|
+
notes: [
|
|
107
|
+
'Preferred: `badgr serve --runtime llama.cpp --hf-repo <repo> --hf-file <file>`.',
|
|
108
|
+
'Override LLAMA_ARG_HF_REPO and LLAMA_ARG_HF_FILE via --env.',
|
|
109
|
+
'Supports any GGUF from HuggingFace Hub.',
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: 'invokeai',
|
|
114
|
+
title: 'InvokeAI',
|
|
115
|
+
description: 'Professional Stable Diffusion image generation and editing with InvokeAI',
|
|
116
|
+
type: 'endpoint',
|
|
117
|
+
image: 'ghcr.io/invoke-ai/invokeai:latest',
|
|
118
|
+
gpu: 'RTX_4090',
|
|
119
|
+
gpu_count: 1,
|
|
120
|
+
port: 9090,
|
|
121
|
+
health_path: '/api/v1/app/version',
|
|
122
|
+
min_vram_gb: 16,
|
|
123
|
+
env: {
|
|
124
|
+
INVOKEAI_ROOT: '/workspace/invokeai',
|
|
125
|
+
PUBLIC_KEY: '<optional-ssh-public-key>',
|
|
126
|
+
},
|
|
127
|
+
notes: [
|
|
128
|
+
'First boot downloads models — allow 5–10 min before the UI is ready.',
|
|
129
|
+
'Set PUBLIC_KEY to inject an SSH key for SCP file transfers.',
|
|
130
|
+
'Models and outputs persist under INVOKEAI_ROOT.',
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: 'kohya-ss',
|
|
135
|
+
title: 'Kohya SS Training',
|
|
136
|
+
description: 'Stable Diffusion LoRA/DreamBooth/fine-tuning via Kohya SS web GUI',
|
|
137
|
+
type: 'endpoint',
|
|
138
|
+
image: 'bmaltais/kohya-ss-gui:latest',
|
|
139
|
+
gpu: 'RTX_4090',
|
|
140
|
+
gpu_count: 1,
|
|
141
|
+
port: 7860,
|
|
142
|
+
health_path: '/',
|
|
143
|
+
min_vram_gb: 8,
|
|
144
|
+
env: {
|
|
145
|
+
HUGGINGFACE_TOKEN: '<optional>',
|
|
146
|
+
},
|
|
147
|
+
notes: [
|
|
148
|
+
'Kohya SS GUI is available at the returned endpoint URL (port 7860).',
|
|
149
|
+
'For headless LoRA training, use `badgr train config.yaml` instead.',
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'text-gen-webui',
|
|
154
|
+
title: 'Text Generation WebUI',
|
|
155
|
+
description: 'Oobabooga text-generation-webui — feature-rich LLM front-end for many backends',
|
|
156
|
+
type: 'endpoint',
|
|
157
|
+
image: 'atinoda/text-generation-webui:default-nightly',
|
|
158
|
+
gpu: 'RTX_4090',
|
|
159
|
+
gpu_count: 1,
|
|
160
|
+
port: 7860,
|
|
161
|
+
health_path: '/',
|
|
162
|
+
min_vram_gb: 16,
|
|
163
|
+
env: {
|
|
164
|
+
HF_TOKEN: '<for-gated-models>',
|
|
165
|
+
},
|
|
166
|
+
notes: [
|
|
167
|
+
'Load models from the web UI or pre-set via command args inside the container.',
|
|
168
|
+
'Supports llama.cpp, ExLlamaV2, AutoGPTQ, transformers, and more.',
|
|
169
|
+
],
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: 'sglang',
|
|
173
|
+
title: 'SGLang Server',
|
|
174
|
+
description: 'High-throughput LLM serving optimised for structured generation (SGLang)',
|
|
175
|
+
type: 'endpoint',
|
|
176
|
+
image: 'lmsysorg/sglang:latest',
|
|
177
|
+
gpu: 'A100',
|
|
178
|
+
gpu_count: 1,
|
|
179
|
+
port: 30000,
|
|
180
|
+
health_path: '/health',
|
|
181
|
+
min_vram_gb: 40,
|
|
182
|
+
env: {
|
|
183
|
+
MODEL_PATH: 'meta-llama/Llama-3.1-8B-Instruct',
|
|
184
|
+
HF_TOKEN: '<for-gated-models>',
|
|
185
|
+
TP_SIZE: '1',
|
|
186
|
+
},
|
|
187
|
+
command: [
|
|
188
|
+
'python', '-m', 'sglang.launch_server',
|
|
189
|
+
'--model-path', '${MODEL_PATH}',
|
|
190
|
+
'--host', '0.0.0.0',
|
|
191
|
+
'--port', '30000',
|
|
192
|
+
'--tp', '${TP_SIZE}',
|
|
193
|
+
],
|
|
194
|
+
notes: [
|
|
195
|
+
'Optimised for structured output (JSON schema, regex) and RadixAttention.',
|
|
196
|
+
'OpenAI-compatible API on port 30000.',
|
|
197
|
+
'Increase TP_SIZE and gpu_count together for tensor parallelism.',
|
|
198
|
+
'HF_TOKEN required for gated models.',
|
|
199
|
+
],
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
name: 'tgi',
|
|
203
|
+
title: 'Text Generation Inference (TGI)',
|
|
204
|
+
description: 'Hugging Face TGI — production-grade LLM inference with continuous batching',
|
|
205
|
+
type: 'endpoint',
|
|
206
|
+
image: 'ghcr.io/huggingface/text-generation-inference:latest',
|
|
207
|
+
gpu: 'RTX_4090',
|
|
208
|
+
gpu_count: 1,
|
|
209
|
+
port: 80,
|
|
210
|
+
health_path: '/health',
|
|
211
|
+
min_vram_gb: 24,
|
|
212
|
+
env: {
|
|
213
|
+
MODEL_ID: 'meta-llama/Llama-3.1-8B-Instruct',
|
|
214
|
+
HF_TOKEN: '<for-gated-models>',
|
|
215
|
+
MAX_INPUT_LENGTH: '4096',
|
|
216
|
+
MAX_TOTAL_TOKENS: '8192',
|
|
217
|
+
NUM_SHARD: '1',
|
|
218
|
+
},
|
|
219
|
+
notes: [
|
|
220
|
+
'OpenAI-compatible API at /v1/chat/completions.',
|
|
221
|
+
'Set NUM_SHARD equal to gpu_count for sharded (tensor-parallel) inference.',
|
|
222
|
+
'HF_TOKEN required for Llama, Gemma, and other gated models.',
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
export const TEMPLATE_MAP = Object.fromEntries(TEMPLATES.map(t => [t.name, t]));
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Build the args array passed to serveCommand / runCommand.
|
|
231
|
+
* Template defaults are applied first; CLI overrides win.
|
|
232
|
+
*/
|
|
233
|
+
export function buildTemplateFlags(template, overrides) {
|
|
234
|
+
const args = ['--image', template.image];
|
|
235
|
+
|
|
236
|
+
args.push('--gpu', overrides.gpu ?? template.gpu);
|
|
237
|
+
|
|
238
|
+
const count = overrides.count ?? template.gpu_count ?? 1;
|
|
239
|
+
if (count > 1) args.push('--count', String(count));
|
|
240
|
+
|
|
241
|
+
if (overrides.region) args.push('--region', overrides.region);
|
|
242
|
+
if (overrides.maxCost != null) args.push('--max-cost', String(overrides.maxCost));
|
|
243
|
+
if (overrides.maxPrice != null) args.push('--max-price', String(overrides.maxPrice));
|
|
244
|
+
if (overrides.tier) args.push('--tier', overrides.tier);
|
|
245
|
+
if (overrides.name) args.push('--name', overrides.name);
|
|
246
|
+
if (overrides.noWait) args.push('--no-wait');
|
|
247
|
+
if (overrides.persistent) args.push('--persistent');
|
|
248
|
+
|
|
249
|
+
if (template.type === 'endpoint' && template.health_path) {
|
|
250
|
+
args.push('--health-path', template.health_path);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Merge env: template defaults first, user overrides win
|
|
254
|
+
const mergedEnv = { ...(template.env || {}), ...(overrides.env || {}) };
|
|
255
|
+
for (const [k, v] of Object.entries(mergedEnv)) {
|
|
256
|
+
if (!v.startsWith('<')) args.push('--env', `${k}=${v}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return args;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Parse the flags that follow `badgr serve template <name>` or `badgr run template <name>`. */
|
|
263
|
+
export function parseTemplateOverrides(args) {
|
|
264
|
+
const overrides = { env: {} };
|
|
265
|
+
let i = 0;
|
|
266
|
+
while (i < args.length) {
|
|
267
|
+
const a = args[i];
|
|
268
|
+
if (a === '--gpu') { overrides.gpu = args[++i]; i++; continue; }
|
|
269
|
+
if (a === '--count') { overrides.count = parseInt(args[++i], 10); i++; continue; }
|
|
270
|
+
if (a === '--region') { overrides.region = args[++i]; i++; continue; }
|
|
271
|
+
if (a === '--max-cost') { overrides.maxCost = parseFloat(args[++i]); i++; continue; }
|
|
272
|
+
if (a === '--max-price') { overrides.maxPrice = parseFloat(args[++i]); i++; continue; }
|
|
273
|
+
if (a === '--max-runtime') { overrides.maxRuntime = parseFloat(args[++i]); i++; continue; }
|
|
274
|
+
if (a === '--tier') { overrides.tier = args[++i]; i++; continue; }
|
|
275
|
+
if (a === '--name') { overrides.name = args[++i]; i++; continue; }
|
|
276
|
+
if (a === '--no-wait') { overrides.noWait = true; i++; continue; }
|
|
277
|
+
if (a === '--persistent') { overrides.persistent = true; i++; continue; }
|
|
278
|
+
if (a === '--env') {
|
|
279
|
+
const kv = args[++i]; i++;
|
|
280
|
+
const idx = kv.indexOf('=');
|
|
281
|
+
if (idx > 0) overrides.env[kv.slice(0, idx)] = kv.slice(idx + 1);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
i++;
|
|
285
|
+
}
|
|
286
|
+
return overrides;
|
|
287
|
+
}
|
package/src/commands/run.js
CHANGED
|
@@ -3,6 +3,7 @@ import { callApi, terminateDeployment } from '../api.js';
|
|
|
3
3
|
import { addReceipt, updateReceipt, generateReceiptId } from '../store.js';
|
|
4
4
|
import { normalizeTier, callWithFallback, HIGH_RATE_THRESHOLD } from '../fallback.js';
|
|
5
5
|
import { formatCliError } from '../errors.js';
|
|
6
|
+
import { TEMPLATE_MAP, buildTemplateFlags, parseTemplateOverrides } from '../catalog.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* badgr run python train.py # gpu=auto, attached
|
|
@@ -334,6 +335,28 @@ const _KNOWN_RUN_FLAGS = new Set([
|
|
|
334
335
|
]);
|
|
335
336
|
|
|
336
337
|
export async function runCommand(config, args, chalk) {
|
|
338
|
+
// `badgr run template <name> [flags]` — expand template defaults then re-dispatch
|
|
339
|
+
if (args[0] === 'template') {
|
|
340
|
+
const name = args[1];
|
|
341
|
+
const t = name && TEMPLATE_MAP[name];
|
|
342
|
+
if (!t) {
|
|
343
|
+
console.error(chalk.red(`\n Unknown template: ${name || '(none)'}\n`));
|
|
344
|
+
console.error(chalk.dim(' Run `badgr template list` to see available templates.'));
|
|
345
|
+
process.exitCode = 1;
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (t.type !== 'job') {
|
|
349
|
+
console.error(chalk.red(`\n "${name}" is an endpoint template — use: badgr serve template ${name}\n`));
|
|
350
|
+
process.exitCode = 1;
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const overrides = parseTemplateOverrides(args.slice(2));
|
|
354
|
+
const expandedArgs = buildTemplateFlags(t, overrides);
|
|
355
|
+
if (overrides.maxRuntime != null) expandedArgs.push('--max-runtime', String(overrides.maxRuntime));
|
|
356
|
+
console.log(chalk.dim(` Template: ${t.title} → badgr run ${expandedArgs.join(' ')}\n`));
|
|
357
|
+
return runCommand(config, expandedArgs, chalk);
|
|
358
|
+
}
|
|
359
|
+
|
|
337
360
|
const { flags, positional } = parseRunArgs(args);
|
|
338
361
|
|
|
339
362
|
// Detect flags that ended up in the command because of broken shell line continuation
|
package/src/commands/serve.js
CHANGED
|
@@ -3,12 +3,16 @@ import { callApi, listDeployments } from '../api.js';
|
|
|
3
3
|
import { addDeployment, addReceipt, updateReceipt, generateReceiptId } from '../store.js';
|
|
4
4
|
import { normalizeTier, callWithFallback, HIGH_RATE_THRESHOLD } from '../fallback.js';
|
|
5
5
|
import { formatCliError } from '../errors.js';
|
|
6
|
+
import { TEMPLATE_MAP, buildTemplateFlags, parseTemplateOverrides } from '../catalog.js';
|
|
7
|
+
|
|
8
|
+
const LLAMA_CPP_IMAGE = 'michaelmanleyx/llama-cpp:server-cuda';
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
11
|
* badgr serve meta-llama/Llama-3.1-8B-Instruct
|
|
9
12
|
* badgr serve meta-llama/Llama-3.1-8B-Instruct --gpu L40S
|
|
10
13
|
* badgr serve BAAI/bge-large-en-v1.5 --task embed
|
|
11
14
|
* badgr serve --image ghcr.io/my-org/diffusers-api:latest --gpu L40S --env MODEL_ID=flux
|
|
15
|
+
* badgr serve --runtime llama.cpp --hf-repo org/repo --hf-file model.gguf --max-cost 10
|
|
12
16
|
*
|
|
13
17
|
* GPU defaults to "AUTO" — backend infers from model size.
|
|
14
18
|
*/
|
|
@@ -35,6 +39,9 @@ export function parseServeArgs(args) {
|
|
|
35
39
|
args[i] === '--no-expanded-search') { flags.noMarketplaceFallback = true; i++; continue; }
|
|
36
40
|
if (args[i] === '--persistent') { flags.persistent = true; i++; continue; }
|
|
37
41
|
if (args[i] === '--yes' || args[i] === '-y') { flags.yes = true; i++; continue; }
|
|
42
|
+
if (args[i] === '--runtime') { flags.runtime = args[++i]; i++; continue; }
|
|
43
|
+
if (args[i] === '--hf-repo') { flags.hfRepo = args[++i]; i++; continue; }
|
|
44
|
+
if (args[i] === '--hf-file') { flags.hfFile = args[++i]; i++; continue; }
|
|
38
45
|
if (args[i] === '--env') {
|
|
39
46
|
const kv = args[++i]; i++;
|
|
40
47
|
if (!flags.env) flags.env = [];
|
|
@@ -73,12 +80,34 @@ function _inferServeProfile(modelName) {
|
|
|
73
80
|
return { label: 'inference (70B+ model)', vram: '80+ GB', gpus: ['H100', 'A100'] };
|
|
74
81
|
}
|
|
75
82
|
|
|
83
|
+
// Mirror of backend workload_profile.py infer_profile_from_gguf.
|
|
84
|
+
// Accepts the --hf-file filename; looks for param-count hints like "35B" or "8x7B".
|
|
85
|
+
function _inferGgufProfile(ggufPath) {
|
|
86
|
+
const s = ggufPath.toLowerCase();
|
|
87
|
+
const moe = s.match(/(\d+)x(\d+)b/);
|
|
88
|
+
let paramsB;
|
|
89
|
+
if (moe) {
|
|
90
|
+
paramsB = parseInt(moe[1]) * parseInt(moe[2]);
|
|
91
|
+
} else {
|
|
92
|
+
const m = s.match(/(\d+)b/);
|
|
93
|
+
paramsB = m ? parseInt(m[1]) : null;
|
|
94
|
+
}
|
|
95
|
+
if (paramsB === null || paramsB <= 9) return { label: 'GGUF inference (≤9B, llama.cpp)', vram: '8+ GB', gpus: ['RTX 4090', 'RTX 3090', 'A6000'] };
|
|
96
|
+
if (paramsB <= 35) return { label: 'GGUF inference (10B–35B, llama.cpp)', vram: '24+ GB', gpus: ['RTX 4090', 'A6000', 'L40S'] };
|
|
97
|
+
return { label: 'GGUF inference (36B+, llama.cpp)', vram: '48+ GB', gpus: ['A6000', 'L40S', 'A100'] };
|
|
98
|
+
}
|
|
99
|
+
|
|
76
100
|
function _serveStageLabel(elapsedSec, healthPath = '/models') {
|
|
77
101
|
if (healthPath === '/models') {
|
|
78
102
|
if (elapsedSec < 45) return 'Starting vLLM…';
|
|
79
103
|
if (elapsedSec < 150) return 'Downloading model…';
|
|
80
104
|
return 'Waiting for /v1/models…';
|
|
81
105
|
}
|
|
106
|
+
if (healthPath === '/health') {
|
|
107
|
+
if (elapsedSec < 60) return 'Starting llama-server…';
|
|
108
|
+
if (elapsedSec < 180) return 'Downloading from Hugging Face…';
|
|
109
|
+
return 'Waiting for /health…';
|
|
110
|
+
}
|
|
82
111
|
if (elapsedSec < 30) return 'Starting container…';
|
|
83
112
|
if (elapsedSec < 120) return 'Container starting…';
|
|
84
113
|
return `Waiting for ${healthPath}…`;
|
|
@@ -86,7 +115,9 @@ function _serveStageLabel(elapsedSec, healthPath = '/models') {
|
|
|
86
115
|
|
|
87
116
|
function _detectHealthPath(image) {
|
|
88
117
|
if (!image) return null;
|
|
89
|
-
|
|
118
|
+
if (image.toLowerCase().includes('comfyui')) return '/system_stats';
|
|
119
|
+
if (image.toLowerCase().includes('llama.cpp')) return '/health';
|
|
120
|
+
return null;
|
|
90
121
|
}
|
|
91
122
|
|
|
92
123
|
/**
|
|
@@ -161,12 +192,34 @@ const _KNOWN_SERVE_FLAGS = new Set([
|
|
|
161
192
|
'--gpu', '--image', '--task', '--count', '--region', '--tier', '--max-price',
|
|
162
193
|
'--name', '--no-wait', '--max-cost', '--health-path', '--check-nodes',
|
|
163
194
|
'--no-fallback', '--strict-capacity', '--no-expanded-search', '--env',
|
|
164
|
-
'--persistent', '--yes', '-y',
|
|
195
|
+
'--persistent', '--yes', '-y', '--runtime', '--hf-repo', '--hf-file',
|
|
165
196
|
]);
|
|
166
197
|
|
|
167
198
|
export async function serveCommand(config, args, chalk) {
|
|
199
|
+
// `badgr serve template <name> [flags]` — expand template defaults then re-dispatch
|
|
200
|
+
if (args[0] === 'template') {
|
|
201
|
+
const name = args[1];
|
|
202
|
+
const t = name && TEMPLATE_MAP[name];
|
|
203
|
+
if (!t) {
|
|
204
|
+
console.error(chalk.red(`\n Unknown template: ${name || '(none)'}\n`));
|
|
205
|
+
console.error(chalk.dim(' Run `badgr template list` to see available templates.'));
|
|
206
|
+
process.exitCode = 1;
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (t.type !== 'endpoint') {
|
|
210
|
+
console.error(chalk.red(`\n "${name}" is a job template — use: badgr run template ${name}\n`));
|
|
211
|
+
process.exitCode = 1;
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const overrides = parseTemplateOverrides(args.slice(2));
|
|
215
|
+
const expandedArgs = buildTemplateFlags(t, overrides);
|
|
216
|
+
console.log(chalk.dim(` Template: ${t.title} → badgr serve ${expandedArgs.join(' ')}\n`));
|
|
217
|
+
return serveCommand(config, expandedArgs, chalk);
|
|
218
|
+
}
|
|
219
|
+
|
|
168
220
|
const { model, flags } = parseServeArgs(args);
|
|
169
221
|
const customImage = flags.image || null;
|
|
222
|
+
const isLlamaCpp = flags.runtime === 'llama.cpp';
|
|
170
223
|
|
|
171
224
|
// Detect flags that ended up as positional args due to broken shell line continuation
|
|
172
225
|
// (e.g. `\ ` with a trailing space instead of `\<newline>`).
|
|
@@ -188,10 +241,22 @@ export async function serveCommand(config, args, chalk) {
|
|
|
188
241
|
return;
|
|
189
242
|
}
|
|
190
243
|
|
|
191
|
-
if (
|
|
244
|
+
if (isLlamaCpp && (!flags.hfRepo || !flags.hfFile)) {
|
|
245
|
+
console.error(chalk.red('\n ✗ --runtime llama.cpp requires --hf-repo and --hf-file\n'));
|
|
246
|
+
console.error(chalk.dim(' Example:'));
|
|
247
|
+
console.error(chalk.dim(' badgr serve --runtime llama.cpp \\'));
|
|
248
|
+
console.error(chalk.dim(' --hf-repo org/model-repo \\'));
|
|
249
|
+
console.error(chalk.dim(' --hf-file model.gguf \\'));
|
|
250
|
+
console.error(chalk.dim(' --max-cost 10\n'));
|
|
251
|
+
process.exitCode = 1;
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (!model && !customImage && !isLlamaCpp) {
|
|
192
256
|
console.error(chalk.red('Usage: badgr serve <model>'));
|
|
193
257
|
console.error(chalk.red(' badgr serve meta-llama/Llama-3.1-8B-Instruct'));
|
|
194
258
|
console.error(chalk.red(' badgr serve --image ghcr.io/my-org/api:latest --gpu L40S'));
|
|
259
|
+
console.error(chalk.red(' badgr serve --runtime llama.cpp --hf-repo org/repo --hf-file model.gguf'));
|
|
195
260
|
return;
|
|
196
261
|
}
|
|
197
262
|
|
|
@@ -233,7 +298,20 @@ export async function serveCommand(config, args, chalk) {
|
|
|
233
298
|
|
|
234
299
|
const effectiveTier = normalizeTier(flags.tier);
|
|
235
300
|
|
|
236
|
-
if (
|
|
301
|
+
if (isLlamaCpp) {
|
|
302
|
+
console.log(chalk.bold('\n⚡ Serving HF GGUF (llama.cpp)\n'));
|
|
303
|
+
console.log(` ${chalk.bold('HF Repo:')} ${flags.hfRepo}`);
|
|
304
|
+
console.log(` ${chalk.bold('HF File:')} ${flags.hfFile}`);
|
|
305
|
+
console.log(` ${chalk.bold('Runtime:')} llama.cpp`);
|
|
306
|
+
console.log(` ${chalk.bold('GPU:')} ${gpuLabel}`);
|
|
307
|
+
if (flags.env?.length) console.log(` ${chalk.bold('Env:')} ${flags.env.join(', ')}`);
|
|
308
|
+
if (gpu === 'AUTO') {
|
|
309
|
+
const prof = _inferGgufProfile(flags.hfFile);
|
|
310
|
+
console.log();
|
|
311
|
+
console.log(` ${chalk.bold('Estimated workload:')} ${prof.label}`);
|
|
312
|
+
console.log(` ${chalk.bold('Estimated minimum VRAM:')} ${prof.vram}`);
|
|
313
|
+
}
|
|
314
|
+
} else if (customImage) {
|
|
237
315
|
console.log(chalk.bold('\n⚡ Serving custom container\n'));
|
|
238
316
|
console.log(` ${chalk.bold('Image:')} ${customImage}`);
|
|
239
317
|
console.log(` ${chalk.bold('GPU:')} ${gpuLabel}`);
|
|
@@ -271,7 +349,9 @@ export async function serveCommand(config, args, chalk) {
|
|
|
271
349
|
d.workload_type === 'endpoint' &&
|
|
272
350
|
(
|
|
273
351
|
(model && d.model === model) ||
|
|
274
|
-
(customImage && d.image === customImage)
|
|
352
|
+
(customImage && d.image === customImage) ||
|
|
353
|
+
(isLlamaCpp && d.image === LLAMA_CPP_IMAGE &&
|
|
354
|
+
d.env?.LLAMA_ARG_HF_REPO === flags.hfRepo && d.env?.LLAMA_ARG_HF_FILE === flags.hfFile)
|
|
275
355
|
)
|
|
276
356
|
);
|
|
277
357
|
if (duplicate) {
|
|
@@ -297,9 +377,12 @@ export async function serveCommand(config, args, chalk) {
|
|
|
297
377
|
const effectiveRegion = flags.region ? flags.region.toUpperCase() : undefined;
|
|
298
378
|
|
|
299
379
|
function buildBody(gpuOverride, tierOverride) {
|
|
380
|
+
const effectiveEnv = isLlamaCpp
|
|
381
|
+
? { LLAMA_ARG_HF_REPO: flags.hfRepo, LLAMA_ARG_HF_FILE: flags.hfFile, ...envObj }
|
|
382
|
+
: envObj;
|
|
300
383
|
return {
|
|
301
384
|
...(model ? { model } : {}),
|
|
302
|
-
...(customImage ? { image: customImage } : {}),
|
|
385
|
+
...(isLlamaCpp ? { image: LLAMA_CPP_IMAGE } : customImage ? { image: customImage } : {}),
|
|
303
386
|
...(flags.task ? { task: flags.task } : {}),
|
|
304
387
|
gpu: gpuOverride || gpu,
|
|
305
388
|
gpu_count: flags.count || 1,
|
|
@@ -307,7 +390,7 @@ export async function serveCommand(config, args, chalk) {
|
|
|
307
390
|
max_price_per_hour: flags.maxPrice,
|
|
308
391
|
name: flags.name,
|
|
309
392
|
tier: tierOverride || effectiveTier,
|
|
310
|
-
...(Object.keys(
|
|
393
|
+
...(Object.keys(effectiveEnv).length > 0 ? { env: effectiveEnv } : {}),
|
|
311
394
|
...(flags.maxCost ? { max_cost_usd: flags.maxCost } : {}),
|
|
312
395
|
};
|
|
313
396
|
}
|
|
@@ -378,10 +461,12 @@ export async function serveCommand(config, args, chalk) {
|
|
|
378
461
|
}
|
|
379
462
|
|
|
380
463
|
// ── Determine health check path ───────────────────────────────────────────
|
|
381
|
-
// Priority: explicit --health-path >
|
|
464
|
+
// Priority: explicit --health-path > llama.cpp → /health > vLLM → /models > auto-detect custom image > null
|
|
382
465
|
let resolvedHealthPath;
|
|
383
466
|
if (flags.healthPath) {
|
|
384
467
|
resolvedHealthPath = flags.healthPath;
|
|
468
|
+
} else if (isLlamaCpp) {
|
|
469
|
+
resolvedHealthPath = '/health';
|
|
385
470
|
} else if (!customImage) {
|
|
386
471
|
resolvedHealthPath = '/models';
|
|
387
472
|
} else {
|
|
@@ -462,7 +547,11 @@ export async function serveCommand(config, args, chalk) {
|
|
|
462
547
|
const serveRate = dep.cost_per_hour || 0;
|
|
463
548
|
|
|
464
549
|
console.log(` ${chalk.bold('Base URL:')} ${chalk.cyan(endpointUrl)}`);
|
|
465
|
-
if (
|
|
550
|
+
if (isLlamaCpp) {
|
|
551
|
+
console.log(` ${chalk.bold('HF Repo:')} ${flags.hfRepo}`);
|
|
552
|
+
console.log(` ${chalk.bold('HF File:')} ${flags.hfFile}`);
|
|
553
|
+
}
|
|
554
|
+
else if (dep.model || model) console.log(` ${chalk.bold('Model:')} ${dep.model || model}`);
|
|
466
555
|
if (customImage) console.log(` ${chalk.bold('Image:')} ${customImage}`);
|
|
467
556
|
console.log(` ${chalk.bold('GPU:')} ${dep.gpu_type} × ${dep.gpu_count}`);
|
|
468
557
|
if (serveRate > 0) console.log(` ${chalk.bold('Rate:')} $${serveRate.toFixed(2)}/hr`);
|
|
@@ -480,10 +569,11 @@ export async function serveCommand(config, args, chalk) {
|
|
|
480
569
|
|
|
481
570
|
if (endpointReady && !customImage) {
|
|
482
571
|
const keySnip = config.apiKey?.slice(0, 4) || 'sk-...';
|
|
572
|
+
const sdkModel = isLlamaCpp ? 'default' : (dep.model || model);
|
|
483
573
|
console.log(` ${chalk.bold('Use with OpenAI SDK:')}`);
|
|
484
574
|
console.log(chalk.dim(` from openai import OpenAI`));
|
|
485
575
|
console.log(chalk.dim(` client = OpenAI(base_url="${endpointUrl}", api_key="${keySnip}...")`));
|
|
486
|
-
console.log(chalk.dim(` resp = client.chat.completions.create(model="${
|
|
576
|
+
console.log(chalk.dim(` resp = client.chat.completions.create(model="${sdkModel}", messages=[...])`));
|
|
487
577
|
console.log();
|
|
488
578
|
}
|
|
489
579
|
}
|