Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ visionset token create --name NAME
visionset token list
visionset token revoke NAME [--yes]
visionset inference create NAME --type local|http --model ID --revision REV
[--device D] [--precision P] [--endpoint URL]
[--device cpu|cuda|cuda:N] [--precision fp16|fp32]
[--endpoint URL]
visionset inference list
visionset inference show|update|delete NAME_OR_ID
visionset inference download NAME_OR_ID
Expand Down
60 changes: 41 additions & 19 deletions docs/inference.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ with WorkspaceService.open("./road-signs") as workspace:
model_id="some/model",
model_revision="abc123",
device="cuda",
precision="fp16",
precision="fp16", # fp16 needs a cuda device; a cpu connection is fp32
)
for one in connections.list():
print(one.name, one.connection_type.value, one.setup_state.value)
Expand Down Expand Up @@ -149,12 +149,16 @@ misdescribes.
**A failure changes nothing.** The connection is marked ready as the last step, after every file is
present — so a download that dies partway leaves it exactly as it was, at `not_set_up`, with the
error on the job. There is no half-ready state to recover from because there is no moment at which
one could be written. Ask again; a partial cache is verified and resumed rather than restarted.
one could be written. Ask again: an interrupted transfer resumes from what it had, and each file
that arrives is checked against the size the hub published for it before it is put in place.

**Asking twice is refused, not repeated.** Once a connection is ready there is nothing left to
fetch, so `download_weights` stops being offered and the request is answered with
`INFERENCE_CONNECTION_NOT_DOWNLOADABLE`. An `http` connection is refused with the same code for the
other reason: its model runs elsewhere, so it has no weights of its own in any state.
**Asking again checks rather than repeats.** `download_weights` stays available once a connection
is `ready`, where the same call re-checks that the snapshot is still complete and fetches only what
is missing — the browser labels it **Verify weights** and puts it in the row's overflow menu. That
is worth doing on a machine where a disk filled or a cache was pruned; it is *completeness* and not
integrity, because a file already in the cache under this revision is found rather than re-read. An
`http` connection is refused with `INFERENCE_CONNECTION_NOT_DOWNLOADABLE` in any state, for the
other reason: its model runs elsewhere, so it has no weights of its own.

## Running on the CPU

Expand All @@ -163,9 +167,17 @@ with a warning in the log. It is a fallback rather than a preference — a works
workstation should still open on a laptop — but it is slower by a large factor, which is why it is
said out loud rather than silently done.

Half precision (`fp16`, `float16`, `half` — the spelling is yours) applies on CUDA only. On a CPU
it is not the conservative choice it looks like: `float16` arithmetic outside CUDA's autocast is
slower than the `float32` it was avoiding.
Half precision applies on CUDA only, and the kernel now says so rather than absorbing it: a `cpu`
connection asking for `fp16` is refused at creation. On a CPU it was never the conservative choice
it looks like — `float16` arithmetic outside CUDA's autocast is slower than the `float32` it was
avoiding — and a setting the adapters drop is one the row would otherwise go on displaying as
though it had an effect.

**Both fields are closed vocabularies.** `device` is `cpu`, `cuda`, or `cuda:N` for the second GPU
on a machine that has one; `precision` is `fp16` or `fp32`, and `float16`, `half`, `float32` and
`full` are accepted as spellings of those two. Anything else is refused with a sentence naming the
members. What this closes is a gap rather than a freedom: `gpu` used to be accepted and then
resolved onto the CPU, so the connection described a run that never happened.

## Suggesting a shape from a click

Expand Down Expand Up @@ -268,22 +280,31 @@ A workspace with none says so and offers one thing — **Add connection**. Creat
steps, because the two kinds share almost no fields: first where the model runs, then that kind's
form.

