Skip to content
Merged
45 changes: 43 additions & 2 deletions bin/lloom.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ import { loadRecipeById, loadRecipes, planRecipe } from '../src/recipes.mjs';
import { RuntimeManager } from '../src/runtime-manager.mjs';
import { runCommand } from '../src/process-control.mjs';
import { applyRuntimePolicyPlan, createRuntimePolicyPlan } from '../src/runtime-policy.mjs';
import {
applyRuntimePolicyConfig,
createRuntimePolicyConfigReport,
parseNumericMemoryFlags,
NUMERIC_MEMORY_FLAGS
} from '../src/runtime-policy-config.mjs';
import { createLloomServer } from '../src/server.mjs';
import { applySetup, createSetupPlan, syncClusterSetupMembers } from '../src/setup.mjs';
import { createSetupStatus } from '../src/setup-status.mjs';
Expand Down Expand Up @@ -110,7 +116,8 @@ const COMMAND_REGISTRY = [
{ name: 'backend-install', aliases: [], tier: 'advanced', needsInstalledConfig: false },
{ name: 'runtimes', aliases: ['runtime-status'], tier: 'advanced', needsInstalledConfig: true },
{ name: 'cluster', aliases: ['cluster-status'], tier: 'advanced', needsInstalledConfig: true },
{ name: 'runtime-plan', aliases: ['runtime-policy'], tier: 'advanced', needsInstalledConfig: true },
{ name: 'runtime-plan', aliases: [], tier: 'advanced', needsInstalledConfig: true },
{ name: 'runtime-policy', aliases: [], tier: 'advanced', needsInstalledConfig: true },
{ name: 'runtime-admit', aliases: [], tier: 'advanced', needsInstalledConfig: true },
{ name: 'runtime-start', aliases: [], tier: 'advanced', needsInstalledConfig: true },
{ name: 'runtime-warmup', aliases: [], tier: 'advanced', needsInstalledConfig: true },
Expand Down Expand Up @@ -205,6 +212,7 @@ Backends and runtimes:
lloom backend-install <backend-id> [--apply --yes] [--step step-id]
lloom runtimes [runtime-id|all]
lloom runtime-plan <runtime-id>
lloom runtime-policy [--max-memory-utilization f] [--reserve-memory-gb n] [--apply --yes]
lloom suspend <model-or-alias> [--apply --yes] [--drain-timeout-ms 300000]
lloom resume <model-or-alias> [--apply --yes]
lloom runtime-admit <runtime-id> [--apply --yes]
Expand Down Expand Up @@ -2632,6 +2640,16 @@ async function main() {
console.log(JSON.stringify(response, null, 2));
},
'runtime-plan': async ({ args, config, command: _command }) => {
if (args.some((arg) => NUMERIC_MEMORY_FLAGS.some((flag) => arg === flag || arg.startsWith(`${flag}=`)))) {
throw new Error(
'runtime-plan is read-only and does not accept numeric memory flags; use `lloom runtime-policy --max-memory-utilization <f> --reserve-memory-gb <n> --apply --yes`'
);
}
if (hasFlag(args, '--apply') || hasFlag(args, '--yes')) {
throw new Error(
'runtime-plan is read-only and does not accept --apply/--yes; use `lloom runtime-policy --apply --yes` to write memory-policy changes'
);
}
const requestedRuntimeId = positional(args)[1];
const livePlan = clusterConfigured(config)
? await gatewayRequest(
Expand Down Expand Up @@ -2850,7 +2868,30 @@ async function main() {
handlers['routing'] = handlers['route'];
handlers['runtime-status'] = handlers['runtimes'];
handlers['cluster-status'] = handlers['cluster'];
handlers['runtime-policy'] = handlers['runtime-plan'];
handlers['runtime-policy'] = async ({ args, config, command: _command }) => {
const flags = parseNumericMemoryFlags(args);
const apply = hasFlag(args, '--apply');
const yes = hasFlag(args, '--yes');
if (!flags) {
if (apply || yes) {
throw new Error('runtime-policy --apply/--yes requires --max-memory-utilization and/or --reserve-memory-gb');
}
// Compatibility: a bare `runtime-policy` with no numeric flags keeps the
// historical read-only alias behavior and delegates to the runtime plan.
return handlers['runtime-plan']({ args, config, command: 'runtime-plan' });
}
if (yes && !apply) {
throw new Error('runtime-policy --yes requires --apply');
}
if (!apply) {
console.log(JSON.stringify(createRuntimePolicyConfigReport(config, flags), null, 2));
return;
}
if (!yes) {
throw new Error('applying a memory-policy change requires both --apply and --yes');
}
console.log(JSON.stringify(await applyRuntimePolicyConfig(config, flags, { apply, yes }), null, 2));
};
handlers['voice-list'] = handlers['voices'];
handlers['install-voice'] = handlers['voice-install'];
handlers['voice-rm'] = handlers['voice-remove'];
Expand Down
29 changes: 29 additions & 0 deletions docs/runtime-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,35 @@ an alias, or the config root.
The legacy `evictable` field (top-level or `policy.evictable`) still normalizes
to `keepWarm: true` when set to `false`, unchanged from earlier releases.

## Explicit memory limits

Preview a numeric memory-policy change before applying it:

```bash
lloom runtime-policy --max-memory-utilization 0.90 --reserve-memory-gb 8
lloom runtime-policy --max-memory-utilization 0.90 --reserve-memory-gb 8 --apply --yes
```

These example values are not a model-specific capacity recommendation. The
utilization must be greater than zero and less than one; the reserve must be
positive. Either option can be supplied alone. The command reports the selected
before/after fields and any per-node overrides, without printing the config.

Utilization updates both predictive admission and the memory guard's utilization
limit. Reserve updates both admission's reserve and the guard's minimum available
memory. The existing guard mode is preserved, including an explicitly selected
`yolo` mode; this command does not turn enforcement on or off. Percentage and
absolute-reserve limits both apply, so the stricter limit can determine admission.

Changes affect the resolved local config file. Configure each managed member's
local policy as needed. Cluster node resource overrides are preserved and shown
in the preview; they can take precedence over global values. A global absolute
`runtimePolicy.memoryBudgetGb` must be resolved explicitly before using these
numeric options. Unrelated settings, routes and credentials are preserved.

`runtime-plan` remains read-only. Without numeric options, `runtime-policy`
retains its historical read-only runtime-plan behavior.

## Idle restoration

The preferred-restore pass starts after all owned, enabled keep-warm pins are
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"check": "npm run check:cluster && npm run check:js && npm run check:entity-stagger && npm run check:python && npm run check:shell && npm run check:spark-deploy && npm run check:workers",
"check:cluster": "node --check src/cluster.mjs && node --check test/cluster.test.mjs",
"check:entity-stagger": "node --check scripts/entity-stagger-benchmark.mjs && node --check test/entity-stagger-benchmark.test.mjs",
"check:js": "node --check bin/lloom.mjs && node --check bin/lloom-host.mjs && node --check src/backend-catalog.mjs && node --check src/benchmarks.mjs && node --check src/bootstrap.mjs && node --check src/client-integrations.mjs && node --check src/community-client.mjs && node --check src/community-site.mjs && node --check src/config.mjs && node --check src/dashboard.mjs && node --check src/doctor.mjs && node --check src/host-memory.mjs && node --check src/host-server.mjs && node --check src/init.mjs && node --check src/installer.mjs && node --check src/interchange.mjs && node --check src/machine-profile.mjs && node --check src/managed-environment.mjs && node --check src/model-acquisition.mjs && node --check src/model-intake.mjs && node --check src/model-removal.mjs && node --check src/onboarding.mjs && node --check src/recipe-index.mjs && node --check src/recipe-pack-export.mjs && node --check src/recipe-pack.mjs && node --check src/registry.mjs && node --check src/route-control.mjs && node --check src/recipes.mjs && node --check src/resource-fit.mjs && node --check src/runtime-manager.mjs && node --check src/runtime-policy.mjs && node --check src/server.mjs && node --check src/setup.mjs && node --check src/setup-status.mjs && node --check src/process-control.mjs && node --check src/security.mjs && node --check src/tts-catalog.mjs && node --check src/voice-profiles.mjs && node --check src/protocol/text.mjs && node --check src/protocol/reasoning-normalize.mjs && node --check src/protocol/reasoning-effort.mjs && node --check src/protocol/responses.mjs && node --check src/protocol/anthropic.mjs && node --check src/protocol/sse.mjs && node --check src/protocol/stream-anthropic.mjs && node --check src/protocol/stream-responses.mjs && node --check src/protocol/index.mjs && node --check scripts/apply-spark-route-catalog.mjs && node --check scripts/capture-live-metrics.mjs && node --check scripts/merge-live-metrics.mjs && node --check scripts/run-qualitative-chat-benchmark.mjs && node --check scripts/run-local-decision-benchmark.mjs && node --check scripts/check-community-deploy.mjs && node --check scripts/generate-client-configs.mjs && node --check scripts/check-interchange.mjs && node --check scripts/check-package.mjs && node --check scripts/release-lib.mjs && node --check scripts/build-release.mjs && node --check scripts/deploy-spark.mjs && node --check test/audio-generations.test.mjs && node --check test/community-host.test.mjs && node --check test/dashboard-status.test.mjs && node --check test/ds4fv-recipe.test.mjs && node --check test/dspark-hotfixes.test.mjs && node --check test/glm53-exl3-recipe.test.mjs && node --check test/host-memory.test.mjs && node --check test/metrics-persistence.test.mjs && node --check test/model-acquisition.test.mjs && node --check test/model-failover.test.mjs && node --check test/qwen38-vllm-recipe.test.mjs && node --check test/reasoning-normalize.test.mjs && node --check test/reasoning-effort.test.mjs && node --check test/resource-fit.test.mjs && node --check test/route-control.test.mjs && node --check test/spark-route-catalog.test.mjs && node --check test/runtime-watchdog.test.mjs && node --check test/tts-catalog.test.mjs && node --check test/voice-profiles.test.mjs && node --check test/chatterbox-recipe.test.mjs && node --check test/hear-recipe.test.mjs && node --check test/smoke.mjs && node --check src/runtime-supervisor.mjs && node --check test/runtime-supervisor.test.mjs && node --check test/comfyui-media.test.mjs && node --check src/runtime-residency.mjs && node --check src/dashboard-scene.mjs && node --check src/dashboard-presence.mjs && node --check src/dashboard-presence-client.mjs && node --check src/first-run.mjs && node --check src/first-run-page.mjs && node --check src/browser-setup.mjs && node --check src/runtime-preferences.mjs && node --check src/installation-jobs.mjs && node --check src/model-installation.mjs && node --check src/dashboard-installation.mjs && node --check src/runtime-memory-safety.mjs && node --check src/dashboard-memory.mjs && node --check src/runtime-memory-usage.mjs && node --check src/config-profiles.mjs && node --check test/config-profiles.test.mjs",
"check:js": "node --check bin/lloom.mjs && node --check bin/lloom-host.mjs && node --check src/backend-catalog.mjs && node --check src/benchmarks.mjs && node --check src/bootstrap.mjs && node --check src/client-integrations.mjs && node --check src/community-client.mjs && node --check src/community-site.mjs && node --check src/config.mjs && node --check src/dashboard.mjs && node --check src/doctor.mjs && node --check src/host-memory.mjs && node --check src/host-server.mjs && node --check src/init.mjs && node --check src/installer.mjs && node --check src/interchange.mjs && node --check src/machine-profile.mjs && node --check src/managed-environment.mjs && node --check src/model-acquisition.mjs && node --check src/model-intake.mjs && node --check src/model-removal.mjs && node --check src/onboarding.mjs && node --check src/recipe-index.mjs && node --check src/recipe-pack-export.mjs && node --check src/recipe-pack.mjs && node --check src/registry.mjs && node --check src/route-control.mjs && node --check src/recipes.mjs && node --check src/resource-fit.mjs && node --check src/runtime-manager.mjs && node --check src/runtime-policy.mjs && node --check src/server.mjs && node --check src/setup.mjs && node --check src/setup-status.mjs && node --check src/process-control.mjs && node --check src/security.mjs && node --check src/tts-catalog.mjs && node --check src/voice-profiles.mjs && node --check src/protocol/text.mjs && node --check src/protocol/reasoning-normalize.mjs && node --check src/protocol/reasoning-effort.mjs && node --check src/protocol/responses.mjs && node --check src/protocol/anthropic.mjs && node --check src/protocol/sse.mjs && node --check src/protocol/stream-anthropic.mjs && node --check src/protocol/stream-responses.mjs && node --check src/protocol/index.mjs && node --check scripts/apply-spark-route-catalog.mjs && node --check scripts/capture-live-metrics.mjs && node --check scripts/merge-live-metrics.mjs && node --check scripts/run-qualitative-chat-benchmark.mjs && node --check scripts/run-local-decision-benchmark.mjs && node --check scripts/check-community-deploy.mjs && node --check scripts/generate-client-configs.mjs && node --check scripts/check-interchange.mjs && node --check scripts/check-package.mjs && node --check scripts/release-lib.mjs && node --check scripts/build-release.mjs && node --check scripts/deploy-spark.mjs && node --check test/audio-generations.test.mjs && node --check test/community-host.test.mjs && node --check test/dashboard-status.test.mjs && node --check test/ds4fv-recipe.test.mjs && node --check test/dspark-hotfixes.test.mjs && node --check test/glm53-exl3-recipe.test.mjs && node --check test/host-memory.test.mjs && node --check test/metrics-persistence.test.mjs && node --check test/model-acquisition.test.mjs && node --check test/model-failover.test.mjs && node --check test/qwen38-vllm-recipe.test.mjs && node --check test/reasoning-normalize.test.mjs && node --check test/reasoning-effort.test.mjs && node --check test/resource-fit.test.mjs && node --check test/route-control.test.mjs && node --check test/spark-route-catalog.test.mjs && node --check test/runtime-watchdog.test.mjs && node --check test/tts-catalog.test.mjs && node --check test/voice-profiles.test.mjs && node --check test/chatterbox-recipe.test.mjs && node --check test/hear-recipe.test.mjs && node --check test/smoke.mjs && node --check src/runtime-supervisor.mjs && node --check test/runtime-supervisor.test.mjs && node --check test/comfyui-media.test.mjs && node --check src/runtime-residency.mjs && node --check src/dashboard-scene.mjs && node --check src/dashboard-presence.mjs && node --check src/dashboard-presence-client.mjs && node --check src/first-run.mjs && node --check src/first-run-page.mjs && node --check src/browser-setup.mjs && node --check src/runtime-preferences.mjs && node --check src/installation-jobs.mjs && node --check src/model-installation.mjs && node --check src/dashboard-installation.mjs && node --check src/runtime-memory-safety.mjs && node --check src/dashboard-memory.mjs && node --check src/runtime-memory-usage.mjs && node --check src/config-profiles.mjs && node --check test/config-profiles.test.mjs && node --check src/runtime-policy-config.mjs && node --check test/runtime-policy-config.test.mjs",
"check:python": "python3 -m py_compile backends/dspark-vllm/apply-patch-pack.py backends/dspark-vllm/packs/miaai-dsv4flash-d1b76251-defaults/patches/*.py backends/dspark-vllm/packs/miaai-ds4fv-f5665e8/patches/*.py backends/dspark-vllm/packs/miaai-ds4fv-f5665e8/patches/vision_exp/*.py backends/qwen38-vllm/apply-ple-fp8-patch.py backends/qwen38-sglang/*.py backends/glm53-exl3/*.py backends/mlx-audio/lloom_audio_server.py backends/hear/lloom_hear_server.py backends/chatterbox/lloom_chatterbox_server.py patches/apply_mtplx_longctx_fix.py backends/comfyui-media/bridge/*.py backends/comfyui-media/graphs/*.py backends/comfyui-media/build/*.py backends/comfyui-media/install.py backends/qwen-image-diffusers/*.py backends/hear/test/test_hear_security.py src/darwin-memory-usage.py",
"check:shell": "bash -n backends/dspark-vllm/entrypoint.sh backends/dspark-vllm/runtime/build-hardened-image.sh backends/dspark-vllm/packs/miaai-dsv4flash-d1b76251-defaults/patches/*.sh backends/dspark-vllm/packs/miaai-ds4fv-f5665e8/apply-runtime.sh backends/dspark-vllm/packs/miaai-ds4fv-f5665e8/patches/*.sh backends/qwen38-vllm/entrypoint.sh backends/qwen38-sglang/entrypoint.sh backends/glm53-exl3/entrypoint.sh backends/mlx-audio/install.sh backends/hear/install.sh backends/chatterbox/install.sh clients/examples/lloom-claude clients/examples/lloom-codex clients/examples/lloom-hermes clients/examples/lloom-opencode clients/examples/lloom-zero scripts/remote-install-spark.sh scripts/remote-stage-spark-worker.sh",
"check:spark-deploy": "node --check test/spark-deploy.test.mjs && node test/spark-deploy.test.mjs",
Expand All @@ -86,7 +86,7 @@
"test:comfyui": "python3 scripts/test-comfyui-media.py",
"test:hear": "python3 -m unittest discover -s backends/hear/test -v",
"test:ux": "node --test test/first-run.test.mjs test/runtime-preferences.test.mjs test/admin-browser-security.test.mjs test/installation-jobs.test.mjs",
"test:memory-safety": "node --test test/runtime-memory-safety.test.mjs"
"test:memory-safety": "node --test test/runtime-memory-safety.test.mjs test/runtime-policy-config.test.mjs"
},
"engines": {
"node": ">=22.19.0"
Expand Down
35 changes: 35 additions & 0 deletions src/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,41 @@ function validateConfig(config, sourcePath, env) {
}
}

// Top-level numeric memory-policy fields. These mirror the ranges used by the
// runtime admission and memory-safety layers so a hand-edited or CLI-written
// config fails loudly instead of silently falling through to defaults.
const topLevelMaxMemoryUtilization = config.runtimePolicy?.maxMemoryUtilization;
if (topLevelMaxMemoryUtilization != null) {
if (
typeof topLevelMaxMemoryUtilization !== 'number' ||
!Number.isFinite(topLevelMaxMemoryUtilization) ||
topLevelMaxMemoryUtilization <= 0 ||
topLevelMaxMemoryUtilization >= 1
) {
errors.push('runtimePolicy.maxMemoryUtilization must be a finite number greater than 0 and less than 1');
}
}
const topLevelReserveMemoryGb = config.runtimePolicy?.reserveMemoryGb;
if (topLevelReserveMemoryGb != null) {
if (
typeof topLevelReserveMemoryGb !== 'number' ||
!Number.isFinite(topLevelReserveMemoryGb) ||
topLevelReserveMemoryGb <= 0
) {
errors.push('runtimePolicy.reserveMemoryGb must be a positive finite number');
}
}
const topLevelMemoryBudgetGb = config.runtimePolicy?.memoryBudgetGb;
if (topLevelMemoryBudgetGb != null) {
if (
typeof topLevelMemoryBudgetGb !== 'number' ||
!Number.isFinite(topLevelMemoryBudgetGb) ||
topLevelMemoryBudgetGb <= 0
) {
errors.push('runtimePolicy.memoryBudgetGb must be a positive finite number');
}
}

const memorySafety = config.runtimePolicy?.memorySafety;
if (memorySafety != null) {
if (typeof memorySafety !== 'object' || Array.isArray(memorySafety)) {
Expand Down
Loading
Loading