# Requesting CPUs and GPUs per call

`options()` sets what a particular call needs:

```python
@eugo.hpc.distribute
def train_step(batch):
    return run_step(batch)

# This call wants a GPU and four cores
future = train_step.options(num_cpus=4, num_gpus=1)(batch)
```

## Why the call site, not the definition

The same function is often invoked in different circumstances. A quick smoke test over ten records does
not need a GPU; the production run over ten million does.

Putting requirements on the definition would force the expensive shape everywhere:

```python
# Defined once
@eugo.hpc.distribute
def process(chunk):
    return transform(chunk)

# Cheap validation pass: default resources
sample = eugo.hpc.get([process(c) for c in chunks[:10]])

# Full run: heavier resources
futures = [process.options(num_cpus=8)(c) for c in chunks]
```

One definition, two resource profiles.

## What the scheduler does with it

The [scheduler](/glossary/scheduler) places each [task](/glossary/task) on a node that can satisfy its request. A task asking for a GPU only
runs on a node that has one free; if none does, it waits.

That waiting is the failure mode worth recognizing. Tasks that queue indefinitely usually mean a
request nothing can satisfy, most often a GPU request on a [cluster](/glossary/cluster) whose nodes have none.

## Over-requesting is expensive twice

```python
# Bad: every task demands a GPU, including the ones doing string parsing
futures = [step.options(num_gpus=1)(x) for x in items]
```

Two costs:

1. **Queueing.** CPU-only tasks wait behind GPU availability they never needed. Throughput drops even
   though the cluster has idle CPU capacity.
2. **Money.** GPU nodes carry a premium. Paying it for work that never touches the GPU is pure waste.

In a mixed pipeline, request GPUs only for the stages that use them:

```python
# Parsing: CPU
parsed = [parse.options(num_cpus=2)(f) for f in files]

# Numeric transform: GPU earns its cost here
transformed = [heavy_math.options(num_gpus=1)(p) for p in parsed]
```

## Fractional GPUs

Several small tasks can share one device:

```python
futures = [infer.options(num_gpus=0.25)(x) for x in items]
```

Four such tasks fit on one GPU. Useful for inference, where a single request rarely saturates the
device. Be careful with memory. Fractional allocation divides scheduling, not VRAM, so four tasks each
wanting most of the card will fail.

## Sensible defaults

Without `options()`, a task gets a default allocation of roughly one CPU and no GPU. That is correct for
most work, and reaching for `options()` on every call is a sign of over-tuning.

Set it when you know a specific reason: the task is multi-threaded internally, it needs a GPU, or it
needs more memory than one core's share.

## Verify rather than assume

Requesting a GPU is not proof one was used. Confirm inside the task:

```python
@eugo.hpc.distribute
def check():
    import torch
    return {
        "cuda": torch.cuda.is_available(),
        "device": torch.cuda.get_device_name(0) if torch.cuda.is_available() else None,
    }

print(eugo.hpc.get(check.options(num_gpus=1)()))
```

[GPU acceleration on Eugo](/courses/gpu-acceleration-on-eugo) covers verification and [transfer costs](/glossary/transfer-cost) in
depth, including the cases where a GPU is measurably slower than its own machine's CPU.

---

**Video:** [Requesting GPUs per task](/videos/requesting-gpus-per-task). The same material, with a transcript.

---

Source: https://university.eugo.io/lesson/distributed-python-with-eugo-hpc/resource-options
