Skip to content

Commit e433b85

Browse files
committed
feat: add concurrent decorator
1 parent 6cce8fd commit e433b85

25 files changed

Lines changed: 819 additions & 63 deletions

File tree

crates/coglet-python/src/lib.rs

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,11 +188,25 @@ fn detect_version(py: Python<'_>, build: &BuildInfo) -> VersionInfo {
188188

189189
fn read_max_concurrency() -> usize {
190190
match std::env::var("COG_MAX_CONCURRENCY") {
191-
Ok(val) => val.parse::<usize>().unwrap_or(1),
191+
Ok(val) => match parse_max_concurrency(&val) {
192+
Some(n) => n,
193+
None => {
194+
warn!(value = %val, "Invalid COG_MAX_CONCURRENCY value, defaulting to 1");
195+
1
196+
}
197+
},
192198
Err(_) => 1,
193199
}
194200
}
195201

202+
fn parse_max_concurrency(val: &str) -> Option<usize> {
203+
match val.parse::<usize>() {
204+
Ok(0) => None,
205+
Ok(n) => Some(n),
206+
Err(_) => None,
207+
}
208+
}
209+
196210
fn read_setup_timeout() -> Option<std::time::Duration> {
197211
match std::env::var("COG_SETUP_TIMEOUT") {
198212
Ok(val) => match val.parse::<u64>() {
@@ -542,10 +556,10 @@ async fn run_worker_with_init() -> Result<(), String> {
542556
info!(predictor_ref = %predictor_ref, num_slots, is_train, "Init received, connecting to transport");
543557

544558
let handler = Arc::new(if is_train {
545-
worker_bridge::PythonPredictHandler::new_train(predictor_ref)
559+
worker_bridge::PythonPredictHandler::new_train(predictor_ref, num_slots)
546560
.map_err(|e| format!("Failed to create handler: {}", e))?
547561
} else {
548-
worker_bridge::PythonPredictHandler::new(predictor_ref)
562+
worker_bridge::PythonPredictHandler::new(predictor_ref, num_slots)
549563
.map_err(|e| format!("Failed to create handler: {}", e))?
550564
});
551565

@@ -568,6 +582,26 @@ async fn run_worker_with_init() -> Result<(), String> {
568582
.map_err(|e| format!("Worker error: {}", e))
569583
}
570584

585+
#[cfg(test)]
586+
mod tests {
587+
use super::*;
588+
589+
#[test]
590+
fn parse_max_concurrency_reads_valid_value() {
591+
assert_eq!(parse_max_concurrency("4"), Some(4));
592+
}
593+
594+
#[test]
595+
fn parse_max_concurrency_rejects_invalid_value() {
596+
assert_eq!(parse_max_concurrency("wat"), None);
597+
}
598+
599+
#[test]
600+
fn parse_max_concurrency_rejects_zero() {
601+
assert_eq!(parse_max_concurrency("0"), None);
602+
}
603+
}
604+
571605
// =============================================================================
572606
// Module init
573607
// =============================================================================

crates/coglet-python/src/worker_bridge.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,12 @@ pub struct PythonPredictHandler {
101101
async_loop: Mutex<Option<Py<PyAny>>>,
102102
/// Handle to the asyncio loop thread for joining on shutdown.
103103
async_thread: Mutex<Option<JoinHandle<()>>>,
104+
max_concurrency: usize,
104105
}
105106

106107
impl PythonPredictHandler {
107108
/// Create a handler in prediction mode.
108-
pub fn new(predictor_ref: String) -> Result<Self, SetupError> {
109+
pub fn new(predictor_ref: String, max_concurrency: usize) -> Result<Self, SetupError> {
109110
let (loop_obj, thread) = Self::init_async_loop()?;
110111
Ok(Self {
111112
predictor_ref,
@@ -114,6 +115,7 @@ impl PythonPredictHandler {
114115
mode: HandlerMode::Predict,
115116
async_loop: Mutex::new(Some(loop_obj)),
116117
async_thread: Mutex::new(Some(thread)),
118+
max_concurrency,
117119
})
118120
}
119121

@@ -122,7 +124,7 @@ impl PythonPredictHandler {
122124
/// NOTE: For bug-for-bug compatibility with cog mainline, use new() instead.
123125
/// Cog mainline's training routes incorrectly use a predict-mode worker.
124126
#[allow(dead_code)]
125-
pub fn new_train(predictor_ref: String) -> Result<Self, SetupError> {
127+
pub fn new_train(predictor_ref: String, max_concurrency: usize) -> Result<Self, SetupError> {
126128
let (loop_obj, thread) = Self::init_async_loop()?;
127129
Ok(Self {
128130
predictor_ref,
@@ -131,6 +133,7 @@ impl PythonPredictHandler {
131133
mode: HandlerMode::Train,
132134
async_loop: Mutex::new(Some(loop_obj)),
133135
async_thread: Mutex::new(Some(thread)),
136+
max_concurrency,
134137
})
135138
}
136139

@@ -281,6 +284,11 @@ impl PredictHandler for PythonPredictHandler {
281284

282285
let pred = PythonPredictor::load(py, &self.predictor_ref)
283286
.map_err(|e| SetupError::load(e.to_string()))?;
287+
if self.max_concurrency > 1 && !pred.is_async() {
288+
return Err(SetupError::setup(
289+
"COG_MAX_CONCURRENCY > 1 requires an async run() or predict() method",
290+
));
291+
}
284292

285293
// Detect SDK implementation
286294
let sdk_impl = match py.import("cog") {

docs/deploy.md

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,11 @@ curl http://localhost:5001/predictions -X POST \
7676

7777
```json
7878
{
79-
"status": "succeeded",
80-
"output": "data:image/png;base64,...",
81-
"metrics": {
82-
"predict_time": 4.52
83-
}
79+
"status": "succeeded",
80+
"output": "data:image/png;base64,...",
81+
"metrics": {
82+
"predict_time": 4.52
83+
}
8484
}
8585
```
8686

@@ -144,8 +144,8 @@ the response contains a base64-encoded data URL by default:
144144

145145
```json
146146
{
147-
"status": "succeeded",
148-
"output": "data:image/png;base64,iVBORw0KGgo..."
147+
"status": "succeeded",
148+
"output": "data:image/png;base64,iVBORw0KGgo..."
149149
}
150150
```
151151

@@ -172,8 +172,8 @@ contains the uploaded URL instead of a data URL:
172172

173173
```json
174174
{
175-
"status": "succeeded",
176-
"output": "https://example.com/upload/image.png"
175+
"status": "succeeded",
176+
"output": "https://example.com/upload/image.png"
177177
}
178178
```
179179

@@ -215,21 +215,25 @@ to stop.)
215215

216216
## Concurrency
217217

218-
By default, the server processes one run at a time. To enable concurrent runs, set the `concurrency.max` option in `cog.yaml`:
218+
By default, the server processes one run at a time. To enable concurrent runs, make your `run()` method async and decorate it with `@cog.concurrent(max=N)`:
219219

220-
```yaml
221-
concurrency:
222-
max: 4
220+
```py
221+
import cog
222+
223+
class Runner(cog.BaseRunner):
224+
@cog.concurrent(max=4)
225+
async def run(self) -> str:
226+
return "hello world"
223227
```
224228

225-
See the [`cog.yaml` reference](yaml.md#concurrency) for more details.
229+
The deprecated [`concurrency.max`](yaml.md#concurrency) field in `cog.yaml` is still supported and takes precedence over the decorator by baking `COG_MAX_CONCURRENCY` into the image.
226230

227231
## Environment variables
228232

229233
You can configure runtime behavior with environment variables:
230234

231235
- `COG_SETUP_TIMEOUT`: Maximum time in seconds for the `setup()` method (default: no timeout).
232-
- `COG_MAX_CONCURRENCY`: Number of concurrent prediction slots (default: 1).
236+
- `COG_MAX_CONCURRENCY`: Number of concurrent prediction slots (default: 1). Overrides both `@cog.concurrent` and deprecated `cog.yaml` concurrency.
233237

234238
See the [environment variables reference](environment.md) for the full list.
235239

docs/environment.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,18 @@ These variables affect a running model server. Set them in `cog.yaml` under `env
173173

174174
### `COG_MAX_CONCURRENCY`
175175

176-
Controls how many predictions the model server can run concurrently.
176+
Controls how many predictions the model server can run concurrently. This overrides both `@cog.concurrent(max=N)` and the deprecated `concurrency.max` field in `cog.yaml`.
177177

178-
By default, Cog runs one prediction at a time. Invalid values are ignored and the default of `1` is used.
178+
By default, Cog runs one prediction at a time unless the model uses `@cog.concurrent(max=N)`. Invalid values are ignored and the default of `1` is used.
179+
180+
Values greater than `1` require an async `run()` method. This applies even when `COG_MAX_CONCURRENCY` is set as a runtime operator override.
181+
182+
Concurrency is resolved in this order, from highest to lowest precedence:
183+
184+
1. `COG_MAX_CONCURRENCY` set at runtime
185+
2. Deprecated `concurrency.max` in `cog.yaml`, which is baked into the image as `COG_MAX_CONCURRENCY`
186+
3. `@cog.concurrent(max=N)` on the async `run()` method
187+
4. Default: `1`
179188

180189
```console
181190
$ COG_MAX_CONCURRENCY=4 docker run -p 5000:5000 my-model

docs/examples.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ If you want a working project to copy from, start with one of these:
1212
- [`hello-context`](https://github.com/replicate/cog/tree/main/examples/hello-context):
1313
shows how to read prediction context
1414
- [`hello-concurrency`](https://github.com/replicate/cog/tree/main/examples/hello-concurrency):
15-
demonstrates the `concurrency` setting in `cog.yaml`
15+
demonstrates the `@cog.concurrent` decorator
1616
- [`hello-train`](https://github.com/replicate/cog/tree/main/examples/hello-train):
1717
defines a training interface
1818
- [`hello-replicate`](https://github.com/replicate/cog/tree/main/examples/hello-replicate):

docs/http.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ Prefer: respond-async
255255
Endpoints for creating and canceling a prediction idempotently
256256
accept a `prediction_id` parameter in their path.
257257
By default, the server runs one prediction at a time,
258-
but this can be increased with the [`concurrency.max`](yaml.md#concurrency) setting.
258+
but this can be increased with [`@cog.concurrent(max=N)`](python.md#async-runners-and-concurrency).
259259
When all prediction slots are in use, the server returns `409 Conflict`.
260260
The client should ensure prediction slots are available
261261
before creating a new prediction with a different ID.

docs/llms.txt

Lines changed: 48 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/python.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,20 @@ class Runner(BaseRunner):
128128
return "hello world";
129129
```
130130

131-
Models that have an async `run()` function can run concurrently, up to the limit specified by [`concurrency.max`](yaml.md#max) in cog.yaml. Attempting to exceed this limit will return a 409 Conflict response.
131+
Models that have an async `run()` function can run concurrently. Use `@cog.concurrent(max=N)` to configure the default maximum concurrency for the function:
132+
133+
```py
134+
import cog
135+
136+
class Runner(cog.BaseRunner):
137+
@cog.concurrent(max=4)
138+
async def run(self) -> str:
139+
return "hello world"
140+
```
141+
142+
Attempting to exceed this limit will return a 409 Conflict response. `max` values greater than `1` require an async `run()` method. The `COG_MAX_CONCURRENCY` environment variable can override the decorator at runtime.
143+
144+
The `max` value must be an integer literal so Cog can configure the model at build time. Use `COG_MAX_CONCURRENCY` when you need to set concurrency dynamically at runtime.
132145

133146
## `Input(**kwargs)`
134147

0 commit comments

Comments
 (0)