Worker affinity
Route workflow executions to specific workers by declaring label requirements in your workflow definition.
Worker affinity lets you tell Flux that a workflow should only run on workers with specific capabilities. Attach labels to workers at startup, then declare matching requirements in your workflow definition. The scheduler routes executions accordingly.
How it works
Labels are static strings attached to a worker process when it starts. A label is a key=value pair — both key and value are strings. A workflow declares an affinity dict; when an execution is scheduled, the dispatcher checks each connected worker’s label set and only sends the execution to workers whose labels satisfy every entry in the affinity dict.
Matching follows three rules:
- A worker matches only if it has all required labels — every key in the affinity dict must be present with the exact value.
- Extra labels on the worker are ignored — a worker with
role=training, gpu=a100, zone=us-1amatches{"role": "training", "gpu": "a100"}without issue. - If no running worker matches at dispatch time, the execution stays in
SCHEDULEDstate until a matching worker connects.
Starting a worker with labels
Pass one --label flag per label at startup:
flux start worker gpu-worker-1 \
--label role=training \
--label gpu=a100 \
--label env=production
Labels are set once at process start and cannot change while the worker runs. To change labels, restart the worker with updated flags.
To see what labels a running worker has:
flux worker list
flux worker show gpu-worker-1
Declaring affinity in a workflow
Use workflow.with_options(affinity=...) to set the affinity constraint:
from flux import workflow, ExecutionContext
@workflow.with_options(affinity={"role": "training", "gpu": "a100"})
async def train_model(ctx: ExecutionContext[str]):
# Only dispatches to workers started with
# --label role=training --label gpu=a100
dataset = ctx.input
return f"Model trained on {dataset}"
The affinity value is a plain dict[str, str]. All values must be strings — match this to how you specify label values on the worker side.
A workflow without affinity runs on any available worker:
@workflow
async def generic_task(ctx: ExecutionContext[str]):
# No affinity — any worker can claim this
return ctx.input
Combining affinity with other options
affinity composes with namespace, requests, schedule, and other with_options parameters:
from flux import workflow, ExecutionContext
from flux.domain.resource_request import ResourceRequest
@workflow.with_options(
namespace="ml",
affinity={"role": "training", "gpu": "a100", "env": "prod"},
requests=ResourceRequest(gpu=1, memory="16Gi"),
)
async def large_training_run(ctx: ExecutionContext[str]):
# Requires: labels match AND >= 1 GPU AND >= 16 GiB free memory
return f"Training: {ctx.input}"
When both affinity and requests are set, a worker must satisfy both. Labels are checked first; then resource availability is checked against the matched workers.
Practical example: routing to a browser worker
Some workflows need a worker with specific tools installed — a headless browser, for instance. Start a dedicated worker:
flux start worker browser-worker \
--label role=browser-agent \
--label browser=chromium
Then declare a workflow that targets it:
from flux import workflow, ExecutionContext, task
@task
async def fetch_page(url: str) -> str:
# This task runs on a worker where browser tools are available
return f"Page content from {url}"
@workflow.with_options(affinity={"role": "browser-agent", "browser": "chromium"})
async def scrape_site(ctx: ExecutionContext[str]):
url = ctx.input
content = await fetch_page(url)
return content
Without the affinity constraint, the scheduler might send scrape_site to a generic worker without browser tools, and the task would fail at runtime.
Resume behavior
When a paused workflow resumes, Flux prefers the original worker. If that worker is no longer connected, any worker matching the affinity constraints can claim it. A worker without the required labels cannot pick up a resumed execution.
Affinity vs resource requests
Labels (affinity) | Resource requests (requests) | |
|---|---|---|
| Question | What kind of worker? | How much capacity? |
| Examples | role=training, gpu=a100, env=prod | cpu=4, memory="8Gi", gpu=1 |
| Mutability | Fixed at worker startup | Checked per execution |
| Matching | All keys must be present with exact values | Resources must be available at claim time |
Use labels for stable capabilities — GPU model, environment type, installed tooling. Use resource requests for quantities that vary. See Resource requests for details.
Beyond hard constraints
Affinity decides which workers can run a workflow. To rank the eligible workers — by latency, load, locality, or custom metrics — add a scoring policy on top with workflow.with_options(routing=score(...)). Scoring is a soft preference evaluated after the affinity and resource filters, so it can never route around a hard constraint. See Dynamic routing.
What’s next
- Resource requests — declare CPU, memory, and GPU requirements alongside labels
- Dynamic routing — rank eligible workers with a scoring policy
- Namespaces — isolate workflows by team or environment
- Workers — worker lifecycle, registration, and the module cache