Skip to content

Commit 3572625

Browse files
committed
Document batch worker flows and harden lock-contention coverage
1 parent 65c902a commit 3572625

40 files changed

Lines changed: 1034 additions & 52 deletions

README.md

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ For expensive black-box objectives, Looptimum starts with bounded exploration
1414
and then shifts to surrogate-guided suggestion ranking to reduce wasted trials.
1515
Its key differentiator is operational: a file-backed, resumable workflow that
1616
keeps state and decision trace local, which fits restricted and client-controlled
17-
environments. The usage model stays simple (`suggest -> evaluate -> ingest`);
17+
environments. The usage model stays simple (`suggest -> evaluate -> ingest`,
18+
with optional locked batches);
1819
see [`docs/how-it-works.md`](docs/how-it-works.md) for algorithm behavior and
1920
tuning consequences.
2021
For a spec-style contract summary, use
@@ -43,7 +44,7 @@ For a spec-style contract summary, use
4344
Looptimum replaces ad hoc sweep loops with a small, explicit workflow:
4445

4546
1. Define parameter bounds, objective schema, and optional constraints.
46-
2. `suggest` one trial.
47+
2. `suggest` one trial by default, or allocate a locked batch with `--count N`.
4748
3. Run that trial in your environment.
4849
4. `ingest` the result and repeat.
4950

@@ -228,12 +229,23 @@ expanded stub in
228229

229230
### `suggest` Output
230231

232+
Count `1` keeps the historical single-suggestion payload. Count `> 1` emits a
233+
bundle JSON object by default:
234+
235+
- `schema_version`
236+
- `count`
237+
- `suggestions` (array of canonical suggestion payloads)
238+
239+
Use `--jsonl` to emit one canonical suggestion JSON object per line for worker
240+
handoff.
241+
231242
Each suggestion includes:
232243

233244
- `schema_version` (semver string, emitted by runtime)
234245
- `trial_id`
235246
- `params`
236247
- `suggested_at`
248+
- `lease_token` (only when `worker_leases.enabled` is true)
237249

238250
### `ingest` Required Fields
239251

@@ -284,9 +296,7 @@ Best ranking rule:
284296

285297
### Compatibility Notes
286298

287-
- `success` is accepted as a deprecated alias and normalized to `ok`.
288-
- Legacy `failure_reason` is accepted as a deprecated alias and normalized to
289-
`terminal_reason`.
299+
- Canonical statuses are `ok`, `failed`, `killed`, and `timeout`.
290300
- For non-`ok` outcomes with no reason provided, ingest synthesizes
291301
`terminal_reason` as `status=<status>`.
292302
- `v0.2.x` state without `schema_version` (or with `0.2.x`) upgrades in-memory
@@ -319,6 +329,13 @@ Best ranking rule:
319329
- `validate [--strict]`: sanity-check config/state; warnings are non-fatal unless `--strict`.
320330
- `doctor [--json]`: print environment/backend/state diagnostics.
321331

332+
Lease note:
333+
334+
- when `worker_leases.enabled` is true, `suggest` emits `lease_token` and
335+
workers must echo it on `heartbeat` and `ingest`
336+
- `max_pending_trials`, when configured, rejects the whole requested batch
337+
before any pending state is created
338+
322339
## Templates (Choose Your Starting Level)
323340

324341
### Template Matrix (Feature Parity + Intended Use)
@@ -342,6 +359,8 @@ The `examples/` folder shows integration patterns, not benchmark leaderboards.
342359
objective (`suggest -> evaluate -> ingest -> status`, typically under one minute)
343360
- `docs/examples/multi_objective/`: generated multi-objective report/state pack
344361
with weighted-sum and lexicographic objective-schema examples
362+
- `docs/examples/batch_async/`: batch bundle, JSONL handoff, lease-token, and
363+
pending-state example pack
345364

346365
Run the tiny end-to-end objective from repo root:
347366

