videoflow.deploy package
Submodules
videoflow.deploy.admission module
Composition admission: does the broker and payload store a flow is about to run on actually provide the delivery guarantees its channels ask for?
Every channel of a flow carries a messaging profile (live_latest for a
REALTIME flow, reliable_work for a BATCH one, unless the operator asks for
another with --require-profile), and every backend advertises what it can
honour (videoflow.backends.capabilities). The planner either admits the
composition or rejects it by name — it never downgrades a request to whatever
the backend happens to offer, which is how a batch job used to end up on an
evictable store with nobody told.
This module is the deploy-time half of that: it turns the broker and store a
flow will run on into capability records, parses the operator’s explicit
requests, and runs the planner. An auto-provisioned broker or store is judged by
its declared profile (jetstream_capabilities / redis_payload_capabilities
— the render is what the profile says it is); a bring-your-own --nats /
--blob-redis-url is read back live before anything is created
(jetstream_capabilities_observed / redis_payload_capabilities_observed:
the connection’s payload limit, the JetStream account’s storage allowance, the
run’s streams when they already exist; Redis’ persistence and eviction
settings), and whatever the probe could not read stays Unknown with the
reason — never assumed. A definite incompatibility is binding (RFC 0006,
accepted); an unobservable capability of a bring-your-own broker or store is a
warning unless the operator named a profile explicitly. Explicit requests also
travel to the provision Job and the workers as VF_PROFILE_REQUESTS_JSON,
emitted only when there are any (D8); there they bind again, against the
streams as provisioned (runtime.provision) and as bound (runtime.worker,
before open()), through messaging.topology.verify_channel_profiles.
- videoflow.deploy.admission.PROBE_TIMEOUT_SECONDS = 5.0
How long a deploy-time probe of a bring-your-own broker or store waits, connect included, before reporting what it could not observe. Short on purpose: it runs on the operator’s machine before anything is created, and an unreachable service is reported as
Unknown('unreachable')for the planner to rule on, not waited for.
- videoflow.deploy.admission.admit(requirements: FlowRequirements, messaging: MessagingCapabilities, payload: PayloadCapabilities | None, *, payload_refs_in_use: bool, enforce: bool, unknown_is_fatal: bool, where: str, runtime: RuntimeCapabilities | None = None, execution: ExecutionCapabilities | None = None) CompositionPlan | None[source]
Run the planner. A rejection that is not binding is printed as a warning and
Nonereturned — today’s behaviour, with the reason on record; a binding one propagates the planner’s error (exit 2 or 3).Two kinds of rejection, bound separately: a definite incompatibility (the declared store is evictable, the transport retains nothing) binds under
enforce; an unobservable capability (a bring-your-own broker or store whose probe could not read the setting — unreachable, refused, timed out) binds only underunknown_is_fatal: with the switch on, a broker that happens to be slow must not turn every deploy into a rejection, so it is a warning there; an explicit request is the one case where “unobserved” must not pass.- Arguments:
payload_refs_in_use: a payload store is configured, so envelopes over the inline threshold offload to it and its durability is part of the channel guarantee.
enforce: definite rejections are binding (explicit requests, or the RFC 0006 switch —
enforce_admission).unknown_is_fatal: unobservable capabilities are binding too (explicit requests only —
unknown_admission).where:
deploy/run-local, for the message.runtime: the runtime store’s read-back (
VF_RUNTIME_STORE_URL), whichrestart_safeanddurable_controlare admitted against; None when no store is configured.execution: what the engine advertises (fused groups, batching); None when the caller is not deploying through an engine.
- videoflow.deploy.admission.enforce_admission(explicit: Sequence[ProfileRequest]) bool[source]
Whether a definite rejection is binding: always, since RFC 0006 was accepted (kept for its callers’ symmetry).
- videoflow.deploy.admission.jetstream_capabilities(profile: BrokerProfile | None, unread: str = 'the broker configuration was not read back (bring-your-own --nats)') MessagingCapabilities[source]
What a JetStream broker offers, from its declared profile: retained, recoverable delivery always (INTEREST retention with redelivery is what the topology provisions); persistence and replication as the profile says. With no profile (a bring-your-own
--nats, or a Service the namespace already had with no profile record —unreadsays which) those two areUnknown— the broker was not read back, and unknown is not “yes”; the CLI reads a bring-your-own broker back withjetstream_capabilities_observedinstead, and the provision Job reads a reused one back in-cluster.
- videoflow.deploy.admission.jetstream_capabilities_observed(nats_url: str, timeout: float = 5.0, stream_names: Sequence[str] = (), fail_fast: bool = True) MessagingCapabilities[source]
What a bring-your-own JetStream broker offers, read back from the live server rather than declared:
max_payload_bytes: the connection’smax_payload(nats-py 2.15.0,nats/aio/client.py:1285, the server’s INFOmax_payload).persistent_storage: fromjs.account_info()(nats/js/manager.py:69->api.AccountInfo,nats/js/api.py:761):limits.max_storage(AccountLimits,api.py:720) is the account’s file-store allowance,-1unlimited and0none — a stream provisioned withstorageunset is a file stream by server default, so the allowance says whether this run’s streams will be file-backed before any exists. Once a stream of the run exists, its appliedconfig.storage(stream_info,manager.py:85->api.StreamInfo.config) is the answer instead. File storage is what the JetStream API can see; whether the directory behind it survives a pod loss is the deployment’s business (an auto-provisioned broker answers that through its declared profile).replication_factor: the smallestconfig.num_replicasover the run’s existing streams;Unknown('unread')while none exists, because the copies a stream keeps are decided when it is created (VF_STREAM_REPLICAS).
A server that answers without JetStream is reported as offering no retained, recoverable delivery (
_core_nats_only). Anything that could not be read isUnknownwith the reason —timeout,auth(credentials refused, or a permissions violation on the API subjects, which the server reports through the error callback while the request itself times out),unreachable,malformed— and never a guess.- Arguments:
timeout: overall bound, connect included.
stream_names: the run’s stream names (
run_stream_names), read when they exist.fail_fast: no reconnects — the operator’s machine. False lets the client retry a broker that is still starting (the in-cluster provision Job).
- videoflow.deploy.admission.local_dev_capabilities() tuple[MessagingCapabilities, PayloadCapabilities][source]
What
videoflow run-local’s docker dev containers offer (deploy.localinfra): a JetStream server without a volume and the RedisRedisProfile.dev()describes — an append-only file,noeviction— whichlocalinfrastarts with exactly those arguments.
- videoflow.deploy.admission.parse_profile_requests(values: Sequence[str] | None, specs: Sequence[NodeSpec]) List[ProfileRequest][source]
--require-profile CHANNEL=PROFILEentries as requests. A channel is the name of the node whose output it carries; the profile is one ofMESSAGING_PROFILES.- Raises:
ConfigError: a malformed entry, a channel no node publishes, an unknown profile, or the same channel named twice.
- videoflow.deploy.admission.redis_payload_capabilities(profile: RedisProfile | None, unread: str = 'the store configuration was not read back (bring-your-own --blob-redis-url)') PayloadCapabilities[source]
What a Redis payload store offers, from its declared profile: durable only with append-only persistence and
noeviction(either alone lets an accepted envelope outlive its bytes);persistent_storageonly on a claim (the dev profile’s emptyDir is durable across a container restart, gone with the pod). No profile (bring-your-own--blob-redis-url, or a reused Service with no profile record —unreadsays which):Unknownuntil read back, which the CLI does withredis_payload_capabilities_observedfor a bring-your-own store and the provision Job does in-cluster for a reused one.
- videoflow.deploy.admission.redis_payload_capabilities_observed(url: str, timeout: float = 5.0) PayloadCapabilities[source]
What a bring-your-own Redis payload store offers, read back live (
wire.redis_payload_store.redis_capabilities_observed:CONFIG GET appendonly / save / maxmemory-policy,INFO cluster,CLUSTER KEYSLOTof the obligation keys). Refused credentials areUnknown('auth'); the delegate reports every other failure with its reason.- Arguments:
url:
redis:///rediss://(redis.Redis.from_url, redis-py 8.0.1).timeout: socket connect and read timeout per command.
- videoflow.deploy.admission.requests_env(explicit: Sequence[ProfileRequest]) dict[str, str][source]
The worker environment entry for explicit requests — empty when there are none.
- videoflow.deploy.admission.requests_from_env(value: str | None) list[ProfileRequest][source]
The inverse of
requests_envon the worker side;[]when unset.
- videoflow.deploy.admission.requirements_for(flow_type: str, specs: Sequence[NodeSpec], explicit: Sequence[ProfileRequest] = (), declared: FlowRequirements | None = None) FlowRequirements[source]
The flow-type presets for every channel, with the operator’s explicit requests replacing theirs, plus what the nodes themselves declared (
deploy.compile.declared_requirements: sink guarantees, execution groups, batching contracts) when the caller has the compiled document.
- videoflow.deploy.admission.run_stream_names(flow_id: str, run_id: str, specs: Sequence[NodeSpec]) list[str][source]
The stream names a run’s nodes publish on, for a probe to read back when they already exist.
- videoflow.deploy.admission.unknown_admission(explicit: Sequence[ProfileRequest]) bool[source]
Whether an unobservable capability is binding: only an explicit request asks for that.
- videoflow.deploy.admission.verify_topology_shape(flow_type: str, flow_id: str, run_id: str, explicit: Sequence[ProfileRequest]) None[source]
Reject an explicit request the flow’s own topology cannot carry, before anything is built, provisioned or applied. Streams are shaped by the flow type (
topology.stream_config_for: REALTIME ⇒ limits / discard-old, BATCH ⇒ interest / discard-new), soreliable_workon a REALTIME channel orlive_lateston a BATCH one contradicts the stream that will exist whatever the broker can do — the same finding the provision Job and the workers report from the read-back, an hour earlier and with nothing to tear down.- Raises:
IncompatibleProfile: naming every such channel.
videoflow.deploy.allocation_dra module
Dynamic Resource Allocation (resource.k8s.io/v1) as an
AcceleratorAllocationBackend — render-only in this release (plan
decision D9).
What it does: turn a WorkloadRequest into the manifests and pod fragments
a DRA-scheduled worker needs — a ResourceClaimTemplate for independent
replicas (one claim per pod) or a shared ResourceClaim for an explicit
execution group, the pod’s spec.resourceClaims entry and the container’s
resources.claims reference, with the DeviceClass named, never created
(it is the driver’s and the cluster administrator’s object). And say, per
feature, whether the cluster and driver at hand can serve a request:
DraEnvironment is the version matrix — Kubernetes ≥ 1.34 for the v1
API, DRAPartitionableDevices for dynamic MIG (alpha and off by default on
1.34/1.35, beta and on from 1.36), DRAConsumableCapacity for shared
capacity (same schedule), MPS as a driver capability with no Kubernetes gate,
and the pairs no driver can honour at once (MPS with dynamic MIG, MPS with
consumable shares) — rejected by name before anything is rendered
(ALLOC-019/020/021/023/024).
What it does not do: read a ResourceSlice, create or watch a claim, or
release one. Those need a DRA driver running in the cluster (the NVIDIA DRA
driver), which the cluster this was built against does not have; every
lifecycle call raises CapabilityError naming that follow-up rather than
pretending. gpu.DraGpu (--gpu-mode dra) is the same rendering behind the CLI.
Field names follow the resource.k8s.io/v1 API reference (read at
implementation time, Kubernetes 1.36 docs): a request is exactly:
{deviceClassName, allocationMode: ExactCount | All, count, selectors[].cel,
capacity.requests}; a pod lists resourceClaims[].resourceClaimTemplateName
or resourceClaimName and a container resources.claims[].name.
- videoflow.deploy.allocation_dra.DRA_GA_VERSION = (1, 34)
The first Kubernetes release serving
resource.k8s.io/v1(DRA GA).
- class videoflow.deploy.allocation_dra.DraAllocationBackend(environment: DraEnvironment | None = None, device_class: str = 'gpu.nvidia.com')[source]
Bases:
AcceleratorAllocationBackend- Arguments:
environment: the
DraEnvironmentcapabilities are judged against;capabilities(mapping)may also carry one (DraEnvironment.from_mapping).device_class: the
DeviceClassevery rendered claim references.
- bindings(claim_id: str, workload_id: str) WorkloadBindings[source]
- capabilities(environment: Mapping[str, Any]) AllocationCapabilities[source]
- inventory(scope: Mapping[str, Any]) Known[InventorySnapshot] | Unknown[source]
- observe(claim_id: str) Known[ClaimObservation] | Unknown[source]
- plan(requests: Sequence[WorkloadRequest], snapshot: InventorySnapshot) FeasiblePlan | Infeasible[source]
- reconcile(claim_id: str, desired: str, expected_generation: str) ClaimObservation[source]
- release(claim_id: str, operation_id: str, expected_generation: str, keep_workloads: bool = False) ReleaseObservation[source]
- render(request: WorkloadRequest, namespace: str, shared_claim: str | None = None, capacity: Mapping[str, str] | None = None) WorkloadBindings[source]
- reserve(plan: FeasiblePlan, operation_id: str, expected_generation: str | None) ClaimObservation[source]
- class videoflow.deploy.allocation_dra.DraEnvironment(kubernetes_version: str, feature_gates: Mapping[str, bool]=<factory>, device_classes: tuple[str, ...]=(), driver: str | None = None, driver_features: frozenset[str] = frozenset({}))[source]
Bases:
objectThe cluster and driver a DRA request is judged against.
- Arguments:
kubernetes_version: the API server’s version string.
feature_gates: gate states known for certain (read from the API server flags, or declared by the operator); a gate not listed takes its version default.
device_classes: the
DeviceClassnames the cluster serves.driver: the DRA driver name (
gpu.nvidia.com), None when no driver publishes ResourceSlices.driver_features: the gated features the driver itself implements (
dynamic-mig,mps,consumable-capacity).
- property api_served: bool
- device_classes: tuple[str, ...] = ()
- driver: str | None = None
- driver_features: frozenset[str] = frozenset({})
- feature_gates: Mapping[str, bool]
- static from_mapping(environment: Mapping[str, Any]) DraEnvironment[source]
- gate_enabled(gate: str) bool | None[source]
True/False when known (declared, or the version default); None when the gate does not exist at this version.
- kubernetes_version: str
Gated feature → why this environment cannot serve it; a feature absent here is available.
- property version: tuple[int, int, int]
- videoflow.deploy.allocation_dra.FEATURE_GATES = {'consumable-capacity': 'DRAConsumableCapacity', 'dynamic-mig': 'DRAPartitionableDevices'}
Which Kubernetes gate each gated feature needs (MPS is purely a driver matter).
- videoflow.deploy.allocation_dra.GATE_SCHEDULE: dict[str, tuple[tuple[int, str, bool], ...]] = {'DRAAdminAccess': ((32, 'alpha', False), (34, 'beta', True), (36, 'stable', True)), 'DRAConsumableCapacity': ((34, 'alpha', False), (36, 'beta', True)), 'DRADeviceTaints': ((33, 'alpha', False), (36, 'beta', True), (37, 'stable', True)), 'DRAPartitionableDevices': ((33, 'alpha', False), (36, 'beta', True)), 'DRAPrioritizedList': ((33, 'alpha', False), (34, 'beta', True), (36, 'stable', True))}
Feature-gate schedule per the Kubernetes feature-gates reference (1.37 docs):
(first minor, stage, on by default)rows in ascending order; a gate is absent before its first row.stablegates are locked on.
- videoflow.deploy.allocation_dra.claim_name_for(workload_id: str) str[source]
The pod-local claim name (
spec.resourceClaims[].name) for a workload.
- videoflow.deploy.allocation_dra.device_request(request: WorkloadRequest, device_class: str, capacity: Mapping[str, str] | None = None) dict[source]
One
spec.devices.requests[]entry: anexactlyrequest fordevice_countdevices of the class, with the request’s hard constraints as CEL selectors on device attributes and any capacity requests. Plain dicts throughout — the shape is the Kubernetes API’s, not ours.
- videoflow.deploy.allocation_dra.gate_stage(gate: str, version: tuple[int, int, int]) tuple[str, bool] | None[source]
(stage, on by default)of a gate at a Kubernetes version, or None when the gate does not exist there.
- videoflow.deploy.allocation_dra.observe_environment(kubectl: str = 'kubectl') DraEnvironment[source]
The environment as the API server reports it, read-only: server version, the
DeviceClassnames served (empty when the API is absent), and the driver behind the first GPUResourceSlice(None when none exists). Gate states are not readable through the API and stay at version defaults; driver features are unknown to a read and stay empty — an operator who knows the driver declares them.
- videoflow.deploy.allocation_dra.parse_version(version: str) tuple[int, int, int][source]
v1.36.3+k3s1→(1, 36, 3); anything unparseable is(0, 0, 0).
- videoflow.deploy.allocation_dra.render_bindings(request: WorkloadRequest, device_class: str, namespace: str, shared_claim: str | None = None, capacity: Mapping[str, str] | None = None) WorkloadBindings[source]
The DRA manifests and fragments for one workload: a
ResourceClaimTemplate(every pod gets its own claim — independent replicas) unlessshared_claimnames aResourceClaimall pods of an execution group share. TheDeviceClassis referenced by name and never rendered.
videoflow.deploy.allocation_kubernetes module
The Kubernetes AcceleratorAllocationBackend: the videoflow GPU pool as the
contract sees it, over the two registered strategies.
exclusive— the device plugin is the authority. Videoflow reads the pool (GFD labels, allocatable, running pods) and plans a per-host packing, but it writes nothing: the scheduler accounts whole-device claims, soreserveis a plan admitted for rendering and readiness is the pod’s, not the claim’s.mix— videoflow itself becomes an authority over geometry (RFC 0004): it claims nodes by a server-enforced compare-and-swap on theirresourceVersion, re-reads the occupancy after claiming and before any geometry write, publishes thenvidia-mig-partedentries under CAS, and judges readiness by evidence correlated with this operation — the per-run entry name and owner epoch it wrote, the manager’ssuccessand the requested slices instatus.allocatable(ALLOC-003/011/033).
What the reads could not see stays unknown: a pod listing the API refused
makes the inventory partial and no plan may admit on it; an unreadable node
makes observe Unknown, never “ready”. Release is fenced on the claim’s
generation (the owner epoch) and keep_workloads=True retains ownership and
geometry for a later explicit release (ALLOC-013). The shared ConfigMap is
never deleted: the last flow out restores the operator’s pointer and leaves a
tombstone (decision D3).
The strategies keep their registry API for the CLI (resolve_specs →
preflight_problems → prepare → cleanup); this module is the same
machinery behind the contract, with the plan explicit (gpu.AllocationPlan).
- videoflow.deploy.allocation_kubernetes.CONSTRAINT_KEYS = {'kubernetes.io/hostname': <function <lambda>>, 'nvidia.com/gpu.memory': <function <lambda>>, 'nvidia.com/gpu.product': <function <lambda>>}
GFD attributes a hard constraint may name, and how the inventory answers them.
- class videoflow.deploy.allocation_kubernetes.KubernetesAllocationBackend(strategy_name: str = 'exclusive', kubectl: str = 'kubectl', clock: Callable[[], float]=<built-in function time>)[source]
Bases:
AcceleratorAllocationBackend- Arguments:
strategy_name: a registered
--gpu-mode(exclusiveormix).kubectl: the kubectl binary every read and write goes through.
- bindings(claim_id: str, workload_id: str) WorkloadBindings[source]
What a pod of
workload_idcarries: the extended-resource limit the strategy renders (a MIG profile name for a sharer), a hostname affinity to the planned node, and — undermix— the owner label the pool selector keys on.
- capabilities(environment: Mapping[str, Any]) AllocationCapabilities[source]
- inventory(scope: Mapping[str, Any]) Known[InventorySnapshot] | Unknown[source]
- property managed_mig: bool
- observe(claim_id: str) Known[ClaimObservation] | Unknown[source]
- plan(requests: Sequence[WorkloadRequest], snapshot: InventorySnapshot) FeasiblePlan | Infeasible[source]
Per-host packing for whole-device requests (a replica’s devices sit on one node) and, under
mix, the layout solver for MIG sharers. A partial snapshot plans nothing: occupancy that could not be read is not idle capacity.
- reconcile(claim_id: str, desired: str, expected_generation: str) ClaimObservation[source]
- release(claim_id: str, operation_id: str, expected_generation: str, keep_workloads: bool = False) ReleaseObservation[source]
mix: this flow’s geometry restored and its claims released throughMixGpu.cleanup(idempotent; a node that did not revert keeps its restore record for a retry, reported aspending_recovery). Withkeep_workloadsnothing is touched: ownership and geometry stay for a later explicit release. A stale generation releases nothing.
- reserve(plan: FeasiblePlan, operation_id: str, expected_generation: str | None) ClaimObservation[source]
exclusive: nothing to write — the plan is admitted and the scheduler accounts the claims.mix: the node claims (CAS), the occupancy re-read, the ConfigMap/ClusterPolicy publication and the geometry labels, throughMixGpu.apply_plan; the claim’s generation is the owner epoch stamped on its nodes.- Raises:
OwnershipConflict: the plan was made on another snapshot, a node was taken, or occupancy changed after the claim.
UnobservableState: occupancy could not be re-read after the claim.
ClusterError: the manager refused or never applied the geometry.
- videoflow.deploy.allocation_kubernetes.constraint_expressions(constraints: Sequence[Constraint]) list[dict][source]
The hard constraints as node-affinity
matchExpressions(soft ones are preferences, not terms).
- videoflow.deploy.allocation_kubernetes.constraint_holds(constraint: Constraint, node: NodeInventory) bool | None[source]
Whether
nodesatisfies a constraint on a verifiable attribute; None when the key is not one.
- videoflow.deploy.allocation_kubernetes.requests_as_specs(requests: Sequence[WorkloadRequest]) list[NodeSpec][source]
The layout solver speaks
NodeSpec: one single-replica spec per request, a MIG sharer carrying its minimum usable memory asgpu_memory_gib.
- videoflow.deploy.allocation_kubernetes.snapshot_from_inventory(inventory: Sequence[NodeInventory], observed_at: float, generation: str | None) InventorySnapshot[source]
The contract’s view of
cluster.gpu_inventory_observed: one identity per card (host ordinal, no UUID — the API does not expose one), occupancy as GPU units held by running pods, the node’s sharing classification and owner stamp, andpartialcompleteness whenever any node’s pod listing failed.
videoflow.deploy.allocation_local module
The local AcceleratorAllocationBackend: this host’s GPUs, read through
nvidia-smi, partitioned across the workers of a run-local flow.
Two policies, one contract (plan §C step 4, decision D4):
shared(the default until RFC 0006 is accepted) reproduces today’s wrap-around walk — when demand exceeds the visible devices, workers share them. What changes is the reporting: every worker receives aDeliveredGrant(VF_GPU_GRANT_JSON) that says how many devices it really got and that the grant is not exclusive, so a requestedgpu_countis never presented as delivered capacity (ALLOC-014, RUN-044).strictrefuses, before any worker is launched, a flow whose exclusive requests do not fit the host’s distinct devices, and admits cooperative sharers only within a declared peak-memory budget plus headroom (VF_GPU_HEADROOM_BYTES, ALLOC-016). A missing declaration is a rejection, never a guess.
What the host says, the backend repeats: a failed nvidia-smi read is an
Unknown inventory (“could not observe”), never a confirmed zero-GPU
machine; an inherited CUDA_VISIBLE_DEVICES narrows the pool by ordinal,
card UUID or MIG UUID exactly as the CUDA runtime would (ALLOC-015). Grants
are written to the workers as UUIDs, the identity that survives renumbering.
There is no server-side compare-and-swap on a single host, so reserve
fences on the inventory generation instead: a plan made on one snapshot is
refused when the host has changed since (a device disappeared, a foreign
process appeared), and release refuses a stale generation the same way.
- videoflow.deploy.allocation_local.GRANT_ENV = 'VF_GPU_GRANT_JSON'
the worker-side copy of its
DeliveredGrant(RFC 0006 ENV-14).- Type:
Env
- videoflow.deploy.allocation_local.HEADROOM_ENV = 'VF_GPU_HEADROOM_BYTES'
bytes kept free on every shared device beyond the declared peaks (allocator fragmentation, the CUDA context of each process). Strict policy only.
- Type:
Env
- class videoflow.deploy.allocation_local.LocalAllocationBackend(policy: str = 'shared', headroom_bytes: int | None = None, host_reader: Callable[[], ~videoflow.backends.outcomes.Known[list[~videoflow.backends.allocation.DeviceIdentity]] | ~videoflow.backends.outcomes.Unknown]=<function host_devices_observed>, used_reader: Callable[[], ~videoflow.backends.outcomes.Known[dict[str, int]] | ~videoflow.backends.outcomes.Unknown]=<function host_memory_used_observed>, mask: str | None = None, inherit_mask: bool = True, clock: Callable[[], float]=<built-in function time>)[source]
Bases:
AcceleratorAllocationBackend- Arguments:
policy:
shared(wrap-around, labelled) orstrict(refuse short or over-budget grants before launch).headroom_bytes: strict-policy memory headroom per shared device; defaults to
VF_GPU_HEADROOM_BYTESor 1 GiB.host_reader / used_reader: the
nvidia-smireads, injectable so a process-level test can present one, zero or unobservable GPUs.mask: a
CUDA_VISIBLE_DEVICESvalue applied to the host pool before planning; by default the one this process inherited from its environment (inherit_mask=Falseignores the environment: the whole host).
- bindings(claim_id: str, workload_id: str) WorkloadBindings[source]
- capabilities(environment: Mapping[str, Any]) AllocationCapabilities[source]
- grant(claim_id: str, workload_id: str) DeliveredGrant[source]
The grant a workload received under this claim (what
bindingsserialises).
- inventory(scope: Mapping[str, Any]) Known[InventorySnapshot] | Unknown[source]
The devices this process may hand out: the host’s, narrowed by the inherited mask.
Unknownwhen the host could not be read — the caller decides what an unobservable host means, never “no GPUs”.
- observe(claim_id: str) Known[ClaimObservation] | Unknown[source]
Ready when every granted device is still enumerated by the host; Unknown when the host cannot be read.
- plan(requests: Sequence[WorkloadRequest], snapshot: InventorySnapshot) FeasiblePlan | Infeasible[source]
- property policy: str
- reconcile(claim_id: str, desired: str, expected_generation: str) ClaimObservation[source]
- release(claim_id: str, operation_id: str, expected_generation: str, keep_workloads: bool = False) ReleaseObservation[source]
- reserve(plan: FeasiblePlan, operation_id: str, expected_generation: str | None) ClaimObservation[source]
Fences the plan on the host: re-reads the inventory and refuses when its generation differs from the one the plan was made on (a device or a foreign process came or went since), returning
failedwith the evidence rather than granting devices the plan never saw.
- videoflow.deploy.allocation_local.device_key(device: DeviceIdentity) str[source]
The mask entry a worker receives for a device: its MIG UUID, else its card UUID, else the ordinal.
- videoflow.deploy.allocation_local.grant_from_env(environ: Mapping[str, str] = environ({'ACCEPT_EULA': 'Y', 'ACTIONS_ID_TOKEN_REQUEST_TOKEN': 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjM4ODI2YjE3LTZhMzAtNWY5Yi1iMTY5LThiZWI4MjAyZjcyMyIsInR5cCI6IkpXVCIsIng1dCI6InlrTmFZNHFNX3RhNGsyVGdaT0NFWUxrY1lsQSJ9.eyJJZGVudGl0eVR5cGVDbGFpbSI6IlN5c3RlbTpTZXJ2aWNlSWRlbnRpdHkiLCJhYyI6Ilt7XCJTY29wZVwiOlwicmVmcy9oZWFkcy9tYXN0ZXJcIixcIlBlcm1pc3Npb25cIjozfV0iLCJhY3NsIjoiMTAiLCJhdWQiOiJ2c286Y2ZmMGE0YWEtZjA3My00ZGJjLTliODQtM2IxNzFmYjRlYTM3IiwiYmlsbGluZ19vd25lcl9pZCI6Ik9fa2dET0F3bC1ydyIsImV4cCI6MTc4OTQyMjk4MSwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9wcmltYXJ5c2lkIjoiZGRkZGRkZGQtZGRkZC1kZGRkLWRkZGQtZGRkZGRkZGRkZGRkIiwiaHR0cDovL3NjaGVtYXMueG1sc29hcC5vcmcvd3MvMjAwNS8wNS9pZGVudGl0eS9jbGFpbXMvc2lkIjoiZGRkZGRkZGQtZGRkZC1kZGRkLWRkZGQtZGRkZGRkZGRkZGRkIiwiaWF0IjoxNzg5NDAwNzgxLCJpc3MiOiJodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tIiwiam9iX2lkIjoiZTcxMDY3NWEtZWQ1NC01NTMzLWIwYWYtMDFkZjE1MjY1ODNhIiwiam9iX3dvcmtmbG93X3JlZiI6InZpZGVvZmxvdy92aWRlb2Zsb3cvLmdpdGh1Yi93b3JrZmxvd3MvZG9jcy55bWxAcmVmcy9oZWFkcy9tYXN0ZXIiLCJqb2Jfd29ya2Zsb3dfc2hhIjoiYmFjYThmYTkwMWQ4OGIwN2YwYzBlM2YwMGE0ZjhhNDI2MzYxMTU1MSIsIm5hbWVpZCI6ImRkZGRkZGRkLWRkZGQtZGRkZC1kZGRkLWRkZGRkZGRkZGRkZCIsIm5iZiI6MTc4OTQwMDQ4MSwib2lkY19leHRyYSI6IntcImFjdG9yXCI6XCJqYWRpZWxhbVwiLFwiYWN0b3JfaWRcIjpcIjEwMjYwNjdcIixcImJhc2VfcmVmXCI6XCJcIixcImNoZWNrX3J1bl9pZFwiOlwiMTA0MDQ0Mjc2NzcyXCIsXCJldmVudF9uYW1lXCI6XCJwdXNoXCIsXCJoZWFkX3JlZlwiOlwiXCIsXCJqb2Jfd29ya2Zsb3dfcmVmXCI6XCJ2aWRlb2Zsb3cvdmlkZW9mbG93Ly5naXRodWIvd29ya2Zsb3dzL2RvY3MueW1sQHJlZnMvaGVhZHMvbWFzdGVyXCIsXCJqb2Jfd29ya2Zsb3dfc2hhXCI6XCJiYWNhOGZhOTAxZDg4YjA3ZjBjMGUzZjAwYTRmOGE0MjYzNjExNTUxXCIsXCJyZWZcIjpcInJlZnMvaGVhZHMvbWFzdGVyXCIsXCJyZWZfcHJvdGVjdGVkXCI6XCJmYWxzZVwiLFwicmVmX3R5cGVcIjpcImJyYW5jaFwiLFwicmVwb3NpdG9yeVwiOlwidmlkZW9mbG93L3ZpZGVvZmxvd1wiLFwicmVwb3NpdG9yeV9pZFwiOlwiMTgxNTU0OTM5XCIsXCJyZXBvc2l0b3J5X293bmVyXCI6XCJ2aWRlb2Zsb3dcIixcInJlcG9zaXRvcnlfb3duZXJfaWRcIjpcIjUwOTUzOTAzXCIsXCJyZXBvc2l0b3J5X3Zpc2liaWxpdHlcIjpcInB1YmxpY1wiLFwicnVuX2F0dGVtcHRcIjpcIjFcIixcInJ1bl9pZFwiOlwiMzQ4NjQzNDM5NjBcIixcInJ1bl9udW1iZXJcIjpcIjExXCIsXCJydW5uZXJfZW52aXJvbm1lbnRcIjpcImdpdGh1Yi1ob3N0ZWRcIixcInNoYVwiOlwiYmFjYThmYTkwMWQ4OGIwN2YwYzBlM2YwMGE0ZjhhNDI2MzYxMTU1MVwiLFwid29ya2Zsb3dcIjpcIkRvY3NcIixcIndvcmtmbG93X3JlZlwiOlwidmlkZW9mbG93L3ZpZGVvZmxvdy8uZ2l0aHViL3dvcmtmbG93cy9kb2NzLnltbEByZWZzL2hlYWRzL21hc3RlclwiLFwid29ya2Zsb3dfc2hhXCI6XCJiYWNhOGZhOTAxZDg4YjA3ZjBjMGUzZjAwYTRmOGE0MjYzNjExNTUxXCJ9Iiwib2lkY19zdWIiOiJyZXBvOnZpZGVvZmxvdy92aWRlb2Zsb3c6cmVmOnJlZnMvaGVhZHMvbWFzdGVyIiwib3JjaF9pZCI6IjU5OTczN2Q4LWE4MjEtNDViNS05NjhkLWQ4MTI1N2VkZmQwZi5idWlsZC5fX2RlZmF1bHQiLCJvd25lcl9pZCI6Ik9fa2dET0F3bC1ydyIsInBsYW5faWQiOiI1OTk3MzdkOC1hODIxLTQ1YjUtOTY4ZC1kODEyNTdlZGZkMGYiLCJyZXBvc2l0b3J5X2lkIjoiMTgxNTU0OTM5IiwicmVwb3NpdG9yeV9vd25lcl9pZCI6IjUwOTUzOTAzIiwicmVwb3NpdG9yeV92aXNpYmlsaXR5IjoicHVibGljIiwicnVuX2lkIjoiMzQ4NjQzNDM5NjAiLCJydW5fbnVtYmVyIjoiMTEiLCJydW5fdHlwZSI6ImZ1bGwiLCJydW5uZXJfaWQiOiIxMDAwMDAwMTY1IiwicnVubmVyX3R5cGUiOiJob3N0ZWQiLCJzY3AiOiJBY3Rpb25zLlJlc3VsdHM6NTk5NzM3ZDgtYTgyMS00NWI1LTk2OGQtZDgxMjU3ZWRmZDBmOmU3MTA2NzVhLWVkNTQtNTUzMy1iMGFmLTAxZGYxNTI2NTgzYSBBY3Rpb25zLlJ1bm5lcjo1OTk3MzdkOC1hODIxLTQ1YjUtOTY4ZC1kODEyNTdlZGZkMGY6ZTcxMDY3NWEtZWQ1NC01NTMzLWIwYWYtMDFkZjE1MjY1ODNhIEFjdGlvbnMuVXBsb2FkQXJ0aWZhY3RzOjU5OTczN2Q4LWE4MjEtNDViNS05NjhkLWQ4MTI1N2VkZmQwZjplNzEwNjc1YS1lZDU0LTU1MzMtYjBhZi0wMWRmMTUyNjU4M2EgZ2VuZXJhdGVfaWRfdG9rZW46NTk5NzM3ZDgtYTgyMS00NWI1LTk2OGQtZDgxMjU3ZWRmZDBmOmU3MTA2NzVhLWVkNTQtNTUzMy1iMGFmLTAxZGYxNTI2NTgzYSBBY3Rpb25zLkdlbmVyaWNSZWFkOjAwMDAwMDAwLTAwMDAtMDAwMC0wMDAwLTAwMDAwMDAwMDAwMCIsInNoYSI6ImJhY2E4ZmE5MDFkODhiMDdmMGMwZTNmMDBhNGY4YTQyNjM2MTE1NTEiLCJ0cnVzdF90aWVyIjoiMiJ9.odJ3vkzgRiNUCihXdyP65ZEvROIonxWdwBRZs1HbSmKcjM5eI-y6DKM1DmgqGX5_zmHqhcvJjQoQz1AQsgwkp-u5uIzFFLB0URMJK6x3xztWigmBI5mxSJgfBBYBndkRORJrkm6HYrkDfCF458M1oORSC1VMt2-qHEH9Lwp1heegCNjZ_PUv0SC6KCWCdoqMQpI-x1o66H0BxaufIW-OOM_Yprtyv_PrLLTfevwxL6r2uBxU_J8-qtayR1fz5n2mwPmlS5SbtN9vitatOU97yuSZY2q-ecQwbZbyt4Ny_BjduyjBTh-Zgt8KfHdrYZjDe7kk12Us7QKTixCiP__Kvw', 'ACTIONS_ID_TOKEN_REQUEST_URL': 'https://run-actions-3-azure-eastus.actions.githubusercontent.com/139//idtoken/599737d8-a821-45b5-968d-d81257edfd0f/e710675a-ed54-5533-b0af-01df1526583a?api-version=2.0', 'ACTIONS_ORCHESTRATION_ID': '599737d8-a821-45b5-968d-d81257edfd0f.build.__default', 'ACTIONS_RUNNER_ACTION_ARCHIVE_CACHE': '/opt/actionarchivecache', 'ACTIONS_RUNNER_RETURN_JOB_RESULT_FOR_HOSTED': '1', 'AGENT_TOOLSDIRECTORY': '/opt/hostedtoolcache', 'ANDROID_HOME': '/usr/local/lib/android/sdk', 'ANDROID_NDK': '/usr/local/lib/android/sdk/ndk/27.3.13750724', 'ANDROID_NDK_HOME': '/usr/local/lib/android/sdk/ndk/27.3.13750724', 'ANDROID_NDK_LATEST_HOME': '/usr/local/lib/android/sdk/ndk/29.0.14206865', 'ANDROID_NDK_ROOT': '/usr/local/lib/android/sdk/ndk/27.3.13750724', 'ANDROID_SDK_ROOT': '/usr/local/lib/android/sdk', 'ANT_HOME': '/usr/share/ant', 'AZURE_EXTENSION_DIR': '/opt/az/azcliextensions', 'BOOTSTRAP_HASKELL_NONINTERACTIVE': '1', 'CHROMEWEBDRIVER': '/usr/local/share/chromedriver-linux64', 'CHROME_BIN': '/usr/bin/google-chrome', 'CI': 'true', 'CONDA': '/usr/share/miniconda', 'DEBIAN_FRONTEND': 'noninteractive', 'DOTNET_MULTILEVEL_LOOKUP': '0', 'DOTNET_NOLOGO': '1', 'DOTNET_SKIP_FIRST_TIME_EXPERIENCE': '1', 'EDGEWEBDRIVER': '/usr/local/share/edge_driver', 'ENABLE_RUNNER_TRACING': 'true', 'GECKOWEBDRIVER': '/usr/local/share/gecko_driver', 'GHCUP_INSTALL_BASE_PREFIX': '/usr/local', 'GITHUB_ACTION': '__run_2', 'GITHUB_ACTIONS': 'true', 'GITHUB_ACTION_REF': '', 'GITHUB_ACTION_REPOSITORY': '', 'GITHUB_ACTOR': 'jadielam', 'GITHUB_ACTOR_ID': '1026067', 'GITHUB_API_URL': 'https://api.github.com', 'GITHUB_ARTIFACTS': '/home/runner/work/_temp/_runner_file_commands/artifacts_39e01021-587e-44b7-9b9d-f6103a2911fc', 'GITHUB_ARTIFACTS_LIST': '/home/runner/work/_temp/_runner_file_commands/artifacts_list_39e01021-587e-44b7-9b9d-f6103a2911fc', 'GITHUB_BASE_REF': '', 'GITHUB_ENV': '/home/runner/work/_temp/_runner_file_commands/set_env_39e01021-587e-44b7-9b9d-f6103a2911fc', 'GITHUB_EVENT_NAME': 'push', 'GITHUB_EVENT_PATH': '/home/runner/work/_temp/_github_workflow/event.json', 'GITHUB_GRAPHQL_URL': 'https://api.github.com/graphql', 'GITHUB_HEAD_REF': '', 'GITHUB_JOB': 'build', 'GITHUB_OUTPUT': '/home/runner/work/_temp/_runner_file_commands/set_output_39e01021-587e-44b7-9b9d-f6103a2911fc', 'GITHUB_PATH': '/home/runner/work/_temp/_runner_file_commands/add_path_39e01021-587e-44b7-9b9d-f6103a2911fc', 'GITHUB_REF': 'refs/heads/master', 'GITHUB_REF_NAME': 'master', 'GITHUB_REF_PROTECTED': 'false', 'GITHUB_REF_TYPE': 'branch', 'GITHUB_REPOSITORY': 'videoflow/videoflow', 'GITHUB_REPOSITORY_ID': '181554939', 'GITHUB_REPOSITORY_OWNER': 'videoflow', 'GITHUB_REPOSITORY_OWNER_ID': '50953903', 'GITHUB_RETENTION_DAYS': '90', 'GITHUB_RUN_ATTEMPT': '1', 'GITHUB_RUN_ID': '34864343960', 'GITHUB_RUN_NUMBER': '11', 'GITHUB_SERVER_URL': 'https://github.com', 'GITHUB_SHA': 'baca8fa901d88b07f0c0e3f00a4f8a4263611551', 'GITHUB_STATE': '/home/runner/work/_temp/_runner_file_commands/save_state_39e01021-587e-44b7-9b9d-f6103a2911fc', 'GITHUB_STEP_SUMMARY': '/home/runner/work/_temp/_runner_file_commands/step_summary_39e01021-587e-44b7-9b9d-f6103a2911fc', 'GITHUB_TRIGGERING_ACTOR': 'jadielam', 'GITHUB_WORKFLOW': 'Docs', 'GITHUB_WORKFLOW_REF': 'videoflow/videoflow/.github/workflows/docs.yml@refs/heads/master', 'GITHUB_WORKFLOW_SHA': 'baca8fa901d88b07f0c0e3f00a4f8a4263611551', 'GITHUB_WORKSPACE': '/home/runner/work/videoflow/videoflow', 'GOROOT_1_24_X64': '/opt/hostedtoolcache/go/1.24.13/x64', 'GOROOT_1_25_X64': '/opt/hostedtoolcache/go/1.25.14/x64', 'GOROOT_1_26_X64': '/opt/hostedtoolcache/go/1.26.8/x64', 'GRADLE_HOME': '/usr/share/gradle-9.7.1', 'HCA_CLOUD_PROVIDER': 'azure', 'HOME': '/home/runner', 'HOMEBREW_CLEANUP_PERIODIC_FULL_DAYS': '3650', 'HOMEBREW_NO_AUTO_UPDATE': '1', 'INVOCATION_ID': '15d8f2ea8d044311994759f59f0fbc0e', 'ImageOS': 'ubuntu24', 'ImageVersion': '20260907.300.1', 'JAVA_HOME': '/usr/lib/jvm/temurin-17-jdk-amd64', 'JAVA_HOME_11_X64': '/usr/lib/jvm/temurin-11-jdk-amd64', 'JAVA_HOME_17_X64': '/usr/lib/jvm/temurin-17-jdk-amd64', 'JAVA_HOME_21_X64': '/usr/lib/jvm/temurin-21-jdk-amd64', 'JAVA_HOME_25_X64': '/usr/lib/jvm/temurin-25-jdk-amd64', 'JAVA_HOME_8_X64': '/usr/lib/jvm/temurin-8-jdk-amd64', 'JOURNAL_STREAM': '9:12689', 'LANG': 'C.UTF-8', 'LOGNAME': 'runner', 'MEMORY_PRESSURE_WATCH': '/sys/fs/cgroup/system.slice/hosted-compute-agent.service/memory.pressure', 'MEMORY_PRESSURE_WRITE': 'c29tZSAyMDAwMDAgMjAwMDAwMAA=', 'NVM_DIR': '/home/runner/.nvm', 'OLDPWD': '/home/runner/work/videoflow/videoflow', 'PATH': '/home/runner/work/_temp/setup-uv-cache/builds-v0/.tmpBKyaem/bin:/home/runner/work/_temp/setup-uv-cache/archive-v0/yrD2oxi3dSpikfW8/bin:/home/runner/work/videoflow/videoflow/.venv/bin:/opt/hostedtoolcache/uv/0.12.13/x86_64:/home/runner/.local/bin:/snap/bin:/home/runner/.local/bin:/opt/pipx_bin:/home/runner/.cargo/bin:/home/runner/.config/composer/vendor/bin:/usr/local/.ghcup/bin:/home/runner/.dotnet/tools:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin', 'PIPX_BIN_DIR': '/opt/pipx_bin', 'PIPX_HOME': '/opt/pipx', 'POWERSHELL_DISTRIBUTION_CHANNEL': 'GitHub-Actions-Linux', 'PSModulePath': '/root/.local/share/powershell/Modules:/usr/local/share/powershell/Modules:/opt/microsoft/powershell/7/Modules:/usr/share/az_15.6.1', 'PWD': '/home/runner/work/videoflow/videoflow', 'RUNNER_ARCH': 'X64', 'RUNNER_ENVIRONMENT': 'github-hosted', 'RUNNER_NAME': 'GitHub Actions 1000000165', 'RUNNER_OS': 'Linux', 'RUNNER_TEMP': '/home/runner/work/_temp', 'RUNNER_TOOL_CACHE': '/opt/hostedtoolcache', 'RUNNER_TRACKING_ID': 'github_f645d36d-cf28-422d-aa73-164bbe3d2cec', 'RUNNER_WORKSPACE': '/home/runner/work/videoflow', 'SELENIUM_JAR_PATH': '/usr/share/java/selenium-server.jar', 'SGX_AESM_ADDR': '1', 'SHELL': '/bin/bash', 'SHLVL': '2', 'SWIFT_PATH': '/usr/share/swift/usr/bin', 'SYSTEMD_EXEC_PID': '1809', 'USER': 'runner', 'USE_BAZEL_FALLBACK_VERSION': 'silent:', 'UV': '/opt/hostedtoolcache/uv/0.12.13/x86_64/uv', 'UV_CACHE_DIR': '/home/runner/work/_temp/setup-uv-cache', 'UV_RUN_RECURSION_DEPTH': '1', 'VCPKG_INSTALLATION_ROOT': '/usr/local/share/vcpkg', 'VIRTUAL_ENV': '/home/runner/work/_temp/setup-uv-cache/builds-v0/.tmpBKyaem', 'XDG_CONFIG_HOME': '/home/runner/.config', 'XDG_RUNTIME_DIR': '/run/user/1001', '_': '/opt/hostedtoolcache/uv/0.12.13/x86_64/uv', 'DOCUTILSCONFIG': '/home/runner/work/videoflow/videoflow/docs/source/docutils.conf'})) DeliveredGrant | None[source]
The
DeliveredGranta worker was launched with, or None outside a local grant.
- videoflow.deploy.allocation_local.host_memory_used_observed() Known[dict[str, int]] | Unknown[source]
card uuid -> bytes in usefromnvidia-smi, orUnknownwhen the read failed.
- videoflow.deploy.allocation_local.snapshot_generation(devices: Sequence[DeviceIdentity], used: Mapping[str, int]) str[source]
A digest of what was observed: the fence
reserve/releasecheck the host against.
videoflow.deploy.broker_profiles module
Broker profiles: the sizing knobs videoflow deploy provisions its in-cluster
NATS and Redis with when the user brings no broker of their own.
deploy.infra renders the dev-grade single-replica Deployments this project
has always shipped, and those must stay byte-identical — the k8s integration
tests and every cluster that already runs them depend on that shape. The
durability work (multi-node JetStream, file-store persistence, an append-only
Redis) needs a second shape, and threading six positional knobs through
ensure_infra/nats_manifests/redis_manifests would have turned every
call site into a lie waiting to happen. So the knobs travel as two small records
this module owns, with one constructor per named profile:
BrokerProfile.dev()— today’s render: one server, emptyDir, nopersistence.
profile = Noneeverywhere indeploy.inframeans exactly this.
BrokerProfile.durable()— a NATS StatefulSet withcluster { routes }and a PVC per pod, so a stream with
jetstream_replicascopies survives a pod and a node.
RedisProfile.dev()— one server with an append-only file on anemptyDir and
noeviction: the same standing as the dev NATS (its file store is on an emptyDir too), so a BATCH flow’sreliable_workchannels are admitted on the dev pair and a pod loss costs both alike.
RedisProfile.durable()— the same append-only file on a PVC, so theblobs survive a pod and a node.
The dev Redis used to be a transport-only cache (persistence off,
volatile-lru). RFC 0006 made composition admission binding, and an
evictable store cannot certify reliable_work (PAY-010: a blob whose
obligations are outstanding must not be evicted, and a store that keeps nothing
across a restart lets an accepted envelope outlive its bytes). Every key still
carries a TTL (PROTOCOL.md BLOB-7) and the reconciler still reclaims orphans, so
memory stays bounded; under pressure a full store now refuses a write — a typed
TransientFailure the publisher sees — instead of silently dropping the oldest
blob. An operator who wants the old cache shape brings their own Redis
(--blob-redis-url), which admission reads back live and refuses for a BATCH
flow.
The records are dataclasses rather than dicts because we own their shape (the
Kubernetes objects they become stay dicts, per deploy.manifests). Validation
happens at construction so a bad profile fails at the CLI, before any manifest is
rendered — a JetStream cluster with an even replica count or a stream replicated
more times than there are servers cannot elect a leader, and the only symptom in
the cluster would be a provision Job that never completes.
Priority: priority_class lands on every pod a profile renders. The
--priority-class flag sets it on the workers and on the infra alike, so a
deploy told to yield to higher-priority work yields everything it created.
- videoflow.deploy.broker_profiles.BROKER_PROFILE_NAMES = ('dev', 'durable')
The profile names
--broker-profileaccepts, in the order the help shows them.
- class videoflow.deploy.broker_profiles.BrokerProfile(replicas: int = 1, jetstream_replicas: int = 1, storage_class: str | None = None, storage_size: str = '10Gi', persistence: bool = False, max_file_store: str = '10GB', priority_class: str | None = None)[source]
Bases:
objectHow the auto-provisioned NATS is shaped.
- Arguments:
replicas: NATS server pods.
1renders the Deployment ofk8s/nats.yaml; more renders a StatefulSet whose pods route to each other over a headless Service (cluster { routes }).jetstream_replicas: copies each stream should keep — what the provisioner asks JetStream for. At most
replicasand at most 5.storage_class: StorageClass of the per-pod PersistentVolumeClaim when
persistenceis on;Nonetakes the cluster default.storage_size: size of that claim (a Kubernetes quantity,
10Gi).persistence: keep the JetStream file store on a PersistentVolumeClaim instead of an emptyDir that dies with the pod.
max_file_store: the
jetstream { max_file_store }server limit; keep it understorage_sizewhen persisting.priority_class:
priorityClassNamefor the NATS pods, or none.
- classmethod dev(priority_class: str | None = None) BrokerProfile[source]
Today’s single-replica, emptyDir NATS — what
profile = Nonemeans.
- classmethod durable(replicas: int = 3, storage_class: str | None = 'local-path', priority_class: str | None = None) BrokerProfile[source]
A JetStream cluster whose streams keep up to three copies on persistent volumes.
jetstream_replicasismin(replicas, 3): three copies is the standard JetStream deployment and five buys little at this scale.
- jetstream_replicas: int = 1
- max_file_store: str = '10GB'
- property name: str
The
--broker-profilename this shape answers to (deploy.infrarecords it on the Service).
- persistence: bool = False
- priority_class: str | None = None
- replicas: int = 1
- property stateful: bool
Whether the profile renders a StatefulSet. True as soon as the pods need a stable identity: a persistent claim per pod, or route peers that must find each other by a predictable DNS name.
- storage_class: str | None = None
- storage_size: str = '10Gi'
- videoflow.deploy.broker_profiles.DEFAULT_STORAGE_CLASS = 'local-path'
The default StorageClass the durable profiles claim from.
local-pathis what k3s (and kind) ship; a cluster without it names its own through--broker-storage-class.
- videoflow.deploy.broker_profiles.REDIS_EVICTION_POLICIES = ('noeviction', 'volatile-lru', 'allkeys-lru', 'volatile-lfu', 'allkeys-lfu', 'volatile-random', 'allkeys-random', 'volatile-ttl')
Redis
maxmemory-policyvalues, as the server spells them.
- videoflow.deploy.broker_profiles.REDIS_PERSISTENCE_MODES = ('none', 'appendonly')
What
RedisProfile.persistencemay be.nonerenders--appendonly nowith RDB snapshots off (transport, not storage);appendonlyrenders an AOF under/data— on the volumeRedisProfile.storagenames.
- videoflow.deploy.broker_profiles.REDIS_STORAGE_MODES = ('emptyDir', 'claim')
an
emptyDirthat lives as long as the pod (a container restart replays the file, a pod loss does not — the dev NATS file store has the same standing) or a PersistentVolumeClaim.- Type:
Where
RedisProfile.persistencewrites
- class videoflow.deploy.broker_profiles.RedisProfile(persistence: str = 'none', eviction: str = 'noeviction', storage: str = 'emptyDir', storage_class: str | None = None, storage_size: str = '10Gi', priority_class: str | None = None)[source]
Bases:
objectHow the auto-provisioned Redis (the large-payload blob store) is shaped.
- Arguments:
persistence:
'none'(RDB and AOF off; the store is transport) or'appendonly'(an AOF under/dataon the volumestoragenames).eviction: the
maxmemory-policy. Both shipped profiles refuse a write when the store is full (noeviction) rather than drop a blob whose readers have not released it; every videoflow key still carries a TTL (PROTOCOL.md BLOB-7), which is what bounds orphans, not eviction.storage:
'emptyDir'(the pod’s lifetime; the dev profile) or'claim'(a PersistentVolumeClaim; the durable profile). Meaningful only with persistence on — a store that writes nothing needs no volume.storage_class: StorageClass of the claim;
Nonetakes the cluster default.storage_size: size of that claim.
priority_class:
priorityClassNamefor the Redis pod, or none.
- classmethod dev(priority_class: str | None = None) RedisProfile[source]
One server, an append-only file on an emptyDir,
noeviction: a blob outlives a container restart (like the dev NATS file store) and is never dropped while its readers hold it, which is whatreliable_workasks of a payload store; a pod loss takes it, which is whattolerated_failuresrefuses the dev pair for.
- classmethod durable(storage_class: str | None = 'local-path', priority_class: str | None = None) RedisProfile[source]
The same append-only, never-evicting Redis on a claim, so its blobs survive a pod and a node.
- eviction: str = 'noeviction'
- property name: str
durable(on a claim),dev(persistent on an emptyDir) orcache(nothing written — the shape an operator asks for explicitly; no--broker-profilename renders it).- Type:
The profile name recorded on the Service
- persistence: str = 'none'
- property persistent: bool
Whether the server writes its data set to disk at all (an AOF under
/data).
- priority_class: str | None = None
- property stateful: bool
Whether the profile keeps its data on a PersistentVolumeClaim.
- storage: str = 'emptyDir'
- storage_class: str | None = None
- storage_size: str = '10Gi'
- videoflow.deploy.broker_profiles.broker_profiles(name: str, replicas: int | None = None, storage_class: str | None = None, priority_class: str | None = None) tuple[BrokerProfile, RedisProfile][source]
The
(BrokerProfile, RedisProfile)pair a profile name denotes — the one lookup--broker-profilegoes through, so the CLI and any programmatic caller resolve a name the same way.- Arguments:
name: one of
BROKER_PROFILE_NAMES.replicas: NATS replica override (
--broker-replicas); durable only.storage_class: claim StorageClass override (
--broker-storage-class); durable only.priority_class:
priorityClassNamefor every infra pod.
- Raises:
ConfigError: an unknown name, or a durable-only override given with the dev profile (which has nothing to apply it to).
videoflow.deploy.build module
Image auto-build for videoflow deploy: when no --image is given, find the
Dockerfile next to the graph module (gpu.Dockerfile for a flow with GPU
nodes), build the videoflow-base image it is FROM if missing, and build the
solution image from the enclosing git root (solution Dockerfiles COPY sibling
packages, so the repo root is the context).
The base images can only be auto-built from a videoflow source checkout
(docker/base/Dockerfile COPYs the source tree); a wheel-only install gets a
precise error with the manual commands instead.
- videoflow.deploy.build.autobuild(graph_dir: str, needs_gpu: bool, context_override: str | None = None) str | None[source]
The whole auto-build path: find the Dockerfile, ensure its base image, build the solution image. Returns the built tag, or None when the solution ships no Dockerfile (caller falls back to explicit-image resolution).
- videoflow.deploy.build.base_image_for(dockerfile_path: str) str | None[source]
The default of the Dockerfile’s
ARG BASE_IMAGE=line, or None if it has none.
- videoflow.deploy.build.build_context_for(graph_dir: str, override: str | None = None) str[source]
The docker build context: an explicit
--build-context, else the git root enclosing the graph (solution Dockerfiles COPY sibling packages from the repo root — e.g. the offside solution copies nine of them), else the graph dir.
- videoflow.deploy.build.build_image(dockerfile: str, context: str, tag: str) None[source]
Builds the solution image, streaming docker output (layer cache makes unchanged rebuilds fast).
- videoflow.deploy.build.default_tag(graph_dir: str) str[source]
Deterministic human-readable tag for the auto-built solution image, e.g.
videoflow-offside:latest.
- videoflow.deploy.build.docker_gpus_available() bool[source]
Whether the local docker daemon has the NVIDIA runtime (for –gpus all).
- videoflow.deploy.build.ensure_base_image(base_ref: str) None[source]
Makes sure
base_ref(avideoflow-base:*image) exists locally, building it from the videoflow source checkout if missing.- Raises:
RuntimeErrorwhen the image is missing and videoflow is not an editable/source install (the base Dockerfile COPYs the source tree, so there is nothing to build from).
- videoflow.deploy.build.find_dockerfile(graph_dir: str, needs_gpu: bool) str | None[source]
The Dockerfile deploy builds the node image from:
gpu.Dockerfilewhen the flow has GPU nodes and one exists, elseDockerfile. None when the solution ships neither (the caller falls back to requiring--image).
- videoflow.deploy.build.run_in_image(image: str, command: List[str], mounts: List[Mount] | None = None, workdir: str | None = None, gpus: bool = False, capture: bool = False, interactive: bool = False) str | None[source]
Runs a command in the solution image with the given hostPath-style mounts (
Mountrecords frommanifests.parse_mounts) — how deploy executes the prepare hook and the graph compile without the graph’s deps on the host.Claim mounts (
manifests.parse_pvc_mounts,Mount.claimset) are skipped: a PersistentVolumeClaim exists only inside the cluster, and this container runs on the operator’s host, where the same directory is reached by the hostPath the claim shadows in the pods (seemanifests.pod_mounts).- Returns:
the command’s stdout when
capture, else None.
- Raises:
RuntimeErroron a non-zero exit (with stderr when captured).
videoflow.deploy.cli module
Command-line entrypoint for deploying a videoflow graph to Kubernetes.
videoflow deploy path/to/graph.py
deploy is one command that does everything: it generates the solution config
(asking the template’s questions when none exists), runs the solution’s prepare
hook, builds and loads the node image into the detected local cluster, provisions
the broker (an in-cluster dev NATS + Redis when --nats is omitted), applies
the flow, and — for a BATCH flow — waits for it to run to completion and then
tears down every resource (Kubernetes workloads + broker streams + owned infra).
A REALTIME flow is applied and left running; stop it later with videoflow
teardown. Each automatic step has an explicit override (--config,
--image, --nats, --mount, --no-prepare, --no-build, …).
Nothing is written to disk (beyond a generated config.yaml) unless
--render-only (write manifest files + kustomization) or --dry-run (print
YAML to stdout) is given.
The graph module must expose a factory (default name build_flow) that returns a
built videoflow.core.flow.Flow without calling .run() on it — the CLI needs
the graph, not a running flow. When the graph’s dependencies are not importable on
this machine, deploy compiles it inside the solution image instead.
- videoflow.deploy.cli.main(argv: list[str] | None = None) int[source]
The one place a videoflow failure becomes an operator-facing message and a process exit status.
The exit status carries the class of failure — 2 your flow, 3 your environment, 4 the flow ran and lost nodes, 5 it stalled — so CI and wrapper scripts can triage without parsing stderr. Everything used to exit 1.
- videoflow.deploy.cli.parse_resources(entries: list[str] | None) dict[str, dict[str, str]][source]
--resourcesvalues as{node or '*': {cpu, memory, cpu_limit, memory_limit}}: each entry isNODE=key:quantity[,key:quantity...](*for every node).- Raises:
ConfigError: an entry is malformed or names an unknown key.
- videoflow.deploy.cli.render_error(error: VideoflowError) None[source]
Prints a failure the way an operator needs to read it: what broke, then what to do about it, then the structured detail — never a traceback, which is a stack of framework internals the reader did not write and cannot act on. Set
VF_DEBUG=1when the traceback is the thing you want.
videoflow.deploy.cluster module
Best-effort detection of what kind of Kubernetes cluster kubectl points at, and the cluster-flavor-specific mechanics that depend on it: how to load a locally built image into it, whether hostPath mounts see the local filesystem, and whether GPU pods are schedulable. Everything here is advisory — deploy still works with explicit flags when detection gets it wrong.
Each flavor is one ClusterFlavorHandler registered at import. Supporting a
new one (microk8s, colima, k0s) is a class plus a register_cluster_flavor
call rather than an edit to three parallel if-ladders, which is what this used to
be — and what made it easy to teach detection about a flavor while forgetting to
teach image loading about it.
- class videoflow.deploy.cluster.ClusterFlavorHandler[source]
Bases:
objectOne local-cluster flavor: how to recognize it, how to get a locally built image into it, and whether its hostPath mounts see the host filesystem.
Subclasses set
nameand implementmatches.load_imagesdefaults to refusing, which is the right answer for anything remote.- hostpath_warning() str | None[source]
Message when hostPath will not resolve against the local filesystem, else None.
- load_images(images: List[str], kubectl: str = 'kubectl') None[source]
Side-loads locally built images so pods can run them without a registry.
- Raises:
RuntimeErrorwhen the images cannot be loaded (remote cluster, missing tool, failed command). The message must name the fix.
- matches(context_name: str, node_labels: Callable[[], str]) bool[source]
Whether the current cluster is this flavor.
node_labelsis a callable so a handler that can decide from the context name alone costs no kubectl call; the result is shared between handlers that do need it.
- name: str = ''
Flavor identifier, returned by
detect_clusterand passed back toload_images/hostpath_warning.
- class videoflow.deploy.cluster.GpuAvailability(per_node_allocatable: Dict[str, int]=<factory>, per_node_in_use: Dict[str, int]=<factory>, occupancy_known: bool = True)[source]
Bases:
objectOne GPU extended resource’s pool capacity with occupancy subtracted: what is allocatable per node, what running pods already claim, and the derived free numbers preflight compares demand against. Raw allocatable alone lies in a shared cluster — the scheduler will not grant units that other workloads hold.
- property allocatable: int
- property free: int
- property in_use: int
- property max_free_on_node: int
the most units any single node can still grant.
- Type:
The per-pod bound
- occupancy_known: bool = True
False when the pod listing behind
per_node_in_usecould not be read:freeis then an upper bound, not a fact, and preflight says so.
- per_node_allocatable: Dict[str, int]
- per_node_in_use: Dict[str, int]
- videoflow.deploy.cluster.active_runs_observed(kubectl: str, namespace: str, flow_id: str) Known[set[str]] | Unknown[source]
The run ids of every workload of
flow_idinnamespace(thevideoflow.io/run-idlabel of its Deployments, StatefulSets and Jobs), or Unknown when the listing failed — a failed read is never “no other run”.
- videoflow.deploy.cluster.allocatable_gpus(kubectl: str = 'kubectl', resource: str = 'nvidia.com/gpu') int[source]
Total allocatable units of one GPU extended resource across the pool’s nodes (0 when no pool node advertises it or the cluster is unreachable).
- videoflow.deploy.cluster.classify_gfd_labels(labels: Mapping[str, str], advertised_units: object = None) str[source]
What one node’s GPU units are, from its GPU Feature Discovery labels:
'physical'(one unit = one whole device),'mig'(hardware-isolated slices),'time-sliced'or'mps'(shares of a device — MPS is recognised from its explicit strategy label alone, with or without the replica/-SHAREDauxiliaries), or'unknown'(no GFD labels to judge by). The pure rule behindclassify_gpu_resource; the reference allocator’s node fixtures use the same one so both agree on every fixture.- Arguments:
advertised_units: the node’s allocatable count of the resource, when known. Under the
singleMIG strategy slices are advertised under the plain resource name, so more units thangpu.countcards is geometry evidence; a MIG-capable node with MIG disabled advertises whole cards and is physical.
- videoflow.deploy.cluster.classify_gpu_resource(kubectl: str = 'kubectl', resource: str = 'nvidia.com/gpu', exclude_nodes: AbstractSet[str] = frozenset({})) str[source]
What one advertised GPU extended resource’s units actually are, from GPU Feature Discovery node labels:
'physical'(one unit = one whole device),'mig'(units are hardware-isolated MIG slices),'time-sliced'(units are shares of a device), or'unknown'(no GFD labels to judge by — e.g. a non-NVIDIA resource, or a cluster without GFD).This is what makes
gpu_count > 1checkable: the scheduler happily grants N units of any integer resource, but only whole physical devices can be spanned by one model. Classification looks only at pool nodes (the samevideoflow.io/gpu-pool=truesnapshot the capacity math reads, minusexclude_nodes) that advertiseresource— a time-sliced node outside the pool advertising the same name is not somewhere a pool workload can land:a
mig-final path segment, ornvidia.com/mig.strategy=singleon a MIG-capable advertising node (slices renamed tonvidia.com/gpu) → mig;nvidia.com/gpu.sharing-strategy=time-slicing, a-SHAREDproduct suffix, ornvidia.com/gpu.replicas> 1 → time-sliced;GFD labels present and none of the above → physical.
Worst case wins across nodes (any time-sliced advertiser taints the answer): the scheduler may place the pod on any advertising node, so the safe claim is the weakest one.
- videoflow.deploy.cluster.combine_classifications(kinds: Iterable[str]) str[source]
One answer for a pool: worst case wins across advertisers. A share kind anywhere makes multi-unit claims impossible everywhere (the scheduler may place the pod on any advertising node), MIG likewise; and an unlabeled advertiser next to a physical one is not “physical” — nothing proves its units are whole devices — it is unresolved.
- videoflow.deploy.cluster.detect_cluster(kubectl: str = 'kubectl') str[source]
Classifies the cluster kubectl currently points at, by asking each registered flavor in order. Context-name conventions identify kind/minikube/docker-desktop; k3s installs use a generic context name (‘default’), so they are confirmed via node facts instead — fetched at most once per call, and only if some handler asks for them.
- videoflow.deploy.cluster.dra_owned_nodes_observed(kubectl: str = 'kubectl') Known[set] | Unknown[source]
The nodes a DRA driver publishes GPU
ResourceSlicesfor — the other allocator’s territory (ALLOC-018).Known(set())when the API serves no slices (or theresource.k8s.iogroup at all: a cluster without DRA has no DRA-owned nodes),Unknownonly when a listing that exists could not be read.
- videoflow.deploy.cluster.get_cluster_flavor(cluster: str) ClusterFlavorHandler[source]
The handler registered under
cluster.- Raises:
RuntimeErrorwhen no flavor is registered under that name.
- videoflow.deploy.cluster.gpu_availability(kubectl: str = 'kubectl', resource: str = 'nvidia.com/gpu', in_use: Dict[str, Dict[str, int]] | None = None, exclude_nodes: AbstractSet[str] = frozenset({})) GpuAvailability[source]
Pool-scoped availability of one GPU extended resource.
in_useaccepts a pre-fetchedgpu_units_in_use()result so a caller checking several resources fetches the pod list once; None fetches it here.exclude_nodesdrops nodes the caller has already ruled out (e.g. mix planning excluded them as another flow’s), so capacity math matches what will be planned.
- videoflow.deploy.cluster.gpu_inventory(kubectl: str = 'kubectl') List[NodeInventory][source]
gpu_inventory_observedfor display-only callers:[]when unknown.
- videoflow.deploy.cluster.gpu_inventory_observed(kubectl: str = 'kubectl') Known[List[NodeInventory]] | Unknown[source]
The videoflow pool’s GPU inventory as
NodeInventoryrecords — only nodes labeledvideoflow.io/gpu-pool=true, because that is the only place the rendered pods can schedule and the only nodes mix mode may repartition. Physical facts come off GPU Feature Discovery labels (nvidia.com/gpu.count,.product,.memory— the last in MiB); nodes without those labels contribute nothing: without GFD there is no inventory to lay out, and the mix strategy reports that as its own preflight problem rather than guessing.Each record also carries the node’s sharing/ownership state (time-slicing signals, existing MIG geometry, the
videoflow.io/gpu-ownerstamp, units in use by running pods). These are facts, not decisions: the cluster is multi-tenant, andMixGpudecides which nodes are usable.Unknownwhen the node listing itself failed; when only the pod listing failed, every record carriesoccupancy_known=Falseso the mix strategy refuses to plan on it rather than treat the pool as idle.
- videoflow.deploy.cluster.gpu_preflight(kubectl: str = 'kubectl', gpu_runtime_class: str | None = None, demand: dict | None = None, gpu_mode: str = 'exclusive', max_per_pod: dict | None = None, pod_claims: dict | None = None) List[str][source]
Checks what a GPU node workload needs (see
manifests._pod_spec): a node labeledvideoflow.io/gpu-pool=true; enough allocatable units of each requested extended resource to satisfy the flow’s whole demand (an under-provisioned flow schedules partially and stalls with the rest of its pods Pending); and — where the NVIDIA container runtime is an opt-in RuntimeClass rather than the node default — a--gpu-runtime-class, without which the pod schedules and then runs with no device. Returns problem strings with copy-pasteable fixes (empty list = OK). A problem prefixed withgpu.IMPOSSIBLE_GPU_REQUESTis fatal regardless of--strict-preflight(the request cannot work by construction).- Arguments:
demand: dict of extended-resource name -> total units the flow requests (sum over GPU nodes of
nb_tasks * gpu_count), or None to skip the capacity comparison and only check that the resource exists.gpu_mode: the
--gpu-modestrategy name whosepreflight_problemsruns the mode-specific checks.max_per_pod: dict of extended-resource name -> largest single-pod claim (
manifests.gpu_max_per_pod), or None to skip the per-node capacity and resource-classification checks (RFC 0003).pod_claims: dict of extended-resource name -> one claim per pod replica (
manifests.gpu_pod_claims), or None to skip the per-host packing check — the one that catches a pool whose free devices are fragmented across hosts although the total and the largest node both suffice.
- videoflow.deploy.cluster.gpu_units_in_use(kubectl: str = 'kubectl') Dict[str, Dict[str, int]][source]
gpu_units_in_use_observedfor display-only callers:{}when unknown. Anything that plans or mutates on occupancy must use the observed form.
- videoflow.deploy.cluster.gpu_units_in_use_observed(kubectl: str = 'kubectl') Known[Dict[str, Dict[str, int]]] | Unknown[source]
Extended-resource units currently claimed by pods, as node name -> resource -> units, summed over the
resources.limitsof every non-terminated pod in the cluster (all namespaces — a foreign workload’s claim occupies a device just as much as ours). Extended resources always carry a domain, so keys containing/are counted and native resources (cpu, memory,hugepages-*) are not.Unknownwhen the pod listing could not be read or parsed. Callers that decide anything destructive on occupancy must branch on that: a failed read does not prove a device idle.
- videoflow.deploy.cluster.hostpath_warning(cluster: str) str | None[source]
A message when hostPath mounts will NOT resolve against the local filesystem (the cluster “node” is a VM/container with its own filesystem), else None. An unregistered flavor warns nothing rather than raising: this is advisory, and deploy should not fail over a missing warning.
- videoflow.deploy.cluster.is_registry_qualified(image: str) bool[source]
Whether an image reference names a registry — its first path component has a dot or a port, or is
localhost(the Docker reference grammar’s rule for tellingregistry.example/ns/imgfrom the implicitdocker.ions/img). Such an image was pushed somewhere the cluster pulls from; a bare local tag was only ever built here.
- videoflow.deploy.cluster.load_images(cluster: str, images: List[str], kubectl: str = 'kubectl') None[source]
Loads locally built docker images into the detected cluster so pods can pull them without a registry.
- Raises:
RuntimeErrorwhen the cluster is remote (push to a registry instead), when a required tool is missing, or when a load command fails.
- videoflow.deploy.cluster.max_allocatable_gpus_per_node(kubectl: str = 'kubectl', resource: str = 'nvidia.com/gpu') int[source]
The largest allocatable count of one GPU extended resource on any single pool node (0 when no pool node advertises it or the cluster is unreachable). The per-pod schedulability bound, complementing
allocatable_gpus: total capacity answers “will the whole flow schedule”, this answers “can any single node host the biggest pod” — all of a pod’sgpu_countdevices must come from one node, so a flow can pass the total-capacity check and still never schedule.
- videoflow.deploy.cluster.nvidia_runtimeclass(kubectl: str = 'kubectl') str | None[source]
The NVIDIA RuntimeClass name when the cluster registers one, else None. Prefers the conventional
nvidiabut also recognizes variant names (e.g. a distro registeringnvidia-container-runtime) so the shared-mode escalation cannot silently no-op on them.
- videoflow.deploy.cluster.refuse_concurrent_run(kubectl: str, namespace: str, flow_id: str, run_id: str) None[source]
The
--single-runpolicy (RFC 0006 §10, RUN-047): refuse to startrun_idwhile another run of the flow holds workloads in the namespace. Decided before any resource is created.- Raises:
ActiveRunConflict: another run is active.
UnobservableState: the listing failed; an unverifiable namespace is not a free one.
- videoflow.deploy.cluster.register_cluster_flavor(handler: ClusterFlavorHandler, before: str | None = 'generic-remote') None[source]
Registers a cluster flavor. Detection tries handlers in registration order, so a new handler is inserted before
before(by default the generic-remote fallback, which matches everything and must stay last).- Arguments:
handler: the flavor handler instance.
before: name of the handler to insert ahead of, or None to append.
- Raises:
ValueError:
beforenames no registered flavor.
videoflow.deploy.compile module
Compiles a graph module to a JSON specs document, for use where the operator
machine cannot import the graph (its ML dependencies live only in the solution
image). videoflow deploy runs this inside that image:
docker run --rm -v <graph_dir>:<graph_dir> -w <graph_dir> <image> python -m videoflow.compile graph.py[:factory]
and rebuilds the NodeSpec list on the host with NodeSpec.from_dict —
the same serialization the provision Job’s specs ConfigMap uses.
Output (stdout): {"flow_id": ..., "flow_type": ..., "specs": [...]}.
- videoflow.deploy.compile.compile_to_dict(target: str, envelope_version: int | None = None) dict[source]
- videoflow.deploy.compile.declared_requirements(flow: Any) FlowRequirements[source]
What a flow’s nodes declare beyond the graph (sink effect guarantees, fused execution groups, batching contracts): the document half of the composition admission. Empty for a flow that declares nothing.
- videoflow.deploy.compile.load_flow(target: str) Flow[source]
- Arguments:
target:
path/to/graph.pyorpath/to/graph.py:factory_name(factory defaults tobuild_flow).
- Returns:
a built
Flowproduced by calling the factory.
- videoflow.deploy.compile.requirements_from_document(document: dict | str) FlowRequirements[source]
The declared requirements of a compile-JSON document (empty when it carries none).
videoflow.deploy.gpu module
GPU allocation strategies: how a GPU node’s pods claim devices, what to preflight before deploying them, and any cluster state a strategy needs to set up for a run and restore afterwards.
Three strategies ship, two of them matching the two-mode GPU design (RFC 0004):
exclusive(default) claims whole physical devices through an integer extended resource, so the scheduler accounts for them and a flow that outgrows the cluster stays Pending rather than thrashing. Under it a GPU unit is one whole device — sharing a device between components is not a capability of the mode, and the node API offers no way to ask for it. (Dev clusters share devices by advertising time-sliced units and keepinggpu_count == 1; the preflight below understands that arrangement.)mix(opt-in) serves declared demands: nodes withgpu_memory_gibget exclusive MIG slices chosen by the layout solver (deploy/mig.py), nodes withgpu_count— declared or defaulted — get whole physical cards. Itsprepare/cleanuphooks apply and restore MIG geometry for the run — over an explicitAllocationPlan(plan_layout→apply_plan→observe_geometry/cleanup), with ownership claimed by compare-and-swap, the occupancy re-read before any geometry write, and readiness judged by evidence of this operation rather than by themig.config.statelabel a previous geometry left behind (plan Phase 4;deploy/allocation_kubernetes.pyis the same machinery behind theAcceleratorAllocationBackendcontract).drarenders Dynamic Resource Allocation claims (deploy/allocation_dra.py) and refuses at preflight unless a DRA driver publishes GPU ResourceSlices.
The mode used to be a bare string branched on in four places (pod resources, manifest validation, preflight, the CLI’s choices), which is why adding a third meant finding all four. A strategy is now one class registered here.
- class videoflow.deploy.gpu.AllocationPlan(layout: GpuLayout, excluded: Mapping[str, str], flow_id: str | None, generation: str | None = None)[source]
Bases:
objectOne deploy’s MIG decision, explicit and immutable (plan Phase 4, ALLOC-011): the solver’s layout, the pool nodes it was not allowed to touch (with the reason each), the flow it was planned for and the inventory generation it was planned on. Passed between
resolve_specs/preflight/prepareinstead of living in the registered strategy singleton, so two deploys in one process — or a retried prepare in a fresh one — cannot act on each other’s cached geometry.- excluded: Mapping[str, str]
- flow_id: str | None
- generation: str | None = None
- class videoflow.deploy.gpu.AppliedGeometry(nodes: tuple[str, ...], entries: Mapping[str, str], epoch: str, namespace: str, expected_allocatable: Mapping[str, Mapping[str, int]])[source]
Bases:
objectWhat
MixGpu.apply_planwrote, so readiness can be judged against this operation: the per-node entry names (nonce’d per run), the owner epoch the claim was stamped under, the namespace the manager runs in, and the allocatable resources each node must advertise once the geometry is live.- entries: Mapping[str, str]
- epoch: str
- expected_allocatable: Mapping[str, Mapping[str, int]]
- namespace: str
- nodes: tuple[str, ...]
- videoflow.deploy.gpu.DEFAULT_GPU_RESOURCE = 'nvidia.com/gpu'
Extended resource used when neither the node nor the deploy names one.
- class videoflow.deploy.gpu.DraGpu(device_class: str = 'gpu.nvidia.com')[source]
Bases:
GpuStrategy--gpu-mode dra: GPU pods claim devices through aResourceClaimTemplateinstead of an extended resource. Renders only —prepare/cleanupare no-ops andpreflight_problemsreports the missing driver, so a deploy stops before applying claims nothing would allocate.- claim_manifests(spec: NodeSpec, namespace: str, flow_id: str, run_id: str) list[dict][source]
The
ResourceClaimTemplatethis node’s pods instantiate, one per pod.
- name: str = 'dra'
Mode name, as used by
--gpu-mode.
- pod_resources(spec: NodeSpec, gpu_resource_name: str | None = None) dict[source]
The container
resourcesfragment for one GPU pod —{}for a strategy that requests nothing.Stays a plain dict on purpose: it is spliced straight into a container spec, so its shape is Kubernetes’
ResourceRequirementsschema and not a record we own. A strategy may legitimately emitrequests,limitsor neither, with arbitrary extended-resource keys.- Arguments:
spec: the node’s
NodeSpec(gpu_count,gpu_resource_name).gpu_resource_name: deploy-level default extended-resource name.
- preflight_problems(kubectl: str = 'kubectl', demand: dict[str, int] | None = None, gpu_runtime_class: str | None = None, max_per_pod: dict[str, int] | None = None, pod_claims: dict[str, list[int]] | None = None) List[str][source]
Strategy-specific preflight problems, each a string naming its fix. The flavor-independent checks (cluster reachable, a labeled GPU node) are run by
cluster.gpu_preflightbefore this is called. A problem prefixed withIMPOSSIBLE_GPU_REQUESTis fatal regardless of--strict-preflight.Third-party strategies should tolerate future keyword inputs (accept
**kwargs): new preflight inputs arrive as keywords withNonedefaults, asmax_per_pod(RFC 0003) andpod_claimsdid.- Arguments:
demand: extended-resource name -> units the flow requests, or None to skip capacity comparison.
gpu_runtime_class: the
--gpu-runtime-classvalue, if given.max_per_pod: extended-resource name -> the largest single-pod claim (
manifests.gpu_max_per_pod), or None to skip per-node checks.pod_claims: extended-resource name -> one claim per pod replica (
manifests.gpu_pod_claims), or None to skip the per-host packing check (pack_pod_claims).
- class videoflow.deploy.gpu.ExclusiveGpu[source]
Bases:
GpuStrategyWhole-device claims through an integer extended resource (the default).
- name: str = 'exclusive'
Mode name, as used by
--gpu-mode.
- pod_resources(spec: NodeSpec, gpu_resource_name: str | None = None) dict[source]
The container
resourcesfragment for one GPU pod —{}for a strategy that requests nothing.Stays a plain dict on purpose: it is spliced straight into a container spec, so its shape is Kubernetes’
ResourceRequirementsschema and not a record we own. A strategy may legitimately emitrequests,limitsor neither, with arbitrary extended-resource keys.- Arguments:
spec: the node’s
NodeSpec(gpu_count,gpu_resource_name).gpu_resource_name: deploy-level default extended-resource name.
- preflight_problems(kubectl: str = 'kubectl', demand: dict[str, int] | None = None, gpu_runtime_class: str | None = None, max_per_pod: dict[str, int] | None = None, pod_claims: dict[str, list[int]] | None = None) List[str][source]
Strategy-specific preflight problems, each a string naming its fix. The flavor-independent checks (cluster reachable, a labeled GPU node) are run by
cluster.gpu_preflightbefore this is called. A problem prefixed withIMPOSSIBLE_GPU_REQUESTis fatal regardless of--strict-preflight.Third-party strategies should tolerate future keyword inputs (accept
**kwargs): new preflight inputs arrive as keywords withNonedefaults, asmax_per_pod(RFC 0003) andpod_claimsdid.- Arguments:
demand: extended-resource name -> units the flow requests, or None to skip capacity comparison.
gpu_runtime_class: the
--gpu-runtime-classvalue, if given.max_per_pod: extended-resource name -> the largest single-pod claim (
manifests.gpu_max_per_pod), or None to skip per-node checks.pod_claims: extended-resource name -> one claim per pod replica (
manifests.gpu_pod_claims), or None to skip the per-host packing check (pack_pod_claims).
- videoflow.deploy.gpu.GPU_OWNER_EPOCH_LABEL = 'videoflow.io/gpu-owner-epoch'
Companion of
GPU_OWNER_LABEL, stamped in the same write with a fresh per-claim value: a release can then tell the claim it is undoing from a later re-claim by the same flow (a crashed deploy’s teardown racing a redeploy), and leave the newer claim standing.
- videoflow.deploy.gpu.GPU_OWNER_LABEL = 'videoflow.io/gpu-owner'
Node label recording which flow owns a node’s MIG geometry. The cluster is multi-tenant: mix’s prepare() stamps it (compare-and-swap, no –overwrite) before partitioning, other flows exclude stamped nodes from planning and scheduling, and cleanup() restores only the nodes its flow stamped. The value is
manifests.k8s_name(flow_id)— identical to the pods’ flow-id label.
- videoflow.deploy.gpu.GPU_POOL_LABEL = 'videoflow.io/gpu-pool'
Node label a GPU pod selects on, and the taint key it tolerates.
- class videoflow.deploy.gpu.GpuStrategy[source]
Bases:
objectOne GPU allocation mode.
A strategy owns three decisions that must agree with each other: what a GPU pod asks the scheduler for (
pod_resources), what makes that request satisfiable and is therefore worth checking first (preflight_problems), and whether the cluster needs temporary reconfiguration to honour it (prepare/cleanup). Splitting them across modules is what made the old string-mode version easy to extend incorrectly.- claim_manifests(spec: NodeSpec, namespace: str, flow_id: str, run_id: str) list[dict][source]
Extra manifests a GPU node needs applied beside its workload (a
ResourceClaimTemplateunder DRA);[]by default. Plain dicts — Kubernetes API objects, rendered where the workload is.
- cleanup(kubectl: str = 'kubectl', flow_id: str | None = None) None[source]
Undoes
prepare. Must be idempotent and tolerant: it is called after apreparethat only partly succeeded, and — for a REALTIME flow, whose lifetime outlives the deploy command — from a latervideoflow teardownthat passes--gpu-modebut shares no state with the deploy that ranprepare. So it cannot assumepreparecompleted, or ran at all. With aflow_idit must restore only that flow’s state; without one it may sweep everything videoflow owns (single-operator escape hatch).
- name: str = ''
Mode name, as used by
--gpu-mode.
- pod_claims(spec: NodeSpec) list[dict][source]
The pod-level
spec.resourceClaimsentries a GPU pod ofspecneeds —[]for the extended-resource strategies, which claim throughpod_resourcesalone. A DRA strategy names its claim here (plan Phase 4).
- pod_resources(spec: NodeSpec, gpu_resource_name: str | None = None) dict[source]
The container
resourcesfragment for one GPU pod —{}for a strategy that requests nothing.Stays a plain dict on purpose: it is spliced straight into a container spec, so its shape is Kubernetes’
ResourceRequirementsschema and not a record we own. A strategy may legitimately emitrequests,limitsor neither, with arbitrary extended-resource keys.- Arguments:
spec: the node’s
NodeSpec(gpu_count,gpu_resource_name).gpu_resource_name: deploy-level default extended-resource name.
- preflight_problems(kubectl: str = 'kubectl', demand: dict[str, int] | None = None, gpu_runtime_class: str | None = None, max_per_pod: dict[str, int] | None = None, pod_claims: dict[str, list[int]] | None = None) List[str][source]
Strategy-specific preflight problems, each a string naming its fix. The flavor-independent checks (cluster reachable, a labeled GPU node) are run by
cluster.gpu_preflightbefore this is called. A problem prefixed withIMPOSSIBLE_GPU_REQUESTis fatal regardless of--strict-preflight.Third-party strategies should tolerate future keyword inputs (accept
**kwargs): new preflight inputs arrive as keywords withNonedefaults, asmax_per_pod(RFC 0003) andpod_claimsdid.- Arguments:
demand: extended-resource name -> units the flow requests, or None to skip capacity comparison.
gpu_runtime_class: the
--gpu-runtime-classvalue, if given.max_per_pod: extended-resource name -> the largest single-pod claim (
manifests.gpu_max_per_pod), or None to skip per-node checks.pod_claims: extended-resource name -> one claim per pod replica (
manifests.gpu_pod_claims), or None to skip the per-host packing check (pack_pod_claims).
- prepare(demand: dict[str, int] | None = None, kubectl: str = 'kubectl', flow_id: str | None = None) None[source]
Cluster setup this strategy needs before a run’s manifests are applied. Default: nothing. A strategy that mutates cluster state here is responsible for restoring it in
cleanup— and, in a multi-tenant cluster, for marking that state withflow_idso concurrent flows keep out of each other’s way.
- resolve_specs(specs: List[NodeSpec], kubectl: str = 'kubectl', default_resource: str | None = None, flow_id: str | None = None) List[NodeSpec][source]
The strategy’s chance to decide names and geometry before anything is rendered or preflighted: called once per deploy, with the compiled specs, before
gpu_demand/gpu_max_per_pod/render_manifestsconsume them. Default: identity. Themixstrategy returns specs whosegpu_resource_namecarries each sharer’s solver-chosen MIG profile — after which the entire downstream pipeline runs unchanged.Like
preflight_problems, new lifecycle inputs arrive as keywords withNonedefaults (flow_iddid): third-party strategies should accept**kwargs.flow_ididentifies the deploying flow so a multi-tenant strategy can tell its own cluster state from another flow’s.- Raises:
ValueError: the flow’s demands cannot be laid out (
mix’sLayoutErroris one) — the deploy should stop before rendering.
- videoflow.deploy.gpu.IMPOSSIBLE_GPU_REQUEST = 'impossible GPU request'
Marker prefix for a preflight problem that is fatal regardless of
--strict-preflight: the request is impossible by construction (a multi-unit claim against a MIG or time-sliced resource), so deploying anyway can only end in an admission error or a silently broken visibility contract. A constant so the CLI’s check never depends on message prose.
- videoflow.deploy.gpu.MIG_APPLY_TIMEOUT_SECONDS = 600
How long prepare() waits for the MIG manager to report success per apply.
- videoflow.deploy.gpu.MIG_CONFIGMAP_NAME = 'videoflow-mig-parted-config'
the operator’s current mig-parted config with the generated videoflow-<node> entries merged in. The MIG manager only reads the ConfigMap named in ClusterPolicy migManager.config.name, so prepare() points that field here for the run.
- Type:
ConfigMap videoflow publishes in the operator namespace
- videoflow.deploy.gpu.MIG_CONFIG_LABEL = 'nvidia.com/mig.config'
The node label the GPU Operator’s MIG manager watches, and its status twin.
- videoflow.deploy.gpu.MIG_CONFIG_NAME_ABSENT = '__absent__'
Sentinel recorded in MIG_CONFIG_NAME_RESTORE_ANNOTATION when the field was absent — never ‘’, which a failed read could be mistaken for.
- videoflow.deploy.gpu.MIG_CONFIG_NAME_RESTORE_ANNOTATION = 'videoflow.io/mig-config-name-restore'
ClusterPolicy annotation recording the pre-videoflow migManager.config.name, so a teardown in a fresh shell can restore it (the ClusterPolicy twin of MIG_RESTORE_ANNOTATION on nodes).
- videoflow.deploy.gpu.MIG_DISABLED_CONFIG = 'videoflow-all-disabled'
Config entry always injected into the merged file so cleanup() can un-MIG a node whose pre-videoflow mig.config label was absent: removing the label triggers no reconfiguration, so such nodes are pointed here first.
- videoflow.deploy.gpu.MIG_ENTRY_ANNOTATION = 'videoflow.io/mig-entry'
Node annotation recording the node’s current mig-parted entry name. Entry names carry a per-run nonce (see
_mig_config_name), so unlike the old fixedvideoflow-<node>scheme they cannot be reconstructed — cleanup() reads this annotation (and the mig.config label) to know which entries in the shared ConfigMap are this flow’s to strip. Stamped before the ConfigMap publish, so even a prepare that crashes between publishing and labeling leaves the record.
- videoflow.deploy.gpu.MIG_LABEL_ABSENT = '__absent__'
The restore record’s value for “the label did not exist” (annotation values cannot be null, and ‘’ is a legitimate label value of its own).
- videoflow.deploy.gpu.MIG_MANAGER_ROLLOUT_TIMEOUT_SECONDS = 300
How long prepare() waits for the mig-manager DaemonSet to remount the videoflow ConfigMap after the ClusterPolicy patch.
- videoflow.deploy.gpu.MIG_OPERATOR_DEFAULT_CONFIGMAP = 'default-mig-parted-config'
The GPU Operator’s stock mig-parted ConfigMap — the merge base when ClusterPolicy names none, and what cleanup() restores when the migManager.config.name field was absent before videoflow touched it.
- videoflow.deploy.gpu.MIG_RESTORE_ANNOTATION = 'videoflow.io/mig-config-restore'
Node annotation where mix’s prepare() records the node’s previous nvidia.com/mig.config label value, so a later cleanup() — possibly a teardown in a fresh shell — can restore it without sharing any state with the deploy that ran prepare(). Key-presence semantics (ALLOC-033): an absent label is recorded as
MIG_LABEL_ABSENTand removed again on restore; an explicitly empty label is recorded as ‘’ and restored as ‘’. (Records written before the sentinel existed hold ‘’ for both; they restore as an empty label, which the MIG manager treats exactly like an absent one.)
- videoflow.deploy.gpu.MIG_TOMBSTONE_ANNOTATION = 'videoflow.io/mig-config-tombstone'
ConfigMap annotation the last flow out stamps on
MIG_CONFIGMAP_NAMEinstead of deleting it (value: UTC time of retirement). kubectl cannot express delete-with-precondition, and a map nothing references is harmless where a wrong delete pulls the file out from under a manager still mounting it — so retirement is strip + tombstone, and removal is the operator’s: kubectl delete configmap videoflow-mig-parted-config -n <gpu-operator namespace>
- class videoflow.deploy.gpu.MixGpu[source]
Bases:
ExclusiveGpuDeclared-demand MIG partitioning (RFC 0004): sharers (
gpu_memory_gib) get exclusive MIG slices chosen by the layout solver, spanners and undeclared-demand nodes get whole physical cards. Pod claims are exclusive-style integer limits — only the names differ, and those are decided inresolve_specs, which is why this subclassesExclusiveGpu.- apply_plan(plan: AllocationPlan, kubectl: str = 'kubectl', flow_id: str | None = None) AppliedGeometry | None[source]
preparefor an explicit plan; returns what was written (None when the plan MIGs nothing). Between claiming the nodes and touching any geometry the occupancy is read again: a pod that landed on a planned card since the plan was made would be destroyed by repartitioning, so a changed or unreadable occupancy releases the claims and aborts (ALLOC-011).
- static apply_plan_to_specs(plan: AllocationPlan, specs: List[NodeSpec]) List[NodeSpec][source]
The specs with each sharer’s solver-chosen MIG profile as its
gpu_resource_name.
- cleanup(kubectl: str = 'kubectl', flow_id: str | None = None) None[source]
Restores every node carrying
MIG_RESTORE_ANNOTATIONto its recorded pre-videoflownvidia.com/mig.configvalue — a node whose label was absent is first pointed atMIG_DISABLED_CONFIGso the manager actually un-partitions the cards (removing the label triggers nothing), then unlabeled — and waits formig.config.state=successbefore declaring a node done. Only when every node reverted AND no other flow’s entries remain in the published map does it restore ClusterPolicymigManager.config.namefromMIG_CONFIG_NAME_RESTORE_ANNOTATIONand delete the published ConfigMap (last one out); otherwise it strips only this flow’s entries. Entry names carry a per-run nonce, so which entries are this flow’s is read off its nodes’mig.configlabels andMIG_ENTRY_ANNOTATIONstamps, never reconstructed. A node that failed keeps its annotation, and the policy patch and ConfigMap stay wired, so a retried teardown can resume — and a node already sitting at its restore target withstate=failedis first bounced through a nonce’d alias of the disabled entry, because rewriting the identical label value is a no-op for the manager and would deadlock every retry. State lives entirely in the cluster, so this works from a teardown that shares nothing with the deploy that ran prepare — and is a no-op when prepare never ran.With a
flow_id, only nodes stampedGPU_OWNER_LABEL=<this flow>are touched — other flows’ geometry stays up. Without one, every node videoflow owns is swept (the single-operator escape hatch, and the pre-ownership behaviour). A node stamped but never restore-annotated — prepare crashed between claiming and labeling — has no geometry to revert, so its claim is simply released.
- name: str = 'mix'
Mode name, as used by
--gpu-mode.
- observe_geometry(kubectl: str, applied: AppliedGeometry) Known[dict[str, str]] | Unknown[source]
One correlated readiness read per node of an
apply_planresult:readywhen the node still carries this operation’s entry name and owner epoch, reportssuccessand advertises the expected slices;failedon the manager’s verdict;pendingotherwise;lostwhen the entry or the claim is no longer this operation’s.Unknownwhen a node could not be read.
- plan_layout(specs: List[NodeSpec], kubectl: str = 'kubectl', flow_id: str | None = None) AllocationPlan[source]
Reads the pool and solves the layout for
specs— an explicit, immutableAllocationPlan(ALLOC-011). RaisesUnobservableStateon an unreadable pool andLayoutErrorwhen the demands cannot be laid out; never caches.
- preflight_for(plan: AllocationPlan, kubectl: str = 'kubectl', demand: dict[str, int] | None = None, gpu_runtime_class: str | None = None) List[str][source]
preflight_problemsfor an explicit plan.
- preflight_problems(kubectl: str = 'kubectl', demand: dict[str, int] | None = None, gpu_runtime_class: str | None = None, max_per_pod: dict[str, int] | None = None, pod_claims: dict[str, list[int]] | None = None) List[str][source]
Strategy-specific preflight problems, each a string naming its fix. The flavor-independent checks (cluster reachable, a labeled GPU node) are run by
cluster.gpu_preflightbefore this is called. A problem prefixed withIMPOSSIBLE_GPU_REQUESTis fatal regardless of--strict-preflight.Third-party strategies should tolerate future keyword inputs (accept
**kwargs): new preflight inputs arrive as keywords withNonedefaults, asmax_per_pod(RFC 0003) andpod_claimsdid.- Arguments:
demand: extended-resource name -> units the flow requests, or None to skip capacity comparison.
gpu_runtime_class: the
--gpu-runtime-classvalue, if given.max_per_pod: extended-resource name -> the largest single-pod claim (
manifests.gpu_max_per_pod), or None to skip per-node checks.pod_claims: extended-resource name -> one claim per pod replica (
manifests.gpu_pod_claims), or None to skip the per-host packing check (pack_pod_claims).
- prepare(demand: dict[str, int] | None = None, kubectl: str = 'kubectl', flow_id: str | None = None) None[source]
Applies the layout’s MIG geometry through the GPU Operator: claim the target nodes for this flow, merge the generated mig-parted config into the operator’s file (preserving other flows’ published entries), publish the result as
MIG_CONFIGMAP_NAME, point ClusterPolicymigManager.config.nameat it (recording the original name inMIG_CONFIG_NAME_RESTORE_ANNOTATION), wait for the mig-manager DaemonSet to remount, then label each MIG’d node with its per-run entry name —videoflow-<node>-<nonce>, see_mig_config_name— (recording its previous label inMIG_RESTORE_ANNOTATIONand the entry name inMIG_ENTRY_ANNOTATION) and wait fornvidia.com/mig.config.state=success. The per-run nonce guarantees the label value changes: the manager reacts only to changes, so a leftoverstate=failedfrom a previous attempt cannot deadlock the retry. The manager only reads the ConfigMap that ClusterPolicy names — a side ConfigMap it never mounts cannot carry the config. Without a MIG manager or a ClusterPolicy it fails actionably, with the config to apply by hand.
- resolve_specs(specs: List[NodeSpec], kubectl: str = 'kubectl', default_resource: str | None = None, flow_id: str | None = None) List[NodeSpec][source]
The strategy’s chance to decide names and geometry before anything is rendered or preflighted: called once per deploy, with the compiled specs, before
gpu_demand/gpu_max_per_pod/render_manifestsconsume them. Default: identity. Themixstrategy returns specs whosegpu_resource_namecarries each sharer’s solver-chosen MIG profile — after which the entire downstream pipeline runs unchanged.Like
preflight_problems, new lifecycle inputs arrive as keywords withNonedefaults (flow_iddid): third-party strategies should accept**kwargs.flow_ididentifies the deploying flow so a multi-tenant strategy can tell its own cluster state from another flow’s.- Raises:
ValueError: the flow’s demands cannot be laid out (
mix’sLayoutErroris one) — the deploy should stop before rendering.
- videoflow.deploy.gpu.PACKING_EXACT_MAX_PODS = 12
within them a negative answer is a proof; beyond them it is first-fit-decreasing’s opinion, and the problem string says so.
- Type:
Bounds of the exhaustive placement search in
pack_pod_claims
- class videoflow.deploy.gpu.PodPacking(feasible: bool, proven: bool, placement: Dict[int, str], unplaced: tuple[int, ...])[source]
Bases:
objectWhether a list of per-pod whole-unit claims fits a pool’s per-node free units with every pod’s whole claim on one node.
- Attributes:
feasible: a placement was found.
proven: the answer is exact — a placement is always a proof, and an infeasible answer is one when an exhaustive search found nothing. False only for an infeasible answer on an instance too large to search, where the scheduler might still succeed.
placement: claim index -> node, when feasible.
unplaced: the claim indices first-fit-decreasing could not place — the evidence the report quotes when nothing fits.
- feasible: bool
- placement: Dict[int, str]
- proven: bool
- unplaced: tuple[int, ...]
- videoflow.deploy.gpu.UNOBSERVABLE_GPU_STATE = 'unobservable GPU state'
Prefix of a preflight problem saying that an occupancy or inventory read the capacity math depends on could not be made. The numbers that follow assume an idle pool — an upper bound, not a fact. Fatal for a mode that mutates the cluster on the strength of it (mix repartitions cards); for exclusive claims it is a warning
--strict-preflightpromotes.
- videoflow.deploy.gpu.expected_allocatable(layout: GpuLayout) dict[str, dict[str, int]][source]
Per MIG’d node, the extended resources the device plugin must advertise once the layout is applied (mixed strategy naming,
nvidia.com/mig-<profile>per slice,nvidia.com/gpuper untouched card). The readiness evidence_wait_for_geometrydemands beyond asuccesslabel: a label left by an earlier geometry says nothing about this one, the advertised resources do.
- videoflow.deploy.gpu.flow_owner_value(flow_id: str) str[source]
The
GPU_OWNER_LABELvalue for a flow:manifests.k8s_name(flow_id), so an arbitrary--flow-idcharset becomes a legal <= 63-char label value — the same value the flow’s pods already carry in their flow-id label.
- videoflow.deploy.gpu.geometry_advertised(allocatable: Mapping[str, int], expected: Mapping[str, int]) bool[source]
Whether a node advertises at least the resources a layout needs on it.
- videoflow.deploy.gpu.get_gpu_mode(name: str) GpuStrategy[source]
The strategy registered under
name.- Raises:
ValueError: no strategy is registered under that name; the message names the known modes and
register_gpu_mode.
- videoflow.deploy.gpu.gpu_resource_provenance(spec: NodeSpec, default: str | None = None) tuple[str, str][source]
resolve_gpu_resourcewith its answer’s source:(name, source)where the source is'strategy'(the spec’s resolved name),'cli-default'(--gpu-resource-name) or'default'(nvidia.com/gpu). The one merge point for the resource name, expressed as declarations to the shared resolver invideoflow.core.provenanceso precedence and provenance are stated once: a strategy’s resolution is a hard decision, the two defaults are soft.
- videoflow.deploy.gpu.pack_pod_claims(claims: Sequence[int], free: Mapping[str, int]) PodPacking[source]
Decide whether every pod’s whole-unit claim can be placed on one node of a pool with
freeunits per node — the per-host check the aggregate arithmetic cannot make: per-node free[3, 3]against pods[2, 2, 2]passes both the total (6 <= 6) and the largest-pod bound (2 <= 3), yet only two pods fit.First-fit-decreasing answers first, and a placement it finds is a proof. When it finds none and the instance is small (at most
PACKING_EXACT_MAX_PODSpods overPACKING_EXACT_MAX_NODESnodes) an exhaustive search settles the question either way; a larger instance is reported infeasible but unproven.- Arguments:
claims: one entry per pod replica (
manifests.gpu_pod_claims).free: node -> free units of the resource being claimed.
- Returns:
a
PodPacking.
- videoflow.deploy.gpu.register_gpu_mode(strategy: GpuStrategy) None[source]
Registers a GPU allocation strategy under its
name. Registering makes the mode selectable via--gpu-mode— the CLI builds its choices from here — so a new strategy needs no CLI edit.- Arguments:
strategy: the strategy instance.
- Raises:
ValueError: the strategy has no
name.
- videoflow.deploy.gpu.registered_gpu_modes() list[str][source]
Registered GPU mode names, sorted. The CLI’s
--gpu-modechoices.
- videoflow.deploy.gpu.resolve_gpu_resource(spec: NodeSpec, default: str | None = None) str[source]
The extended-resource name a GPU spec requests: the spec’s resolved name (internal — only a GPU strategy sets it, e.g.
mixassigning a MIG profile), else the deploy default (--gpu-resource-name), elsenvidia.com/gpu.
videoflow.deploy.images module
Resolve the container image a node’s worker runs in on Kubernetes.
There is no module-path “family” inference: a user defines their processors in their
own package and builds their own image (their code + deps on top of
videoflow-base), so the image must be stated explicitly. Resolution order, first
match wins:
a deploy-time override for the node (
--image-override <name>=<ref>)the node’s own
image=kwarg (declared in graph code)the deploy-time default (
--image <ref>)
If none apply, resolution raises with an actionable message instead of guessing.
This module also owns the pull policy — how the cluster obtains that image once it
is named. It lives here rather than in manifests because the CLI must read the
default while building its parser, and manifests imports the optional yaml
extra at module scope; this module imports nothing outside the stdlib.
- videoflow.deploy.images.parse_override(spec: str) tuple[source]
Parses a
name=refCLI override into a(name, ref)tuple.
- videoflow.deploy.images.resolve_image(node_name: str, node_image: str | None, default_image: str | None = None, overrides: dict | None = None) str[source]
- Arguments:
node_name: the node’s stable name (matched against
overrides).node_image: the image declared on the node (
Node.image), or None.default_image: the deploy-time flow default (
--image), or None.overrides: mapping of node name to image ref (
--image-override).
- Returns:
the resolved image ref (str).
- Raises:
ValueErrorif no image can be determined for the node.
videoflow.deploy.infra module
Auto-provisioning of the dev broker infrastructure videoflow deploy needs
when the user doesn’t bring their own: an in-cluster NATS JetStream server and,
when the blob store is wanted, a Redis. Built as plain dicts (same convention as
manifests) so they ship inside the package, parametrize by namespace, and
carry an ownership label for selective teardown.
Two shapes, chosen by a deploy.broker_profiles profile:
the dev profile (
profile = None): single replica, emptyDir — a faithful port ofk8s/nats.yamlfor NATS, and a Redis whose append-only file lives on an emptyDir withnoeviction(RedisProfile.dev()), so the pair has one standing: both survive a container restart, neither a pod loss, and a BATCH flow’sreliable_workchannels are admitted on it. For production, bring your own broker (the official NATS Helm chart, a managed Redis) and pass--nats/--blob-redis-url.the durable profile: a NATS StatefulSet whose pods route to each other through a headless Service (
cluster { routes }) and keep the JetStream file store on a PersistentVolumeClaim each, plus an append-only Redis on a claim of its own. Enough for a stream to survive a pod, a node, or a rollout; still not a tuned production broker.
Ownership rule: a pre-existing nats/redis Service in the namespace is
reused as-is and never owned; only components this module applied are returned
as “created” and later torn down. Each Service records the profile it was
rendered from (videoflow.io/profile, plus videoflow.io/replicas for
NATS) so a later deploy that finds it can judge what it is reusing — admission
runs against the recorded profile, not the one the deploy would have rendered —
and refuse an explicit --broker-profile that contradicts it
(reused_infra / adopt_profiles). Persistent claims a durable profile created
are deliberately not torn down with the workloads — the data is the point —
so a redeploy finds it; reclaim them by hand with
kubectl delete pvc -n <namespace> -l videoflow.io/infra.
- videoflow.deploy.infra.LABEL_PROFILE = 'videoflow.io/profile'
the profile it was rendered from (
BrokerProfile.name/RedisProfile.name) and, for NATS, its replica count — what a later deploy reads to judge the infrastructure it reuses.- Type:
Recorded on each client Service
- videoflow.deploy.infra.NATS_SERVICE = 'nats'
The client Service every profile exposes (what
infra_urlsnames) and the headless one a StatefulSet’s pods address each other through.
- videoflow.deploy.infra.REDIS_CLAIM = 'redis-data'
The claim a durable Redis keeps its append-only file on.
- class videoflow.deploy.infra.ReusedInfra(nats: dict | None, redis: dict | None)[source]
Bases:
objectWhat
ensure_infrawill find and reuse: the labels of thenatsandredisServices already in the namespace (None= absent, so this deploy creates it). A component that is present but carries no profile record was created by hand or by an older videoflow.- nats: dict | None
- redis: dict | None
- videoflow.deploy.infra.adopt_profiles(reuse: ReusedInfra, requested: str | None, broker: BrokerProfile, redis: RedisProfile, namespace: str) tuple[BrokerProfile | None, RedisProfile | None][source]
The profiles admission judges a deploy by, given what the namespace already runs: a component this deploy creates is judged by the profile it renders; a reused one by the profile its creator recorded on the Service, or by nothing (
None— unread, for the planner to rule on) when it carries no record. An operator who named a profile that contradicts a record is refused:ensure_infrawould reuse the other shape silently otherwise.- Arguments:
reuse: what
reused_infrafound.requested: the
--broker-profilename the operator passed, orNonewhen they left the choice to the deploy.broker / redis: the profiles the deploy renders for what is missing.
Returns:
(BrokerProfile | None, RedisProfile | None).- Raises:
ConfigError: an explicit profile contradicts a reused component’s record.
- videoflow.deploy.infra.ensure_infra(kubectl: str, namespace: str, need_redis: bool, profile: BrokerProfile | None = None, redis_profile: RedisProfile | None = None) tuple[source]
Applies the NATS (and, when
need_redis, Redis) of the given profiles unless a Service of the same name already exists in the namespace (bring-your-own is reused, not owned).- Arguments:
profile: how to shape NATS;
Noneis the dev profile.redis_profile: how to shape Redis;
Noneis the dev profile.
- Returns:
(urls, created)whereurlsmapsnats/redisto in-cluster URLs (redisis None when not needed) andcreatedlists only the components THIS call applied (what teardown may later delete).
- videoflow.deploy.infra.infra_urls(namespace: str) dict[source]
The in-cluster URLs workers use once the dev infra is up.
- videoflow.deploy.infra.nats_conf(namespace: str, profile: BrokerProfile | None = None) str[source]
The
nats.confthe server pods run with.The dev profile’s text is the one
k8s/nats.yamlcarries. A stateful profile addsserver_name(JetStream clustering requires a unique name per server;$POD_NAMEis substituted by the server from the pod’s downward-API env) and, for more than one replica, aclusterblock whose routes name every peer through the headless Service — the seed list JetStream’s Raft groups form over.
- videoflow.deploy.infra.nats_manifests(namespace: str, profile: BrokerProfile | None = None) list[source]
The NATS JetStream server for
namespace+ infra labels: the single-replica Deployment ofk8s/nats.yamlfor the dev profile (None), a StatefulSet with a headless Service, route peers and one claim per pod for a durable one. The client Service is namednatsin every profile, which is whatinfra_urlsand the reuse rule key on.
- videoflow.deploy.infra.redis_manifests(namespace: str, profile: RedisProfile | None = None) list[source]
Single-replica Redis for the large-payload blob store.
Both shipped profiles run
noevictionwith an append-only file under/data: a blob is never dropped while a reader still holds it (every key carries a TTL, PROTOCOL.md BLOB-7, and the reconciler reclaims orphans, so that is what bounds memory), and a container restart replays the file.maxmemorystays capped at 4 GB so a stuck pipeline hits a refused write — a typed failure the publisher sees — before the node OOMs (the redis:7 default is unlimited memory); the container limit sits above it to leave headroom for allocator fragmentation.Dev profile (
None): the file lives on an emptyDir, the pod’s lifetime — the same standing as the dev NATS file store.Durable profile: the file lives on a PersistentVolumeClaim, and the Deployment uses a
Recreatestrategy, since a ReadWriteOnce claim cannot be held by the old and the new pod at once during a rollout.RedisProfile(persistence = 'none')still renders the old transport-only cache (no volume, nothing written) for an operator who asks for it.
- videoflow.deploy.infra.reused_infra(kubectl: str, namespace: str, need_redis: bool) ReusedInfra[source]
The Services a deploy into
namespacewould reuse rather than create.
- videoflow.deploy.infra.service_labels(kubectl: str, namespace: str, name: str) dict | None[source]
The labels of a Service in the namespace, or
Nonewhen there is no such Service.
- videoflow.deploy.infra.teardown_infra(kubectl: str, namespace: str, components: List[str], profile: BrokerProfile | None = None) None[source]
Deletes the given auto-provisioned components by ownership label. Best-effort (never raises). A stateful
profileadds the StatefulSet to the kinds deleted; the PersistentVolumeClaims either profile created are left in place (see the module docstring).
- videoflow.deploy.infra.wait_infra_ready(kubectl: str, namespace: str, created: List[str], timeout_secs: int = 120, profile: BrokerProfile | None = None) None[source]
Blocks until each freshly created infra workload rolls out; raises on timeout. Pass the same
profileasensure_infraso a stateful NATS is awaited as the StatefulSet it is.
videoflow.deploy.localinfra module
Auto-provisioning of the dev broker videoflow run-local needs when the user
doesn’t bring their own: a NATS JetStream container and, for the large-payload
blob store, a Redis container, both published on localhost. The local analogue of
videoflow.infra, which does the same job in-cluster for videoflow deploy.
Ownership rule, identical to infra: if something is already listening on the
port — docker compose up -d, a bare nats-server -js, a previous
--keep-infra run — it is reused as-is and never torn down. Only containers
this module started are returned as “created”, and only those are stopped later.
Dev-grade only: no persistence, no auth, host-published ports. Point --nats /
--blob-redis-url at a real broker for anything else.
- videoflow.deploy.localinfra.docker_available() bool[source]
True when a docker daemon is reachable.
- videoflow.deploy.localinfra.ensure_local_infra(need_redis: bool = True, nats_url: str | None = None, redis_url: str | None = None) Tuple[dict, List[str]][source]
Starts a dev NATS (and, when
need_redis, Redis) container unless something is already listening on the port.- Returns:
(urls, created)whereurlsmapsnats/redisto localhost URLs (redisis None when not needed) andcreatedlists only the components THIS call started — the only ones teardown may stop.
- Raises:
RuntimeErrorwhen a container must be started but docker is unavailable, or whendocker runfails.
- videoflow.deploy.localinfra.local_infra_urls() dict[source]
The localhost URLs workers use once the dev containers are up.
- videoflow.deploy.localinfra.port_open(host: str, port: int, timeout: float = 1.0) bool[source]
True when something accepts a TCP connection — the “is it already up?” probe.
videoflow.deploy.manifests module
Renders Kubernetes manifests for a compiled flow (a list of NodeSpec). One
workload per node, chosen by flow type:
- BATCH flow -> every node is a Job (each worker exits 0 when its upstream
end-of-stream drains, so the whole flow runs to completion).
- REALTIME flow -> a finite producer is a Job; every other node is a Deployment
(or a StatefulSet if partitioned) that stays up until the control-channel stop.
plus a per-node ConfigMap holding the node’s env, a shared ConfigMap for the NATS URL, and a default-deny-except-broker NetworkPolicy.
Manifests are built as plain dicts and serialized with yaml.dump rather than
text-templated, so the output is always structurally valid YAML.
The dict/dataclass split here is deliberate: anything that mirrors the Kubernetes
API schema (workloads, ConfigMaps, Services, pod specs, …) stays a dict, because
we do not own that schema and a typed mirror of it would be a second source of
truth that silently drifts from the real API — and would still have to become a
dict at the YAML boundary. Records this module owns outright (Mount,
ProvisionSplit) are dataclasses, since nothing external constrains their shape.
- class videoflow.deploy.manifests.Mount(name: str, host_path: str, container_path: str, read_only: bool, claim: str | None = None)[source]
Bases:
objectOne volume threaded into every node workload — a host path (and then also into the prepare / compile containers by
deploy.build.run_in_image) or a PersistentVolumeClaim. Produced byparse_mounts/parse_pvc_mounts; rendered into a volume + volumeMount by_pod_spec.- Arguments:
name: the volume name, unique within the pod (
vf-mount-<i>for a host path,vf-pvc-<i>for a claim).host_path: absolute path on the cluster node. Empty for a claim mount, which has no host side —
run_in_imageskips those.container_path: absolute path it appears at inside the container.
read_only: whether the container gets it read-only.
claim: the PersistentVolumeClaim name for a claim mount;
Nonefor a host path. The claim must exist in the flow’s namespace — it is what lets a multi-node cluster share a work directory that no node’s own filesystem holds (an RWX claim over NFS), where a hostPath would silently mount an empty directory on every node but one.
- claim: str | None = None
- container_path: str
- host_path: str
- name: str
- read_only: bool
- class videoflow.deploy.manifests.ProvisionSplit(provision: List[dict], worker: List[dict])[source]
Bases:
NamedTupleThe two apply phases of a flow’s manifests, in the order they must be applied.
A
NamedTuplerather than a plain@dataclass: the phases are genuinely ordered (phase 1 must be applied and awaited before phase 2), and existing callers unpack the result positionally, which keeps working unchanged.- Arguments:
provision: manifests the provision Job needs before any worker publishes.
worker: every node’s own ConfigMap/workload/service/PDB.
- provision: List[dict]
Alias for field number 0
- worker: List[dict]
Alias for field number 1
- videoflow.deploy.manifests.delete_resources(kubectl: str, namespace: str, flow_id: str, run_id: str | None = None) None[source]
Single source of truth for tearing down a flow’s Kubernetes resources: deletes every kind in
DELETABLE_KINDSmatching the flow-id label, scoped to one run whenrun_idis given (else every run of the flow). Best-effort (never raises).
- videoflow.deploy.manifests.dump_manifests(manifests: List[dict]) str[source]
Serializes a list of manifest dicts to a single multi-document YAML string.
- videoflow.deploy.manifests.flow_name(flow_id: str, *parts: object) str[source]
The name of a flow-wide resource shared by every run of the flow (today: the NetworkPolicy).
- videoflow.deploy.manifests.flow_spec_configmap(specs: List[NodeSpec], flow_id: str, run_id: str) dict[source]
ConfigMap holding the compiled specs as JSON, mounted by the provisioning init Job.
- videoflow.deploy.manifests.gpu_demand(specs: List[NodeSpec], default_resource: str | None = None) dict[str, int][source]
Whole-flow GPU demand per extended-resource name: every replica of a GPU node claims its own
gpu_countdevices exclusively, so this is the allocatable capacity the cluster must have for the flow to fully schedule. The single source of truth for deploy’s preflight andvideoflow explain— the two must never disagree.Deliberately a
dict[str, int]and not a dataclass: the keys are cluster-defined extended-resource names (nvidia.com/gpu,amd.com/gpu, a MIG profile, a vendor’s own), discovered from the specs at runtime. There is no fixed field set to name, and every caller iterates it against the cluster’s equally open-ended allocatable map, so this is a mapping in substance, not a record wearing a dict’s clothes.- Arguments:
specs: the compiled flow. Non-GPU nodes contribute nothing.
default_resource: extended-resource name for GPU nodes that don’t name their own (
--gpu-resource-name). Defaults tonvidia.com/gpu.
- Returns:
resource name -> total devices the flow needs at full scale. Empty when no node requests a GPU.
- videoflow.deploy.manifests.gpu_max_per_pod(specs: List[NodeSpec], default_resource: str | None = None) dict[str, int][source]
The largest single-pod claim per extended-resource name —
gpu_demand’s sibling and the other half of preflight’s input. Total demand answers “will the whole flow schedule”; this answers “can any single node host the biggest pod”: all of one replica’sgpu_countdevices must sit on one Kubernetes host, so a flow can satisfy the total and still never schedule (RFC 0003).Same
dict[str, int]-not-dataclass shape asgpu_demand, for the same reason: the keys are open-ended cluster-defined extended-resource names.- Arguments:
specs: the compiled flow. Non-GPU nodes contribute nothing.
default_resource: as on
gpu_demand.
- Returns:
resource name -> the largest
gpu_countany one replica requests. Empty when no node requests a GPU.
- videoflow.deploy.manifests.gpu_pod_claims(specs: List[NodeSpec], default_resource: str | None = None) dict[str, list[int]][source]
Every pod’s claim per extended-resource name — the third of
gpu_demand’s siblings and the input to preflight’s per-host packing check.gpu_demandsums the claims andgpu_max_per_podtakes the largest; both are bounds a fragmented pool can satisfy while still placing only some of the pods (free[3, 3]per node holds two of threegpu_count = 2replicas), so the packing check needs the claims themselves: one entry per replica —nb_taskscopies of the node’sgpu_count— in spec order.Same
dict-not-dataclass shape as its siblings, for the same reason.- Arguments:
specs: the compiled flow. Non-GPU nodes contribute nothing.
default_resource: as on
gpu_demand.
- Returns:
resource name -> one
gpu_countper replica. Empty when no node requests a GPU.
- videoflow.deploy.manifests.headless_service(spec: NodeSpec, flow_id: str, run_id: str) dict[source]
Headless Service backing a partitioned node’s StatefulSet (required for stable pod network identity/ordinals).
- videoflow.deploy.manifests.host_resources_for(spec: NodeSpec, resources: Mapping[str, Mapping[str, str]] | None) dict[str, str][source]
The host requests/limits one node’s container renders: the descriptor’s
spec.resources.cpu/memorydefaults, under the operator’s*entry, under the entry for this node (--resources). Empty when nobody asked.
- videoflow.deploy.manifests.k8s_name(*parts: object) str[source]
Joins parts into a DNS-1123-safe Kubernetes resource name.
- videoflow.deploy.manifests.nats_configmap(flow_id: str, run_id: str, nats_url: str, blob_redis_url: str | None = None, blob_ttl_seconds: int | None = None) dict[source]
- videoflow.deploy.manifests.network_policy(flow_id: str) dict[source]
Allows worker pods to talk to each other and out to the broker/DNS, and denies everything else ingress by default. Kept intentionally permissive on egress since the NATS/Redis services may live in another namespace.
- videoflow.deploy.manifests.node_configmap(spec: NodeSpec, flow_id: str, flow_type: str, run_id: str, envelope_version: int, profile_requests: dict | None = None, blob_reader_ids: List[str] | None = None, parent_replicas: List[int] | None = None, replica_slots: int | None = None) dict[source]
- videoflow.deploy.manifests.parse_mounts(values: List[str] | None) List[Mount][source]
Parses repeatable
--mountspecs intoMountrecords consumed by_pod_spec.- Arguments:
values: list of
/host/path:/container/path[:ro]strings. The single-path shorthand/path[:ro]mounts the same absolute path on both sides (what a flow compiled against local files needs, since the paths baked into node params must resolve identically in the pods).
- Returns:
a list of
Mount, in argument order.nameis only unique within one call, so a concatenation of twoparse_mountsresults repeats names — fine forrun_in_image(docker-vignores them), which is the only caller that concatenates.
- Raises:
ValueErroron a relative path or a malformed suffix.
- videoflow.deploy.manifests.parse_pvc_mounts(values: List[str] | None) List[Mount][source]
Parses repeatable
--mount-pvcspecs into claimMountrecords consumed by_pod_spec(apersistentVolumeClaimvolume + volumeMount on every node workload; the provision Job never touches node data and gets none).- Arguments:
values: list of
claim:/container/path[:ro]strings. The claim must already exist in the namespace the flow deploys to.
- Returns:
a list of
Mountwithclaimset andhost_pathempty, in argument order, namedvf-pvc-<i>— a namespace disjoint fromparse_mounts’svf-mount-<i>, so the two lists concatenate into one pod without a name collision.
- Raises:
ValueErroron a missing or invalid claim name, a relative path, or a malformed suffix.
- videoflow.deploy.manifests.pod_disruption_budget(spec: NodeSpec, flow_id: str, run_id: str) dict[source]
Keeps at least one replica of a multi-replica node available during voluntary disruptions (node drains, upgrades).
- videoflow.deploy.manifests.pod_mounts(mounts: List[Mount]) List[Mount][source]
The mounts a pod actually renders, from the full list the deploy collected.
The rule, deterministic and applied nowhere else: every claim mount is kept, and a hostPath mount whose container path lies at or under a claim mount’s container path is dropped from the pod. Such a path is served by the claim — a solution’s
work_dirstaged inside an RWX share, say — and a hostPath over it would shadow the claim on every node whose own filesystem does not hold that directory, mounting an empty root-owned directory where the artifacts should land. The dropped mount is still honoured bydeploy.build.run_in_image, which runs the prepare/compile containers on the host, where the path does resolve. Input order is preserved.- Arguments:
mounts: one
parse_mountsresult plus oneparse_pvc_mountsresult.
- Returns:
mountsitself (same order, every element) when it holds no claim; otherwise the filtered list.
- videoflow.deploy.manifests.provision_init_job(flow_id: str, run_id: str, flow_type: str, image: str, nats_cm_name: str, image_pull_policy: str = 'IfNotPresent', priority_class: str | None = None, stream_replicas: int = 1, profile_requests: dict | None = None) dict[source]
A one-shot Job that runs
videoflow.provisionto create all streams/durables before workers start (required so BATCH interest-retention streams don’t drop early messages). Runs onimage— any worker image has the framework + broker client installed.profile_requests(theVF_PROFILE_REQUESTS_JSONentry, RFC 0006 ENV-13) reaches it too, so the entrypoint admits the composition against the live broker before creating anything and verifies the streams it created carry the requested profiles; absent when none were made.Its pull policy must match the workers’: this Job runs first, so when it is the one that cannot pull, the flow fails before any worker starts and the only symptom is the provision-wait timeout. The same goes for
priority_class: a flow that yields to other tenants’ work must yield here too, or its provision pod is the one that cannot schedule.
- videoflow.deploy.manifests.render_manifests(specs: List[NodeSpec], flow_id: str, flow_type: str, nats_url: str, run_id: str, namespace: str = 'default', default_image: str | None = None, image_overrides: dict | None = None, blob_redis_url: str | None = None, blob_ttl_seconds: int | None = None, autoscaling: bool = False, max_replicas: int = 10, nats_monitoring_endpoint: str | None = None, envelope_version: int | None = None, provision_image: str | None = None, mounts: List[Mount] | None = None, gpu_runtime_class: str | None = None, gpu_mode: str = 'exclusive', gpu_resource_name: str | None = None, gpu_autoscaling: bool = False, image_pull_policy: str = 'IfNotPresent', supervision: SupervisionPolicy | None = None, priority_class: str | None = None, profile_requests: dict | None = None, stream_replicas: int = 1, rollout_policy: str | None = None, gpu_nodes: List[str] | None = None, resources: Mapping[str, Mapping[str, str]] | None = None) list[source]
Returns a list of manifest dicts for the whole flow. The caller decides whether to
yaml.dumpthem to files (CLI) or apply them via the API (engine).rollout_policy:
drainorsurgefor every Deployment (--rollout-policy, seerollout_strategy); None keeps the API default.gpu_nodes: hostnames every GPU pod is pinned to (
--gpu-nodes); None leaves placement to the pool label.resources: host requests/limits per node name (
*for all), merged over the descriptors’spec.resources.cpu/memory(--resources, seehost_resources_for).run_id: per-run identifier stamped into each node’s env (scopes broker streams).
blob_ttl_seconds: TTL override for offloaded payloads (
--blob-ttl-seconds, PROTOCOL.md BLOB-7);Nonelets workers pick the flow-type default.supervision: restart policy for every node workload, rendered into the Job
backoffLimit. The local engine honours the same object, which is what makes the two engines’ failure behaviour identical rather than merely similar.default_image: image ref used for any node that didn’t declare its own (
--image).image_overrides: mapping of node name to image ref (
--image-override).autoscaling: if True, emit a KEDA ScaledObject per processor node.
max_replicas: upper bound for autoscaled processors.
nats_monitoring_endpoint: NATS monitoring host:port for KEDA (defaults to
nats.<namespace>.svc:8222).envelope_version: wire version for the whole run. The only supported version is
4(the language-neutral protobuf wire); defaults to the ambientDEFAULT_ENVELOPE_VERSION, and an explicit incompatible pin is rejected.provision_image: image the one-shot provision Job runs on — must have videoflow + the broker client (i.e. a Python image), which a vendor
default_imagemay not be. Defaults todefault_image; set it explicitly (--provision-image) for flows whose default image is a non-Python vendor image.mounts:
Mountrecords fromparse_mounts(hostPath volumes) andparse_pvc_mounts(persistentVolumeClaimvolumes), concatenated — each becomes a volume + volumeMount on every node workload (not the provision Job, which never touches node data). A hostPath whose path lies under a claim’s is dropped from the pods: seepod_mounts.priority_class:
priorityClassNamefor every pod this flow creates, the provision Job included (--priority-class).Noneleaves the cluster’s default priority in place.gpu_runtime_class:
runtimeClassNameto put on GPU pods (--gpu-runtime-class). Needed where the NVIDIA container runtime is registered as an opt-in RuntimeClass rather than the node default — on k3s,nvidia. Without it such a pod schedules onto a GPU node and then runs with no device visible.gpu_mode: GPU strategy name (
'exclusive', the default — each GPU replica claims whole physical devices via the extended resource; seedeploy.gpu).gpu_resource_name: deploy-level extended-resource name for GPU claims (
--gpu-resource-name), for clusters advertising whole devices under a non-default name. Defaults tonvidia.com/gpu. Must denote whole physical devices — a MIG profile here is rejected (seevalidate_gpu_specs).gpu_autoscaling: emit KEDA ScaledObjects for GPU nodes too. Off by default: each autoscaled replica claims its own GPUs, so scaling to
max_replicascan demand more devices than the cluster has and strand pods Pending.image_pull_policy:
imagePullPolicyfor every rendered container, workers and the provision Job alike (--image-pull-policy). Defaults toIfNotPresent, which is what makes a locally built-and-loaded image actually run — seeDEFAULT_IMAGE_PULL_POLICY.- Raises:
ValueErrorif a node has no resolvable image (seevideoflow.deploy.images), ifimage_pull_policyis not a valid k8s policy, or if the wire settings are incompatible with the flow’s components.
- videoflow.deploy.manifests.rollout_strategy(rollout_policy: str) dict[source]
The Deployment
strategyfor a rollout policy (RUN-029, ALLOC-029):drain→Recreate(every old replica is gone before a new one starts — what a GPU node needs when its devices cannot be held by two generations at once);surge→RollingUpdatewith one extra replica and none unavailable, which needs spare capacity for that extra replica.
- videoflow.deploy.manifests.run_name(flow_id: str, run_id: str, *parts: object) str[source]
The name of one run’s resource:
vf-<flow>-<run>[-<node>][-<suffix>](RFC 0006 §10, run-scoped names). Two runs of one flow therefore neverkubectl applyover each other’s ConfigMaps or workloads; only the flow-wide NetworkPolicy (flow_name) is shared between them.
- videoflow.deploy.manifests.scaled_object(spec: NodeSpec, flow_id: str, run_id: str, nats_monitoring_endpoint: str, max_replicas: int) dict | None[source]
A KEDA ScaledObject that scales a processor Deployment on NATS JetStream consumer lag.
nb_tasksbecomes the floor (minReplicaCount); KEDA scales up towardmax_replicaswhen the node’s input stream backs up. Returns None for node kinds that aren’t autoscaled (producers, consumers).Requires KEDA installed in the cluster and the NATS monitoring endpoint (port 8222) reachable at
nats_monitoring_endpoint.
- videoflow.deploy.manifests.split_provision_manifests(manifests: List[dict], flow_id: str, run_id: str) ProvisionSplit[source]
Partitions rendered manifests into a
ProvisionSplitso the caller can apply provisioning first and wait for it before starting workers.The provision phase is everything the provision Job needs to create the broker streams/durables (including its EOS interest anchors) before any worker publishes: the broker + specs ConfigMaps, the NetworkPolicy, and the provision Job itself. The worker phase is every node’s ConfigMap/workload/service/PDB.
- Arguments:
manifests: the full rendered manifest list from
render_manifests.flow_id, run_id: what the provision-phase resource names are derived from.
- Returns:
a
ProvisionSplit(provision, worker), each preserving input order.
- videoflow.deploy.manifests.validate_gpu_specs(specs: List[NodeSpec], default_resource: str | None = None) None[source]
Hard build-time GPU validation — the checks that are provable from the specs alone, with no cluster in sight. Distinct from preflight (which compares against live cluster state and is skippable): what fails here can never work on any cluster, so it raises instead of warning.
Today that is one rule: a multi-device grant (
gpu_count > 1) against a MIG resource. MIG slices are hardware-isolated partitions — one CUDA process addresses one MIG instance and there is no P2P between instances — so the pod would receive devices its model cannot span (RFC 0003).- Raises:
ValueError: naming the node and the fix.
- videoflow.deploy.manifests.workload(spec: NodeSpec, flow_id: str, run_id: str, flow_type: str, image: str, nats_cm_name: str, mounts: List[Mount] | None = None, gpu_runtime_class: str | None = None, gpu_mode: str = 'exclusive', gpu_resource_name: str | None = None, image_pull_policy: str = 'IfNotPresent', supervision: SupervisionPolicy | None = None, priority_class: str | None = None, rollout_policy: str | None = None, gpu_nodes: List[str] | None = None, host_resources: Mapping[str, str] | None = None) dict[source]
- Arguments:
supervision: restart policy for this node’s workload. Rendered into the Job
backoffLimit; the local engine honours the same object in its supervisor, which is what keeps the two engines’ failure behaviour identical instead of merely similar.priority_class:
priorityClassNamefor the pod, or none (the cluster’s default priority).rollout_policy: how a Deployment replaces its pods on an update —
drain(Recreate: the old replicas stop before the new start; a GPU node whose devices cannot be held twice) orsurge(one extra replica at a time, none unavailable). None keeps the API default. Jobs and StatefulSets are not rolled this way.gpu_nodes: hostnames the node’s GPU pods may schedule on (
--gpu-nodes).host_resources:
cpu/memoryrequests andcpu_limit/memory_limitfor the worker container (--resources, descriptor defaults).
videoflow.deploy.mig module
MIG geometry knowledge and the mix-mode layout solver (RFC 0004).
The mix GPU strategy turns declared demands — spanners needing whole physical
devices (gpu_count) and sharers needing an isolated fraction of one device
(gpu_memory_gib) — into a concrete per-card plan: which cards stay whole,
which get MIG-partitioned, and into which profiles. Everything in this module is
pure and deterministic: it computes against an inventory handed to it and never
talks to a cluster, which is what makes the solver exhaustively unit-testable.
The strategy (deploy/gpu.py) owns fetching the inventory and applying the
result; deploy/cluster.py owns reading the inventory off GPU Feature
Discovery labels.
A card holds a multiset of profiles only when it can place it: each profile
occupies a fixed width on the card’s memory-slice grid and may start only at
the positions NVIDIA’s placement table allows (nvidia-smi mig -lgipp; the MIG
user guide’s “GPU instance profile placements”), so compute slices, memory and
legal start positions are three separate budgets. Two 3g.20gb on an A100
fill the whole grid (widths 4 at starts 0 and 4) and leave no room for a
1g.5gb although the compute slices (3 + 3 + 1 = 7) would fit — a layout the
totals accept and nvidia-mig-parted would refuse after the operator has
already been told it is feasible (ALLOC-002). A profile without placement data
(a third-party table) is checked by totals only. nvidia-mig-parted remains
the executor: it chooses the instance positions; the solver only promises that a
legal assignment exists.
- class videoflow.deploy.mig.CardPlan(node: str, card_index: int, profiles: Dict[str, int] | None = None)[source]
Bases:
objectWhat one physical card becomes under the layout: whole (
profilesis None — spanner or undeclared-demand territory) or MIG’d intoprofiles(profile name -> instance count).- card_index: int
- property is_mig: bool
- node: str
- profiles: Dict[str, int] | None = None
- class videoflow.deploy.mig.GpuLayout(cards: List[CardPlan] = <factory>, spec_resources: Dict[str, str]=<factory>, slice_demand: Dict[str, int]=<factory>)[source]
Bases:
objectThe solver’s answer: the per-card plan, each sharer spec’s resolved extended resource, and the total slice demand per resource (what the device plugin must advertise once the geometry is applied — preflight compares against it).
- mig_nodes() List[str][source]
Nodes with at least one MIG’d card, sorted — the nodes whose geometry
prepare()must apply.
- slice_demand: Dict[str, int]
- spec_resources: Dict[str, str]
- exception videoflow.deploy.mig.LayoutError[source]
Bases:
ValueErrorNo feasible MIG layout exists for the flow’s demands on this inventory. The message names the demand that failed and the fix.
- class videoflow.deploy.mig.MigProfile(name: str, memory_gib: float, slices: int, max_per_gpu: int, width: int = 0, placements: tuple[int, ...] = ())[source]
Bases:
objectOne MIG profile a card family supports:
1g.10gbneeds 1 of the card’s compute slices and yields a 10 GiB instance, at mostmax_per_gpuper card.- Arguments:
width: how many positions of the card’s memory-slice grid an instance occupies (
nvidia-smi mig -lgippprints{starts}:width); 0 when the table carries no placement data and the profile is checked by totals only.placements: the grid positions an instance may start at, from the same query — a
3g.20gbon an A100 starts at 0 or 4, never at 2.
- max_per_gpu: int
- memory_gib: float
- name: str
- placements: tuple[int, ...] = ()
- property resource: str
The Kubernetes extended resource the device plugin advertises for it.
- slices: int
- width: int = 0
- class videoflow.deploy.mig.MigTable(family: str, match_substrings: List[str], total_slices: int, profiles: List[MigProfile], grid: int = 0)[source]
Bases:
objectThe MIG geometry of one GPU family: how to recognize it from the GFD product label, how many compute slices a card has, the profiles it offers, and the size of the memory-slice grid the profiles’ placements refer to (
grid; 0 when the table carries no placement data).- family: str
- grid: int = 0
- match_substrings: List[str]
- placement_checked() bool[source]
Whether every profile carries placement data, so
placeis a real check.
- profiles: List[MigProfile]
- smallest_profile_for(memory_gib: float) MigProfile | None[source]
- total_slices: int
- class videoflow.deploy.mig.NodeInventory(name: str, product: str, card_count: int, memory_gib_per_card: float, time_sliced: bool = False, mig_config: str | None = None, mig_partitioned: bool = False, owner: str | None = None, used_units: Dict[str, int]=<factory>, mig_allowed: bool = True, occupancy_known: bool = True, allocatable: Dict[str, int]=<factory>, dra_owned: bool = False)[source]
Bases:
objectOne GPU node’s inventory as read off node labels and allocatable resources (
cluster.gpu_inventory): the physical facts (product, card count, memory) plus the sharing/ownership state the mix strategy filters on before solving. The fields pastmemory_gib_per_cardare facts, not decisions — this module never excludes a node;gpu.MixGpupartitions the inventory into usable and excluded nodes and hands the solver only the usable ones.- allocatable: Dict[str, int]
The node’s advertised extended resources (
status.allocatable, integer quantities only): what a static MIG slice request is matched against.
- card_count: int
- dra_owned: bool = False
no device-plugin planning and never videoflow’s managed-MIG hooks (ALLOC-018).
- Type:
The node is owned by a DRA driver (it publishes ResourceSlices)
- memory_gib_per_card: float
- mig_allowed: bool = True
Whether the solver may plan MIG geometry here. The mix strategy clears it on busy nodes: repartitioning destroys running workloads, but whole-card spanner claims are scheduler-accounted and remain safe.
- mig_config: str | None = None
The node’s
nvidia.com/mig.configlabel value, None when absent.
- mig_partitioned: bool = False
The node already carries carved MIG geometry (advertises
nvidia.com/mig-*or its product names a MIG profile) — GFD’sgpu.countthen counts only non-MIG cards, socard_indexno longer maps to physical positions.
- name: str
- occupancy_known: bool = True
False when the pod listing behind
used_unitscould not be read. Unknown occupancy is not zero occupancy: the mix strategy refuses to plan on such a node rather than repartition cards another tenant may hold.
- owner: str | None = None
videoflow.io/gpu-ownerlabel value — the flow that MIG’d this node.
- product: str
- time_sliced: bool = False
Units are shares of a device (
-SHAREDproduct, time-slicing strategy, or replicas > 1) — physical card positions are not addressable.
- used_units: Dict[str, int]
Extended-resource units currently requested by running pods, per resource.
- videoflow.deploy.mig.layout_to_mig_parted_config(layout: GpuLayout, config_prefix: str = 'videoflow') str[source]
The
nvidia-mig-partedconfig implementinglayout, as YAML text — one named config per MIG’d node (<prefix>-<node>), covering every card on that node so untouched cards are explicitlymig-enabled: false. This is both whatprepare()applies through the GPU Operator’s MIG manager and what preflight prints for manual application when no MIG manager is present.Emitted by string assembly rather than a YAML library on purpose: the shape is fixed and tiny, and this module must import cleanly without the optional
yamlextra.
- videoflow.deploy.mig.mig_table_for_product(product: str) MigTable | None[source]
The registered MIG table matching a GFD
nvidia.com/gpu.productlabel, or None for a card with no MIG support (consumer GPUs, unknown families).
- videoflow.deploy.mig.place(profiles: Sequence[MigProfile], grid: int) List[int] | None[source]
A legal, non-overlapping start position per profile on a
grid-position memory-slice grid, or None when the multiset cannot be placed. Exhaustive (a card has at most eight positions and seven instances) and order-free: the caller’s profile order never decides feasibility, only which of the equivalent assignments is returned. Widest profiles are tried first so the search prunes early; the result is reported in the caller’s order.
- videoflow.deploy.mig.register_mig_table(table: MigTable) None[source]
Registers a GPU family’s MIG geometry. Same extension shape as the GPU strategy and cluster-flavor registries: a new card family is one call, not an edit to the solver.
- videoflow.deploy.mig.solve_layout(inventory: List[NodeInventory], specs: List[NodeSpec]) GpuLayout[source]
Computes a feasible card layout for the flow’s GPU demands, or raises
LayoutErrornaming the demand that cannot be placed and the fix.The inventory handed in is assumed to be already filtered to usable nodes — the mix strategy excludes nodes owned by other flows, time-sliced or pre-MIG’d nodes, and clears
mig_allowedon busy ones before calling this.Deterministic (sorted walks throughout) so re-running against the same inventory reproduces the same geometry — which is what makes
prepare()idempotent andexplaintruthful about what deploy will do. Placement order:Spanners first — every replica of a GPU spec with no declared
gpu_memory_gibtakesgpu_countwhole cards on one node (gpu_countdefaults to 1: the undeclared-demand node is the degenerate spanner). Rigid demand, so it goes first, largest claims first.Sharers packed second — every replica of a spec with
gpu_memory_gibtakes one MIG instance of the smallest fitting profile, first-fit onto already-open MIG cards, opening new cards as needed. All of one spec’s replicas use one profile on one card family, so the spec resolves to a single extended-resource name.
videoflow.deploy.solution module
Solution conventions for one-command deploys. A “solution” is a graph module shipped with optional sibling files:
config.template.yaml— a valid config plus two extension blocks stripped on write:x-questions(inputs deploy asks for interactively when no config exists) andx-mounts(paths from the resolved config that must be hostPath-mounted into prep containers and worker pods).
prepare.py— an idempotent prep hook deploy runs inside the solution image before compiling (its outputs get baked into the compiled specs).
x-questions entries: {key, prompt, type, default, choices, item_key,
item_value}. key is a dotted path into the config (digit segments index
int-keyed maps). Types: str (default), int, float, choice (with
choices), path (one filesystem path, validated and absolutized), and
paths (comma-separated paths expanded into a mapping via item_key,
e.g. 'cam{i}', and item_value, e.g. {video: '{path}'}). That set is
extensible: see register_question_type.
x-mounts entries are path templates: '{cameras.*.video}:ro' (dotted
lookup into the resolved config, * fans out), '{work_dir}',
'~/.videoflow:/root/.videoflow'. A single path resolves to a same-path
hostPath mount (host and container see the same absolute path — required
because the paths baked into node params at compile time must resolve
identically in the pods); a host:container pair maps them explicitly
(e.g. the operator’s home caches onto the container root’s). A
pvc:<claim>:<path>[:ro] entry mounts an existing PersistentVolumeClaim at
<path> in the pods instead — the form for data that lives in the cluster
rather than on the operator’s machine (a shared model cache, an RWX work
directory on a multi-node cluster); <path> takes the same {dotted}
lookup and resolves relative to the solution directory like the others.
- videoflow.deploy.solution.PVC_MOUNT_PREFIX = 'pvc:'
Marks an
x-mountsentry (and a resolved mount spec) as a claim mount.
- videoflow.deploy.solution.ask_questions(questions: ~typing.List[dict], base_dir: str, input_fn: ~typing.Callable[[str], str] = <built-in function input>) dict[source]
Prompts for each x-question on the terminal; returns {dotted_key: typed_value}.
- Raises:
ValueError: a question names an unregistered type (checked before prompting — see
validate_question_types).
- videoflow.deploy.solution.ensure_config(graph_dir: str, config_arg: str | None = None, interactive: bool = True, input_fn: ~typing.Callable[[str], str] = <built-in function input>) str | None[source]
The config file deploy should use: an explicit
--config, an existingconfig.yamlnext to the graph, or — when the solution ships a template — one generated by asking the template’s questions. None when the solution has no config convention at all.- Raises:
SystemExitlisting every question when a config must be generated but the session is non-interactive.
- videoflow.deploy.solution.find_prepare(graph_dir: str) str | None[source]
The solution’s
prepare.pyhook, or None when it ships none.
- videoflow.deploy.solution.prepare_command(config_path: str | None = None, python_exe: str = 'python') List[str][source]
The argv for the prepare hook, run with the solution directory as cwd.
- videoflow.deploy.solution.register_question_type(qtype: str, coercer: Callable[[dict, str, str], Any]) None[source]
Registers a coercer for an
x-questionstype, called ascoercer(question, answer, base_dir)wherequestionis the raw x-questions entry (so a coercer can read its own extra keys, aschoicereadschoices),answeris the operator’s raw string, andbase_diris the solution directory that relative paths resolve against.A coercer signals a bad answer by raising
ValueError; the prompt loop catches it, shows the message, and re-asks.- Arguments:
qtype: the
typestring used inconfig.template.yaml.coercer: callable returning the coerced config value.
- videoflow.deploy.solution.registered_question_types() list[source]
The
x-questionstype names currently registered, sorted.
- videoflow.deploy.solution.render_config(template: dict, answers: dict) str[source]
The final config YAML: template body with answers applied and x-* blocks stripped.
- videoflow.deploy.solution.resolve_mounts(template: dict | None, config: dict, graph_dir: str) List[str][source]
Expands the template’s
x-mountsagainst the resolved config into mount specs:/abs/path[:ro]and/host:/container[:ro]formanifests.parse_mounts, andpvc:<claim>:/abs/path[:ro]formanifests.parse_pvc_mounts(split_mount_specsseparates the two). Relative config values resolve againstgraph_dir(matching a prep/compile container whose workdir is the graph dir);~expands.- Raises:
ValueError: a
pvc:entry names no claim or no path.
- videoflow.deploy.solution.run_prepare_local(graph_dir: str, config_path: str | None = None) bool[source]
Runs the solution’s
prepare.pyon this host, with the solution directory as the working directory so itsimport commonresolves. Used byrun-local(whose workers are local anyway) and asdeploy’s fallback when there is no image to run it in.- Returns:
False when the solution ships no hook (nothing was run).
- Raises:
subprocess.CalledProcessErrorwhen the hook exits non-zero.
- videoflow.deploy.solution.split_mount_specs(specs: List[str]) tuple[List[str], List[str]][source]
Partitions
resolve_mountsoutput into(host_specs, claim_specs): the host-path specs asmanifests.parse_mountstakes them, and the claim specs with theirpvc:prefix stripped, asmanifests.parse_pvc_mountstakes them. Order is preserved within each list.
- videoflow.deploy.solution.validate_question_types(questions: list | None) None[source]
Checks every question’s
typeis registered, before any prompting starts.Done up front deliberately: the prompt loop treats
ValueErroras a bad answer and re-asks, so an unregistered type discovered mid-loop would re-prompt forever over something the operator cannot fix by typing.- Raises:
ValueError: a question names an unregistered type; the message names the offending key, the known types, and how to add one.