feat: an optional lane says it is optional, and what enabling it costs (4296)
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 2s
Build images / build-agent (push) Successful in 5s
CI / extension-version (push) Successful in 2s
CI / frontend-build (push) Successful in 23s
CI / backend-lint-and-test (push) Successful in 31s
Build images / build-web (push) Successful in 1m21s
Build images / smoke-web (push) Skipped
Build images / build-ml (push) Successful in 2m12s
Build images / promote (push) Skipped
CI / integration (push) Successful in 2m39s
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 2s
Build images / build-agent (push) Successful in 5s
CI / extension-version (push) Successful in 2s
CI / frontend-build (push) Successful in 23s
CI / backend-lint-and-test (push) Successful in 31s
Build images / build-web (push) Successful in 1m21s
Build images / smoke-web (push) Skipped
Build images / build-ml (push) Successful in 2m12s
Build images / promote (push) Skipped
CI / integration (push) Successful in 2m39s
Operator, 2026-09-22: "since the ml-worker is optional it should be shown as such in the UI and have a warning about what it does and that it pulls the models and what models and their projected size and ram requirements to run." The card previously said "a few GB, once" — a number sourced from nothing, which is exactly the hand-wave I had flagged in this step's own survey log as something that should be measured rather than asserted. ONE FACT CORRECTED WHILE WRITING THE COPY. I had named the lane "ML tagging". It downloads an EMBEDDER: google/siglip-so400m-patch14-384. WD14 tagging is the GPU agent's job — celery_app.py:5 still names both, but that has been stale since B3 (#1238), when the agent took over and this lane was left as the CPU embed fallback for stacks running no agent (see MLSettings.cpu_embed_enabled). Telling someone the lane "does tagging" would have been wrong in exactly the way this request exists to prevent. The facts are structured data on the lane, not prose in a component: ModelRequirement(repo, approx_download_bytes, approx_resident_bytes, measured). The API carries them; the card renders them. Numbers come from the system, wording from the UI. ML_BYTES_PER_SLOT IS NOW DERIVED from that requirement rather than stated separately. They have to be one number: the figure quoted to the operator before they enable the lane and the figure the cap enforces. Two copies could disagree, and the UI would promise a slot the cap then refuses. `measured=False` travels with the numbers and the card renders "about". They are estimates from the checkpoint's parameter count and dtype — ~877M params at fp32 is ~3.5GB of weights — not from a build. This decides whether someone's server survives, so it is labelled rather than rounded into something that reads like a fact. A test asserts the flag is false, to be flipped in the same commit that records a real measurement. The card now shows: an "optional" chip in the row itself (someone scanning the table should not have to enable a lane to learn it was never required), and before the switch, what the lane does, that you only need it if you are NOT running the GPU agent, the repo id, the download size, the per-slot RAM, and why the ceiling is what it is — including saying plainly when a box has too little memory to run it at all. Keyed on the lane's own `optional` flag, not on the name 'ml', so a second optional lane gets the same treatment without anyone remembering to add it. A test asserts no REQUIRED lane declares a model: if one ever needs a download, it stops being required. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
This commit is contained in:
@@ -43,7 +43,15 @@
|
||||
<tbody>
|
||||
<tr v-for="lane in lanesList" :key="lane.name">
|
||||
<td>
|
||||
<div>{{ lane.display_name }}</div>
|
||||
<div class="d-flex align-center" style="gap: 6px;">
|
||||
<span>{{ lane.display_name }}</span>
|
||||
<!-- Said in the row, not only in the warning below. Someone
|
||||
scanning the table to work out why a lane is off should
|
||||
not have to enable it to find out it was never required. -->
|
||||
<v-chip v-if="lane.optional" size="x-small" variant="tonal">
|
||||
optional
|
||||
</v-chip>
|
||||
</div>
|
||||
<!-- Not present is NOT zero slots — it is "nothing answered".
|
||||
Saying "stopped" here would be a verdict drawn from an
|
||||
unswept read, and the operator would go looking for a crash
|
||||
@@ -111,10 +119,42 @@
|
||||
</tbody>
|
||||
</v-table>
|
||||
|
||||
<p v-if="mlOff" class="fc-section__hint mt-3">
|
||||
ML tagging is off. Turning it on downloads the tagging model the first
|
||||
time it runs — a few GB, once.
|
||||
</p>
|
||||
<!-- What an optional, off lane will cost BEFORE it is switched on.
|
||||
Every number here comes from the lane payload, and `measured`
|
||||
travels with them — an estimate is labelled rather than rounded
|
||||
into something that reads like a fact. The previous version of this
|
||||
said "a few GB", which told the operator nothing they could plan
|
||||
with and was not sourced from anything. -->
|
||||
<v-alert
|
||||
v-for="lane in offOptionalLanes" :key="`advisory-${lane.name}`"
|
||||
type="info" variant="tonal" density="compact" class="mt-3"
|
||||
>
|
||||
<div class="font-weight-medium mb-1">
|
||||
{{ lane.display_name }} is optional and currently off
|
||||
</div>
|
||||
<p class="mb-2">
|
||||
It computes image embeddings on the CPU — what similarity search,
|
||||
duplicate grouping and tag suggestions are built on. You only need it
|
||||
if you are <em>not</em> running the GPU agent, which does the same
|
||||
work faster.
|
||||
</p>
|
||||
<p class="mb-1">Turning it on downloads, once:</p>
|
||||
<ul class="mb-2">
|
||||
<li v-for="m in lane.models" :key="m.repo">
|
||||
<code>{{ m.repo }}</code> —
|
||||
{{ approx(m.measured) }}{{ gb(m.download_bytes) }} to download,
|
||||
and about {{ gb(m.resident_bytes) }} of RAM for
|
||||
<strong>each</strong> slot while it runs.
|
||||
</li>
|
||||
</ul>
|
||||
<p class="mb-0 text-caption">
|
||||
Each slot loads its own copy, which is why this container caps the
|
||||
lane at {{ lane.ceiling }}.
|
||||
<template v-if="lane.ceiling === 0">
|
||||
It has too little memory to run this at all.
|
||||
</template>
|
||||
</p>
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
@@ -134,10 +174,25 @@ const busy = ref(null)
|
||||
const notice = ref(null)
|
||||
|
||||
const lanesList = computed(() => store.lanes?.lanes ?? [])
|
||||
const mlOff = computed(() =>
|
||||
lanesList.value.some((l) => l.name === 'ml' && !l.enabled),
|
||||
|
||||
// Optional lanes that are OFF — the only ones whose cost the operator has not
|
||||
// already accepted. Keyed on the lane's own `optional` flag rather than on the
|
||||
// name 'ml', so a second optional lane gets the same treatment without anyone
|
||||
// remembering to add it here.
|
||||
const offOptionalLanes = computed(() =>
|
||||
lanesList.value.filter((l) => l.optional && !l.enabled && l.models?.length),
|
||||
)
|
||||
|
||||
function gb(bytes) {
|
||||
return `${(bytes / 1024 ** 3).toFixed(1)} GB`
|
||||
}
|
||||
|
||||
// An unmeasured figure says so. It decides whether someone's server survives,
|
||||
// and presenting an estimate as a measurement is the failure this guards.
|
||||
function approx(measured) {
|
||||
return measured ? '' : 'about '
|
||||
}
|
||||
|
||||
async function apply(lane, fields) {
|
||||
busy.value = lane.name
|
||||
notice.value = null
|
||||
|
||||
Reference in New Issue
Block a user