- **Local** opens pre-filled with the suggested model, `facebook/sam2-hiera-base-plus` at `main`,
a `cpu` device and `fp16` precision. Every one of those is a starting point you can type over.
Underneath the fields is what fetching that revision would cost — the size described above, read
while you are still deciding. If this machine has no `local-inference` extra the size cannot be
read, and the form says so, in the server's own words, with the install command. **It stays
usable**: creating a connection downloads nothing, so not knowing the size is information rather
than a barrier.
- **Local** opens on a curated model, a `cpu` device and `fp32` precision. The model field is a
grouped list — the SAM 2.1 ladder under *Interactive segmentation*, Grounding DINO under
*Text-prompt detection* — showing each entry's download size and a line on what it is for, and
each one is pinned to a revision this build was checked against. **Custom model…** is the last
entry and reveals the free model id and revision fields: the list guides, it does not restrict,
and any model this build has an adapter for remains typeable. Device and precision are lists too,
and the precision list follows the device, because half precision applies on CUDA only. Underneath
is what fetching that revision would cost — the size described above, read while you are still
deciding. If this machine has no `local-inference` extra the size cannot be read, and the form
says so, in the server's own words, with the install command. **It stays usable**: creating a
connection downloads nothing, so not knowing the size is information rather than a barrier.
- **HTTP** asks for the endpoint URL. There is no credential field; where a secret would live is
still open (`cf. #421`), and a field added ahead of that answer would be answering it.

Each row shows its name, its kind, `model @ revision`, and its status as a word — **Ready** or
**Not set up** — beside a colour, never as a colour alone. A local row that is not set up carries
**Download weights**, which launches the background job described above and reports its progress
in place; the row becomes **Ready** when the job finishes. A machine without the extra still shows
the control, and pressing it answers with the install command — a control that vanished would take
the remedy with it.
in place; the row becomes **Ready** when the job finishes, without a reload. A row that is already
ready carries **Verify weights** in its overflow menu instead — the same request, re-checking the
snapshot. A machine without the extra still shows the control, and pressing it answers with the
install command — a control that vanished would take the remedy with it.

A failed download leaves the row at **Not set up**, because weights arrive or they do not, and says
what happened in the job's own words with what to do about it. There is no separate retry button:
**Download weights** is the retry.

Editing does not offer to change the kind, because the kind is not editable. Deleting asks once
and says exactly what it destroys: *annotations keep their model provenance; only this
Expand All @@ -304,6 +325,7 @@ control.
visionset inference size some/model --revision abc123
visionset inference create local-detector \
--type local --model some/model --revision abc123 --device cuda --precision fp16
# --device takes cpu, cuda or cuda:N; --precision takes fp16 or fp32, and fp16 needs a cuda device
visionset inference list
visionset inference show local-detector --json
visionset inference update local-detector --revision def456
Expand Down
24 changes: 23 additions & 1 deletion frontend/ui-core/src/data/inferenceQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export interface SuggestionOut {

export type ConnectionType = components["schemas"]["ConnectionType"];
export type ConnectionSetupState = components["schemas"]["ConnectionSetupState"];
export type Precision = components["schemas"]["Precision"];
export type DownloadSizeOut = components["schemas"]["DownloadSizeOut"];

export const inferenceKeys = {
Expand Down Expand Up @@ -134,7 +135,7 @@ export interface ConnectionInput {
readonly modelId: string;
readonly modelRevision: string;
readonly device?: string | null;
readonly precision?: string | null;
readonly precision?: Precision | null;
readonly endpointUrl?: string | null;
}

Expand Down Expand Up @@ -209,6 +210,27 @@ export function useDownloadWeights() {
});
}

/**
* Re-read every connection, because something that changes one has finished.
*
* The mutations above invalidate the list themselves; this is for the change
* that does not arrive as a mutation's response. A weights download answers
* `202` and finishes later, in a background job — and when it finishes it has
* moved `setup_state` and, with it, what the row may be asked to do. Nothing
* re-reads the list at that moment unless somebody says so, which is why the row
* used to sit at `Not set up` until the page was reloaded.
*
* Lives here rather than beside the screen for this module's stated reason: the
* list, its key and its invalidation are one fact, and a second spelling of the
* key under a screen is how two callers come to disagree about what is stale.
*/
export function useRefreshConnections(): () => void {
const queries = useQueryClient();
return () => {
void queries.invalidateQueries({ queryKey: inferenceKeys.connections() });
};
}