@@ -447,3 +466,15 @@ python3 templates/bo_client_demo/run_bo.py suggest \
447466
--project-root templates/bo_client_demo \
448467
--json-only
449468
```
469+
470+
For worker fan-out, use line-delimited output:
471+
472+
```bash
473+
python3 templates/bo_client_demo/run_bo.py suggest \
474+
--project-root templates/bo_client_demo \
475+
--count 3 \
476+
--jsonl
477+
```
478+
479+
Bundle JSON, JSONL handoff, `max_pending_trials`, and lease-token examples are
480+
captured in `docs/examples/batch_async/README.md`.

client_harness_template/tests/test_phase6_assets.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
PYPROJECT = REPO_ROOT / "pyproject.toml"
1212
TYPE_SAFETY_DOC = REPO_ROOT / "docs" / "type-safety.md"
1313
MULTI_OBJECTIVE_EXAMPLE = REPO_ROOT / "docs" / "examples" / "multi_objective"
14+
BATCH_ASYNC_EXAMPLE = REPO_ROOT / "docs" / "examples" / "batch_async"
1415

1516

1617
def test_golden_acquisition_log_has_expected_shape_and_timestamps() -> None:
@@ -109,3 +110,67 @@ def test_multi_objective_example_pack_has_expected_artifacts() -> None:
109110
manifest_payload = json.loads(manifest_path.read_text(encoding="utf-8"))
110111
assert manifest_payload["scalarization_policy"] == "weighted_sum"
111112
assert manifest_payload["objective_vector"] == {"loss": 0.3, "throughput": 2.0}
113+
114+
115+
def test_batch_async_example_pack_has_expected_artifacts() -> None:
116+
assert BATCH_ASYNC_EXAMPLE.exists(), f"missing batch/async example pack: {BATCH_ASYNC_EXAMPLE}"
117+
118+
readme_path = BATCH_ASYNC_EXAMPLE / "README.md"
119+
config_path = BATCH_ASYNC_EXAMPLE / "bo_config.json"
120+
bundle_path = BATCH_ASYNC_EXAMPLE / "suggestion_bundle.json"
121+
jsonl_path = BATCH_ASYNC_EXAMPLE / "suggestions.jsonl"
122+
status_suggest_path = BATCH_ASYNC_EXAMPLE / "status_after_batch_suggest.json"
123+
status_ingest_path = BATCH_ASYNC_EXAMPLE / "status_after_ingest.json"
124+
report_path = BATCH_ASYNC_EXAMPLE / "state" / "report.json"
125+
manifest_path = BATCH_ASYNC_EXAMPLE / "state" / "trials" / "trial_1" / "manifest.json"
126+
127+
for path in (
128+
readme_path,
129+
config_path,
130+
bundle_path,
131+
jsonl_path,
132+
status_suggest_path,
133+
status_ingest_path,
134+
report_path,
135+
manifest_path,
136+
):
137+
assert path.exists(), f"missing batch/async example artifact: {path}"
138+
139+
config_payload = json.loads(config_path.read_text(encoding="utf-8"))
140+
assert config_payload["batch_size"] == 2
141+
assert config_payload["max_pending_trials"] == 3
142+
assert config_payload["worker_leases"] == {"enabled": True}
143+
144+
bundle_payload = json.loads(bundle_path.read_text(encoding="utf-8"))
145+
assert bundle_payload["count"] == 2
146+
assert [item["trial_id"] for item in bundle_payload["suggestions"]] == [1, 2]
147+
assert all(item["lease_token"] for item in bundle_payload["suggestions"])
148+
149+
jsonl_payloads = [
150+
json.loads(line)
151+
for line in jsonl_path.read_text(encoding="utf-8").splitlines()
152+
if line.strip()
153+
]
154+
assert [item["trial_id"] for item in jsonl_payloads] == [1, 2]
155+
assert [item["lease_token"] for item in jsonl_payloads] == [
156+
item["lease_token"] for item in bundle_payload["suggestions"]
157+
]
158+
159+
suggest_status = json.loads(status_suggest_path.read_text(encoding="utf-8"))
160+
assert suggest_status["pending"] == 2
161+
assert suggest_status["leased_pending"] == 2
162+
assert suggest_status["worker_leases_enabled"] is True
163+
164+
ingest_status = json.loads(status_ingest_path.read_text(encoding="utf-8"))
165+
assert ingest_status["pending"] == 0
166+
assert ingest_status["leased_pending"] == 0
167+
assert ingest_status["best"]["trial_id"] == 2
168+
169+
report_payload = json.loads(report_path.read_text(encoding="utf-8"))
170+
assert report_payload["counts"]["observations"] == 2
171+
assert report_payload["top_trials"][0]["trial_id"] == 2
172+
173+
manifest_payload = json.loads(manifest_path.read_text(encoding="utf-8"))
174+
assert manifest_payload["lease_token"] == bundle_payload["suggestions"][0]["lease_token"]
175+
assert manifest_payload["heartbeat_count"] == 1
176+
assert manifest_payload["heartbeat_meta"] == {"worker": "worker-1", "queue": "batch"}

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ Reference artifacts:
4646

4747
- `../PILOT.md`
4848
- `examples/README.md`
49+
- `examples/batch_async/README.md`
4950
- `examples/multi_objective/README.md`
5051
- `examples/state_snapshots/README.md`
5152
- `examples/decision_trace/README.md`

docs/ci-knob-tuning.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,12 @@ Why unsupported:
8181
Queueing guidance:
8282

8383
- bound in-flight evaluations using explicit concurrency limit
84+
- keep `max_pending_trials` aligned with the largest controller batch you plan
85+
to request
8486
- ingest results in deterministic order (by completion time or trial id)
8587
- on repeated lock contention, fail fast and retry controller job
88+
- if `worker_leases.enabled` is on, preserve `lease_token` with each worker task
89+
and echo it on `heartbeat` / `ingest`
8690

8791
## 4) Contamination Controls
8892

@@ -144,7 +148,7 @@ jobs:
144148
with:
145149
python-version: "3.12"
146150
- run: python -m pip install -r requirements-dev.txt
147-
- run: python templates/bo_client/run_bo.py suggest --project-root templates/bo_client --json-only > /tmp/suggestion.json
151+
- run: python templates/bo_client/run_bo.py suggest --project-root templates/bo_client --count 3 --jsonl > /tmp/suggestions.jsonl
148152
- uses: actions/upload-artifact@v4
149153
with:
150154
name: looptimum-state
@@ -163,7 +167,7 @@ jobs:
163167
with:
164168
name: looptimum-state
165169
path: templates/bo_client/state
166-
- run: echo "Run external evaluator here; write result payload artifact"
170+
- run: echo "Run one worker per JSONL line; preserve lease_token if present and write one result payload artifact per trial"
167171

168172
controller_ingest:
169173
needs: evaluator

docs/decision-trace.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ Decision traces are written as JSON Lines:
99

1010
- `state/acquisition_log.jsonl`
1111

12-
Each line corresponds to one `suggest` decision attempt.
12+
Each line corresponds to one trial-level `suggest` decision attempt. Batch
13+
`suggest --count N` writes one line per allocated trial.
1314

1415
Related runtime log:
1516

@@ -58,6 +59,8 @@ Top-level fields:
5859
Important nuance:
5960

6061
- successful `suggest` attempts create pending state for that `trial_id`
62+
- batched successful `suggest` commands create one such record per allocated
63+
trial id
6164
- all-infeasible `suggest` attempts still log a decision, but they do not
6265
create a pending trial or increment authoritative state
6366

docs/examples/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Integration pattern note:
1010

1111
Included:
1212

13+
- `batch_async/`: generated bundle JSON, JSONL handoff, lease-token, and
14+
state/report examples for batch suggest flows
1315
- `multi_objective/`: generated weighted-sum / lexicographic example pack with
1416
`status`, `report`, and trial-manifest outputs
1517
- `state_snapshots/`: sample state/log/CSV snapshots captured from a temp run
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Batch + Async Worker Example Pack
2+
3+
Generated reference artifacts for a clean batch run captured from
4+
`templates/bo_client_demo`.
5+
6+
This pack shows the current batch/async surface:
7+
8+
- `bo_config.json`: example config with `batch_size = 2`,
9+
`max_pending_trials = 3`, and `worker_leases.enabled = true`
10+
- `suggestion_bundle.json`: default count-2 `suggest` bundle output
11+
- `suggestions.jsonl`: equivalent worker-handoff JSONL form of the same
12+
allocation
13+
- `status_after_batch_suggest.json`: pending-state headline with
14+
`leased_pending = 2`
15+
- `result_1.json` / `result_2.json`: canonical ingest payloads for the leased
16+
trials
17+
- `status_after_ingest.json`: final run headline after both leased ingests
18+
- `state/bo_state.json`: authoritative state after the batch completes
19+
- `state/report.json` / `state/report.md`: explicit report outputs for the run
20+
- `state/trials/trial_<id>/manifest.json`: per-trial manifests showing
21+
`lease_token`, heartbeat metadata, and artifact pointers
22+
23+
The captured flow is:
24+
25+
1. controller runs `suggest --count 2 --json-only`
26+
2. workers receive either the bundle payload or the JSONL lines
27+
3. worker 1 claims its trial with `heartbeat --lease-token <token>`
28+
4. both results are ingested with matching `--lease-token`
29+
5. `report` is generated from the finished state
30+
31+
Operational notes illustrated here:
32+
33+
- count-1 `suggest` stays backward compatible; batch output starts at count `> 1`
34+
- `--jsonl` is the worker-oriented serialization for the same canonical
35+
suggestion objects
36+
- `max_pending_trials` is a whole-batch guardrail, not a partial allocator
37+
- lease tokens are CLI-side claim checks; they are not embedded into ingest
38+
payload JSON
39+
40+
These files are documentation examples, not benchmark claims.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"seed": 17,
3+
"max_trials": 40,
4+
"initial_random_trials": 6,
5+
"candidate_pool_size": 600,
6+
"surrogate": {
7+
"type": "rbf_proxy",
8+
"length_scale": 0.22
9+
},
10+
"acquisition": {
11+
"type": "ucb",
12+
"kappa": 1.8,
13+
"xi": 0.01
14+
},
15+
"feature_flags": {
16+
"enable_botorch_gp": false,
17+
"fallback_to_proxy_if_unavailable": true,
18+
"enable_service_api_preview": false,
19+
"enable_dashboard_preview": false,
20+
"enable_auth_preview": false
21+
},
22+
"batch_size": 2,
23+
"max_pending_trials": 3,
24+
"worker_leases": {
25+
"enabled": true
26+
},
27+
"stopping": {
28+
"min_improvement": 0.0001,
29+
"patience_trials": 10
30+
},
31+
"paths": {
32+
"state_file": "state/bo_state.json",
33+
"observations_csv": "state/observations.csv",
34+
"acquisition_log_file": "state/acquisition_log.jsonl",
35+
"constraints_schema_file": "../_shared/schemas/constraints.schema.json",
36+
"ingest_schema_file": "../_shared/schemas/ingest_payload.schema.json",
37+
"objective_schema_schema_file": "../_shared/schemas/objective_schema.schema.json",
38+
"search_space_schema_file": "../_shared/schemas/search_space.schema.json",
39+
"suggestion_schema_file": "../_shared/schemas/suggestion_payload.schema.json"
40+
}
41+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"primary_objective": {
3+
"name": "loss",
4+
"direction": "minimize",
5+
"tolerance": 0.0,
6+
"failure_handling": "record_and_continue"
7+
},
8+
"secondary_objectives": []
9+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"parameters": [
3+
{
4+
"name": "x1",
5+
"type": "float",
6+
"bounds": [0.0, 1.0],
7+
"description": "First controllable factor"
8+
},
9+
{
10+
"name": "x2",
11+
"type": "float",
12+
"bounds": [0.0, 1.0],
13+
"description": "Second controllable factor"
14+
}
15+
]
16+
}

0 commit comments

Comments
 (0)