Skip to content

Commit 3e25316

Browse files
Copilottoby-colemanchrisk314
authored
feat: Implement resource requirements for components (#194)
# Summary Adds resource specification support for components to enable Ray actor resource allocation and Tuner placement group optimization. Resources can be declared as class attributes on Component definitions (recommended) or overridden at instantiation time. # Changes **Core Schema** - Added `Resource` Pydantic class in `plugboard_schemas.component` with fields: `cpu`, `gpu`, `memory` (integer bytes), `resources` (custom dict) - Supports full SI prefix notation: - Decimal SI: `n`, `u`, `m`, `k`, `M`, `G`, `T`, `P`, `E` (e.g., `"250m"` → 0.25, `"5k"` → 5000) - Binary: `Ki`, `Mi`, `Gi`, `Ti`, `Pi`, `Ei` (e.g., `"10Mi"` → 10,485,760 bytes) - Default: `cpu=0.001`, others zero - Implements `to_ray_options()` for Ray actor configuration - Memory stored as integer bytes for precision **Component Integration** - Added `resources` class attribute to `Component` for declaring default resource requirements - Extended `Component.__init__()` with optional `resources` parameter to override class-level defaults - Updated `ComponentArgsDict`, `ComponentArgsSpec` for YAML support - Resources auto-exported and reconstructed from configuration - Each component instance creates an instance-scoped copy of resources **Ray Execution** - Modified `RayProcess._create_component_actor()` to apply resource requirements via `ray.remote(**ray_options)` - Falls back to default `Resource()` when unspecified - Import statements organized at top of file **Tuner Optimization** - Added `Tuner._calculate_placement_bundles()` to aggregate component resources - Creates `PlacementGroupFactory` with tuner overhead (0.5 CPU) + component bundle - Import statements organized at top of file **Documentation** - Updated `Component` class docstring to document the `resources` class attribute - Added `Component.__init__()` docstring explaining resource parameter behavior - Added comprehensive section in `docs/examples/tutorials/running-in-parallel.md` explaining both approaches: - Class-level resource declaration (recommended) - Constructor override for flexibility - Python and YAML examples demonstrating both resource specification approaches **Usage Examples** Class-level declaration (recommended): ```python from plugboard.component import Component from plugboard.schemas import Resource class CPUIntensiveTask(Component): io = IO(inputs=["x"], outputs=["y"]) resources = Resource(cpu=2.0) # Declare at class level async def step(self) -> None: # Your logic here pass # Uses class-level resources component = CPUIntensiveTask(name="my-task") ``` Constructor override: ```python # Override class-level resources when needed component = CPUIntensiveTask( name="my-task", resources=Resource(cpu=4.0) # Override to 4 CPUs ) ``` YAML config: ```yaml components: - type: my.CPUIntensiveTask args: name: my-task # Optionally override class-level resources resources: cpu: 4.0 memory: "512Mi" ``` **Tests** - Unit tests: Resource parsing with all SI prefixes, validation, Ray conversion - Integration tests: Component/ProcessSpec/Tuner scenarios, class-level resource declaration - Manual verification (full test suite requires Ray dependencies) <!-- START COPILOT ORIGINAL PROMPT --> <details> <summary>Original prompt</summary> > > ---- > > *This section details on the original issue you should resolve* > > <issue_title>feat: Implement resource requirements for components</issue_title> > <issue_description>### Summary > > Users would like to be able to specify custom resource requirements on `Component` objects. These can be passed to Ray when a `RayProcess` is used or a `Tuner` object is run. > > Requirements: > * Must be able to specify `Resource` requirements for `cpu`, `gpu`, `memory`, `resources`. These should be defined as a Pydantic class in `plugboard_schemas`. > * Must be able to specify resources as a numerical value, or as a string (to be validated by Pydantic), e.g. `250m` for 0.25 or `10Mi` for 1024 * 1024 * 10. `resources` should be a dictionary of string key, numerical value pairs. > * Must be able to convert these requirements to their Ray equivalents, see https://docs.ray.io/en/latest/ray-core/api/doc/ray.actor.ActorClass.options.html, i.e. `cpu = "250m"` gets converted to `num_cpus=0.25`. > * Must be able to pass a `Resource` to a `Component` when instantiating it. This will require changes to the `Component` and its schema, so that we can specify resource requirements in YAML config. > * The default requirement should be `{"cpu": 0.001}`, to be used if the user does not provide anything. > * Resource requirements should be passed to the actor options in `RayProcess`. > * They should be used in the resource placement group inside `Tuner`. > > ### Example > > _No response_ > > ### Alternatives > > _No response_</issue_description> > > ## Comments on the Issue (you are @copilot in this section) > > <comments> > </comments> > </details> <!-- START COPILOT CODING AGENT SUFFIX --> - Fixes #193 <!-- START COPILOT CODING AGENT TIPS --> --- 💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey). --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: toby-coleman <13170610+toby-coleman@users.noreply.github.com> Co-authored-by: Toby Coleman <toby@tobycoleman.com> Co-authored-by: Chris Knight <chrisk314@gmail.com> Co-authored-by: chrisk314 <2366658+chrisk314@users.noreply.github.com>
1 parent 229a08f commit 3e25316

17 files changed

Lines changed: 850 additions & 23 deletions

File tree

.github/workflows/lint-test.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ jobs:
7575
test-unit:
7676
name: Tests - unit
7777
runs-on: ubuntu-latest
78-
timeout-minutes: 5
78+
timeout-minutes: 8
7979
strategy:
8080
matrix:
8181
python_version: [3.12, 3.13]
@@ -113,7 +113,7 @@ jobs:
113113
test-integration:
114114
name: Tests - integration
115115
runs-on: ubuntu-latest
116-
timeout-minutes: 6
116+
timeout-minutes: 10
117117
strategy:
118118
matrix:
119119
python_version: [3.12, 3.13]
@@ -157,7 +157,7 @@ jobs:
157157
test-integration-tuner:
158158
name: Tests - integration:tuner
159159
runs-on: ubuntu-latest
160-
timeout-minutes: 5
160+
timeout-minutes: 8
161161
strategy:
162162
matrix:
163163
python_version: [3.12, 3.13]

docs/api/schemas/schemas.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
::: plugboard.schemas
1+
::: plugboard_schemas

docs/examples/tutorials/running-in-parallel.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,61 @@ Specifying the process type and channel builder type in the YAML is the only cha
7070

7171
1. Tell Plugboard to use a [`RayProcess`][plugboard.process.RayProcess] instead of the default [`LocalProcess`][plugboard.process.LocalProcess].
7272
2. Also change the connector builder to [`RayConnector`][plugboard.connector.RayConnector], which will build [`RayChannel`][plugboard.connector.RayChannel] objects when creating the `Process`.
73+
74+
## Specifying resource requirements
75+
76+
When running components on Ray, you can specify resource requirements for each component to control how Ray allocates computational resources. This is particularly useful when you have components with different resource needs (e.g., CPU-intensive vs GPU-intensive tasks) and you are running on a Ray cluster.
77+
78+
!!! tip
79+
Normally Ray will start automatically when you are using Plugboard locally. If you want to start a separate Ray instance, for example so that you can control the configuration options, you can launch it from the [CLI](https://docs.ray.io/en/latest/ray-core/starting-ray.html). For example, this command will start a Ray instance with enough resources to run the example below.
80+
81+
```sh
82+
uv run ray start --head --num-cpus=4 --num-gpus=1 --resources='{"custom_hardware": 5}'
83+
```
84+
85+
### Declaring resources at component definition
86+
87+
The recommended way to specify resource requirements is to declare them as a class attribute when defining your component. This makes the [`Resource`][plugboard.schemas.Resource] requirements explicit and part of the component's definition:
88+
89+
```python
90+
--8<-- "examples/tutorials/004_using_ray/resources_example.py:definition"
91+
```
92+
93+
1. Declare resources when defining the class.
94+
95+
### Overriding resources at instantiation
96+
97+
You can also override resource requirements when creating component instances. This is useful when you want to use the same component class with different resource requirements:
98+
99+
```python
100+
--8<-- "examples/tutorials/004_using_ray/resources_example.py:77:77"
101+
```
102+
103+
1. Pass a [`Resource`][plugboard.schemas.Resource] object to override the CPU requirements for this component.
104+
105+
### Example
106+
107+
For example, you can specify [`Resource`][plugboard.schemas.Resource] requirements like this when defining components:
108+
109+
```python
110+
--8<-- "examples/tutorials/004_using_ray/resources_example.py:resources"
111+
```
112+
113+
1. Override the resource requirement on this instance.
114+
2. Use resources specified in class definition.
115+
3. Use default resources.
116+
117+
Or override them in YAML configuration:
118+
119+
```yaml
120+
--8<-- "examples/tutorials/004_using_ray/resources-example.yaml:11:"
121+
```
122+
123+
1. Override DataProducer to require 1 CPU (instead of the default 0.001).
124+
2. CPUIntensiveTask already declares cpu: 2.0 at the class level, so this matches the class definition.
125+
3. Add 512MB memory requirement to CPUIntensiveTask (extending the class-level resources).
126+
4. Requires 0.5 CPU, specified in Kubernetes-style format (500 milli CPUs). This matches the class-level declaration.
127+
5. Requires 1 GPU, matching the class-level declaration.
128+
6. Add a custom resource called `custom_hardware`. This needs to be specified in the configuration of your Ray cluster to make it available.
129+
130+
See the [Ray documentation](https://docs.ray.io/en/latest/ray-core/scheduling/resources.html) for more information about specifying resource requirements.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Example YAML configuration with resource requirements
2+
#
3+
# This configuration demonstrates how to specify resource requirements
4+
# for components in a Plugboard process. Resources can be specified as:
5+
# - Numerical values: cpu: 2.0
6+
# - Milli-units: cpu: "250m" (equals 0.25)
7+
# - Memory units: memory: "100Mi" (equals 100 * 1024 * 1024 bytes)
8+
#
9+
# Note: Resources can be declared at the component class level (recommended)
10+
# or overridden in the YAML configuration as shown below.
11+
plugboard:
12+
process:
13+
type: plugboard.process.RayProcess
14+
connector_builder:
15+
type: plugboard.connector.RayConnector
16+
args:
17+
name: resource-example-process
18+
components:
19+
- type: examples.tutorials.004_using_ray.resources_example.DataProducer
20+
args:
21+
name: producer
22+
iters: 10
23+
resources:
24+
cpu: 1.0 # (1)!
25+
- type: examples.tutorials.004_using_ray.resources_example.CPUIntensiveTask
26+
args:
27+
name: cpu-task
28+
# CPUIntensiveTask has class-level resources (cpu: 2.0)
29+
# Override to use more memory
30+
resources:
31+
cpu: 2.0 # (2)!
32+
memory: "512Mi" # (3)!
33+
- type: examples.tutorials.004_using_ray.resources_example.GPUTask
34+
args:
35+
name: gpu-task
36+
# GPUTask has class-level resources (cpu: "500m", gpu: 1)
37+
# Can override or extend with custom resources
38+
resources:
39+
cpu: "500m" # (4)!
40+
gpu: 1 # (5)!
41+
resources:
42+
custom_hardware: 2 # (6)!
43+
connectors:
44+
- source: producer.output
45+
target: cpu-task.x
46+
- source: cpu-task.y
47+
target: gpu-task.data
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""Example demonstrating resource requirements for components in RayProcess."""
2+
3+
# fmt: off
4+
import asyncio
5+
import typing as _t
6+
7+
import ray
8+
9+
from plugboard.component import Component, IOController as IO
10+
from plugboard.connector import RayConnector
11+
from plugboard.process import RayProcess
12+
from plugboard.schemas import ComponentArgsDict, ConnectorSpec, Resource
13+
14+
15+
# --8<-- [start:definition]
16+
class CPUIntensiveTask(Component):
17+
"""Component that requires more CPU resources.
18+
19+
Resource requirements are declared as a class attribute.
20+
"""
21+
22+
io = IO(inputs=["x"], outputs=["y"])
23+
resources = Resource(cpu=2.0) # (1)!
24+
25+
async def step(self) -> None:
26+
"""Execute CPU-intensive computation."""
27+
# Simulate CPU-intensive work
28+
result = sum(i**2 for i in range(int(self.x * 10000)))
29+
self.y = result
30+
# --8<-- [end:definition]
31+
32+
33+
class GPUTask(Component):
34+
"""Component that requires GPU resources.
35+
36+
Resource requirements are declared as a class attribute.
37+
"""
38+
39+
io = IO(inputs=["data"], outputs=["result"])
40+
resources = Resource(cpu="500m", gpu=1) # Declare resources at class level
41+
42+
async def step(self) -> None:
43+
"""Execute GPU computation."""
44+
# Simulate GPU computation
45+
self.result = self.data * 2
46+
47+
48+
class DataProducer(Component):
49+
"""Produces data for processing."""
50+
51+
io = IO(outputs=["output"])
52+
53+
def __init__(self, iters: int, **kwargs: _t.Unpack[ComponentArgsDict]) -> None:
54+
"""Initialize DataProducer with iteration count."""
55+
super().__init__(**kwargs)
56+
self._iters = iters
57+
58+
async def init(self) -> None:
59+
"""Initialize the data sequence."""
60+
self._seq = iter(range(self._iters))
61+
62+
async def step(self) -> None:
63+
"""Produce the next data value."""
64+
try:
65+
self.output = next(self._seq)
66+
except StopIteration:
67+
await self.io.close()
68+
69+
70+
async def main() -> None:
71+
"""Run the process with resource-constrained components."""
72+
# --8<-- [start:resources]
73+
# Resources can be declared at the class level (see CPUIntensiveTask and GPUTask above)
74+
# or overridden when instantiating components
75+
process = RayProcess(
76+
components=[
77+
CPUIntensiveTask(name="cpu-task", resources=Resource(cpu=1.0)), # (1)!
78+
GPUTask(name="gpu-task"), # (2)!
79+
DataProducer(name="producer", iters=5), # (3)!
80+
],
81+
connectors=[
82+
RayConnector(spec=ConnectorSpec(source="producer.output", target="cpu-task.x")),
83+
RayConnector(spec=ConnectorSpec(source="cpu-task.y", target="gpu-task.data")),
84+
],
85+
)
86+
# --8<-- [end:resources]
87+
88+
async with process:
89+
await process.run()
90+
91+
print("Process completed successfully!")
92+
93+
94+
if __name__ == "__main__":
95+
if not ray.is_initialized():
96+
# Ray must be initialised with the necessary resources
97+
ray.init(num_cpus=5, num_gpus=1, resources={"custom_hardware": 10}, include_dashboard=True)
98+
asyncio.run(main())

mkdocs.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ plugins:
3333
default_handler: python
3434
handlers:
3535
python:
36-
paths: [src]
36+
paths: [plugboard, plugboard-schemas]
3737
options:
3838
docstring_style: google
3939
show_source: false
@@ -113,6 +113,7 @@ watch:
113113
- docs
114114
- examples
115115
- plugboard
116+
- plugboard-schemas
116117
- README.md
117118
- CONTRIBUTING.md
118119

plugboard-schemas/plugboard_schemas/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from importlib.metadata import version
1010

1111
from ._common import PlugboardBaseModel
12-
from .component import ComponentArgsDict, ComponentArgsSpec, ComponentSpec
12+
from .component import ComponentArgsDict, ComponentArgsSpec, ComponentSpec, Resource
1313
from .config import ConfigSpec, ProcessConfigSpec
1414
from .connector import (
1515
DEFAULT_CONNECTOR_CLS_PATH,
@@ -77,6 +77,7 @@
7777
"ProcessArgsDict",
7878
"ProcessArgsSpec",
7979
"RAY_STATE_BACKEND_CLS_PATH",
80+
"Resource",
8081
"StateBackendSpec",
8182
"StateBackendArgsDict",
8283
"StateBackendArgsSpec",

0 commit comments

Comments
 (0)