/**
* What fetching that revision would cost, read before anybody agrees to it.
*
Expand Down
26 changes: 20 additions & 6 deletions frontend/ui-core/src/generated/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2593,8 +2593,7 @@ export interface components {
model_revision: string;
/** Name */
name: string;
/** Precision */
precision?: string | null;
precision?: components["schemas"]["Precision"] | null;
};
/**
* ConnectionOut
Expand Down Expand Up @@ -2624,8 +2623,7 @@ export interface components {
model_revision: string;
/** Name */
name: string;
/** Precision */
precision: string | null;
precision: components["schemas"]["Precision"] | null;
setup_state: components["schemas"]["ConnectionSetupState"];
/**
* Updated At
Expand Down Expand Up @@ -2692,8 +2690,7 @@ export interface components {
model_revision?: string | null;
/** Name */
name?: string | null;
/** Precision */
precision?: string | null;
precision?: components["schemas"]["Precision"] | null;
};
/**
* DatasetChangeOut
Expand Down Expand Up @@ -3144,6 +3141,23 @@ export interface components {
*/
type: "polyline";
};
/**
* Precision
* @description The numeric precision a local connection asks its weights to be loaded in.
*
* A closed vocabulary rather than the free text this field started as, on
* ``ConnectionType``'s test: the set is small, the kernel is what decides
* whether a member is usable on a given device, and it grows only by a
* deliberate kernel change — bf16 arriving later is exactly that change.
*
* Free text here was not neutrality but a gap. ``fp32x`` was accepted and then
* ignored; so was ``fp16`` beside ``cpu``, which the adapters silently drop
* (see :func:`precisions_for`). A field whose wrong values are absorbed rather
* than refused is a field that cannot tell somebody they are configuring a run
* that will not happen.
* @enum {string}
*/
Precision: "fp16" | "fp32";
/**
* ProgressCounts
* @description How many assets sit in each annotation state.
Expand Down
5 changes: 4 additions & 1 deletion frontend/ui-core/src/generated/checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,11 @@ export const checkConnectionSetupState: Check<Schemas["ConnectionSetupState"]> =
export const checkConnectionType: Check<Schemas["ConnectionType"]> =
/*#__PURE__*/ oneOf(["local", "http"] as const);

export const checkPrecision: Check<Schemas["Precision"]> =
/*#__PURE__*/ oneOf(["fp16", "fp32"] as const);

export const checkConnectionOut: Check<Schemas["ConnectionOut"]> =
/*#__PURE__*/ object({ "allowed_actions": [true, arrayOf(checkConnectionAction)], "connection_type": [true, checkConnectionType], "created_at": [true, isString], "device": [true, either([isString, isNull] as const)], "endpoint_url": [true, either([isString, isNull] as const)], "id": [true, isString], "model_id": [true, isString], "model_revision": [true, isString], "name": [true, isString], "precision": [true, either([isString, isNull] as const)], "setup_state": [true, checkConnectionSetupState], "updated_at": [true, isString] } as const);
/*#__PURE__*/ object({ "allowed_actions": [true, arrayOf(checkConnectionAction)], "connection_type": [true, checkConnectionType], "created_at": [true, isString], "device": [true, either([isString, isNull] as const)], "endpoint_url": [true, either([isString, isNull] as const)], "id": [true, isString], "model_id": [true, isString], "model_revision": [true, isString], "name": [true, isString], "precision": [true, either([checkPrecision, isNull] as const)], "setup_state": [true, checkConnectionSetupState], "updated_at": [true, isString] } as const);

export const checkConnectionPage: Check<Schemas["ConnectionPage"]> =
/*#__PURE__*/ object({ "items": [true, arrayOf(checkConnectionOut)], "total": [true, isInteger] } as const);
Expand Down
Loading
Loading