Skip to content

Commit 4f9b6ca

Browse files
committed
fix(deploy): pre-flight port check + safer GPU default; align E2E demo with init --flowmesh
Doctest agent flagged that `lumilake deploy init` without `--flowmesh` silently produces a broken stack (no workers), and that the `"all"` CUDA default conflicts with other GPU stacks on shared hosts. Port collisions only surface at docker bind time today — too late to give a useful error. * `lumilake deploy up` now runs `_check_port_collisions` before any docker compose call, covering both the lumilake server port and the FlowMesh stack's ports. Failed pre-flight raises `DeployError` with the offending ports listed. * Promoted `flowmesh._env_ports` → `flowmesh.env_ports` for the new caller. * `CUDA_VISIBLE_DEVICES` default reverts to blank (skip GPU workers); the `.env.example` comment tells the user to set a free index on their host rather than `"all"`. * `docs/E2E_DEMO.md`: `init --flowmesh` is mandatory; load script documented twice (explicit creds before init, auto-detect after); demo input bumped to three stocks; the port-conflict troubleshooting bullet points at the pre-flight rather than naming specific ports. Signed-off-by: Zhengyuan Su <su.zhengyuan@u.nus.edu>
1 parent 5ba88a6 commit 4f9b6ca

8 files changed

Lines changed: 120 additions & 37 deletions

File tree

README.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,29 @@ lumilake deploy -C ~/lumilake-deploy up # bring the stack up via
5656

5757
Note: a real workflow run also requires running PostgreSQL and S3-compatible storage; agent-style retrievals (`DataRetrievalOp` with `type: agent`) additionally require `LUMID_DATA_URL`. See `docs/ENV.md` for the env contract. If you don't have your own data plane, the repo ships a bundled Postgres + MinIO at `scripts/dev/compose.data-plane.yml` — see `docs/E2E_DEMO.md` for the full three-step demo flow (data plane → load demo data → run a workflow).
5858

59-
Submit and inspect a workflow:
59+
Submit and inspect a workflow. From a source checkout the example
60+
workflow file is at `examples/templates/yaml/trading-agent.yaml`;
61+
PyPI installs do not ship the templates, so pass an absolute path to a
62+
workflow file you have locally:
6063

6164
```bash
65+
# From a source checkout:
6266
uv run lumilake login http://127.0.0.1:9000
63-
uv run lumilake job submit examples/templates/yaml/trading-agent.yaml --format yaml --input 'Stock=NVDA,AAPL' --output-prefix demo/trading-agent
64-
uv run lumilake job list
65-
uv run lumilake job watch <job_id>
67+
uv run lumilake job submit examples/templates/yaml/trading-agent.yaml \
68+
--format yaml --input 'Stock=NVDA,AAPL,MSFT' --output-prefix demo/trading-agent
69+
70+
# From a PyPI install (lumilake on PATH; supply your own workflow file):
71+
lumilake login http://127.0.0.1:9000
72+
lumilake job submit /path/to/your/workflow.yaml \
73+
--format yaml --input 'Stock=NVDA,AAPL,MSFT' --output-prefix demo/trading-agent
74+
75+
lumilake job list
76+
lumilake job watch <job_id>
6677
```
6778

79+
See `docs/E2E_DEMO.md` for a full reproduction using the bundled demo
80+
workflows and dataset.
81+
6882
## Data Access
6983

7084
- **SQL retrievals** connect directly to `DATABASE_URL`.

docs/ARCHITECTURE.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ Lumilake is organized as a small control plane around workflow parsing, scheduli
3535
v
3636
+-------------------+
3737
| Archive + results |
38-
| S3 or lumid.data |
38+
| S3 (S3_ARCHIVE_ |
39+
| PREFIX) |
3940
+-------------------+
4041
```
4142

@@ -61,4 +62,9 @@ Lumilake is organized as a small control plane around workflow parsing, scheduli
6162

6263
## Storage Model
6364

64-
Lumilake separates compute data from job archive data. Direct mode reads and writes compute data through configured PostgreSQL and S3-compatible services. lumid.data mode forwards SQL and storage operations to lumid.data instead. Job records and runtime artifacts use `S3_ARCHIVE_PREFIX` in both modes, with lumid.data handling the storage calls when configured.
65+
Lumilake separates compute data from job archive data.
66+
67+
- **SQL `DataRetrievalOp`** connects directly to `DATABASE_URL`.
68+
- **S3 `DataRetrievalOp`** connects directly to `S3_URL`.
69+
- **Agent `DataRetrievalOp`** (`type: agent`) routes through lumid.data's `/agent/v1` endpoint and therefore requires `LUMID_DATA_URL`.
70+
- **Archive** (job records, runtime artifacts) is always written under `S3_ARCHIVE_PREFIX` using the same `S3_URL` connection.

docs/E2E_DEMO.md

Lines changed: 53 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,24 @@ image-generation pair also reads HTML files from
3232
- `pg_dump` / `pg_restore` (PostgreSQL 16+) on `PATH` — used by the
3333
demo-data scripts.
3434
- Disk: ~1 GB free in `~/.cache/lumilake-demo` for the bundle.
35-
- A model provider key for the LLM ops (`OPENAI_API_KEY` or equivalent).
3635
- For agent-retrieval only: a running lumid.data instance and
3736
`LUMID_DATA_URL` set.
3837

38+
The bundled demo workflows use locally-served open-weight models
39+
(`Qwen/Qwen3-8B` for text, `llava-hf/llava-1.5-7b-hf` for vision,
40+
`Tongyi-MAI/Z-Image-Turbo` for image gen) — no `OPENAI_API_KEY` is
41+
required. Set one only if you author workflows that call hosted
42+
providers.
43+
3944
The `mc` MinIO client is **not** required; the bundled scripts use the
4045
`minio` Python library that ships with `lumilake-sdk`.
4146

47+
> **Working directory.** Step 1 and Step 2 commands use repo-relative
48+
> paths (`scripts/dev/...`). Run them from a Lumilake source checkout
49+
> root, or pass absolute paths. Step 3 commands use `lumilake deploy
50+
> -C <dir>` and `lumilake job submit <abs-path>` so they work from any
51+
> CWD once `lumilake` is installed.
52+
4253
---
4354

4455
## Step 1 — Bring up the data plane (optional)
@@ -100,10 +111,19 @@ defaults if you've overridden them.
100111
`lumilake_demo` schema, and uploads `news/{html,images}` into
101112
`s3://<bucket>/example-data/news/`.
102113

103-
It reads database / S3 credentials from a `.env` file (auto-detected by
104-
walking up from the current directory). If you ran `lumilake deploy
105-
init` in step 3 first, the defaults already point at step 1's data
106-
plane.
114+
It reads database / S3 credentials from a `.env` file (auto-detected
115+
by walking up from the current directory). If you haven't run
116+
`lumilake deploy init` yet — which writes a `.env` — pass the data-plane
117+
URLs explicitly:
118+
119+
```bash
120+
uv run python scripts/dev/load_demo_data.py \
121+
--database-url postgresql://lumilake:lumilake_password@127.0.0.1:15432/lumilake \
122+
--s3-url s3://lumilake:lumilake_password@127.0.0.1:19100/lumilake-demo
123+
```
124+
125+
Once `lumilake deploy init` has written a `.env`, the no-flag form
126+
auto-detects everything:
107127

108128
```bash
109129
uv run python scripts/dev/load_demo_data.py
@@ -169,20 +189,30 @@ from a source checkout via `uv run lumilake ...`.
169189

170190
```bash
171191
mkdir -p ~/lumilake-deploy
172-
lumilake deploy -C ~/lumilake-deploy init # writes .env from the bundled template
192+
lumilake deploy -C ~/lumilake-deploy init --flowmesh # writes .env + .env.flowmesh
173193
```
174194

195+
`--flowmesh` is required for the bundled FlowMesh stack: without it
196+
`lumilake deploy up` only starts the lumilake-server container and
197+
every job submission fails with `FlowMeshConnectionError` because no
198+
workers are reachable.
199+
175200
The shipped `.env.example` is **pre-pointed at step 1's data plane**
176201
(`postgresql://lumilake:lumilake_password@127.0.0.1:15432/lumilake`,
177202
`s3://lumilake:lumilake_password@127.0.0.1:19100/lumilake-demo`). Open
178203
`~/lumilake-deploy/.env` only if you need to:
179204

180-
- Set a model provider key (`OPENAI_API_KEY`, etc.).
205+
- Set a model provider key (`OPENAI_API_KEY`, etc.) — only if you
206+
author workflows that call hosted providers; the bundled demos run
207+
on local open-weight models.
181208
- Point at your own Postgres / S3 (override `DATABASE_URL`, `S3_URL`,
182209
`S3_USER_DATA_PREFIX`).
183210
- Enable agent retrievals (`LUMID_DATA_URL=http://127.0.0.1:9102`).
184-
- Override `CUDA_VISIBLE_DEVICES` if the default `"all"` (one worker
185-
per `nvidia-smi`-detected GPU) is too aggressive.
211+
- Set `LUMILAKE_GPU_DEVICES` to one or more free GPU indices on your
212+
host (default is empty — no GPU workers). On a shared host, pick an
213+
index that other stacks are not using rather than `"all"`. This is
214+
distinct from `CUDA_VISIBLE_DEVICES` in `.env.flowmesh`, which only
215+
scopes what the FlowMesh server container sees.
186216

187217
Then bring up the stack:
188218

@@ -201,7 +231,7 @@ WORKFLOW_DIR=/path/to/lumilake_OSS/examples/templates
201231

202232
lumilake job submit "$WORKFLOW_DIR/yaml/trading-agent.yaml" \
203233
--format yaml \
204-
--input 'Stock=NVDA,AAPL' \
234+
--input 'Stock=NVDA,AAPL,MSFT' \
205235
--output-prefix demo/trading-agent
206236

207237
# Watch progress.
@@ -215,10 +245,9 @@ The other workflow pairs follow the same shape — swap the YAML path
215245
(or JSON path + `--format n8n`).
216246

217247
**Image-generation note.** Requires a GPU-equipped FlowMesh worker.
218-
`lumilake deploy up` reads `CUDA_VISIBLE_DEVICES` from `.env`: the
219-
shipped default `"all"` spawns one worker per `nvidia-smi`-detected
220-
GPU; set it to a comma-separated subset (`"0,1"`) for partial use, or
221-
blank to skip GPU worker creation.
248+
`lumilake deploy up` reads `LUMILAKE_GPU_DEVICES` from `.env`: the
249+
shipped default is blank (no GPU workers). Set it to a free GPU index
250+
on your host (e.g. `"0"`) or a comma-separated subset for partial use.
222251

223252
**Agent-retrieval note.** Requires `LUMID_DATA_URL` set in your `.env`;
224253
the agent retrievals route through lumid.data's `/agent/v1` endpoint.
@@ -281,5 +310,13 @@ halves are loaded as a unit and versioned together.
281310
that at least one CPU worker is registered; image-generation also
282311
needs a GPU worker.
283312
- **`unresolved placeholder` on `{symbol}`** — empty input list. Pass
284-
`--input 'Stock=NVDA,AAPL'` (the YAML's `inputs: Stock: []` is a
285-
template slot, not a default value).
313+
`--input 'Stock=NVDA,AAPL,MSFT'` (the YAML's `inputs: Stock: []` is a
314+
template slot, not a default value).
315+
- **Port collision at `lumilake deploy up`** — the deploy CLI runs a
316+
pre-flight check and prints the conflicting port and its role. Free
317+
the port on the host (e.g. tear down a competing stack), or pick a
318+
free value for `LUMILAKE_SERVER_PORT` in `.env` (server) and
319+
`SERVER_HTTP_PORT` / `SERVER_GRPC_PORT` / `REDIS_CONTROL_PORT` /
320+
`REDIS_TELEMETRY_PORT` in `.env.flowmesh` (orchestrator). If you
321+
change `SERVER_HTTP_PORT`, update `LUMILAKE_RUNTIME_ORCHESTRATOR_URL`
322+
in `.env` and `FLOWMESH_BASE_URL` in `.env.flowmesh` to match.

docs/WORKFLOWS.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,12 @@ params:
5252

5353
## Examples
5454

55-
Runnable examples live in `examples/templates/`:
55+
Runnable examples live in `examples/templates/`. Each is shipped as a
56+
YAML + n8n pair; see `docs/E2E_DEMO.md` for the full reproduction
57+
recipe (data plane, demo dataset, deploy).
5658

57-
- `examples/templates/yaml/agent-retrieval-mini.yaml`
58-
- `examples/templates/yaml/image-generation.yaml`
59-
- `examples/templates/n8n/image-generation.json`
59+
- `examples/templates/yaml/trading-agent.yaml` + `examples/templates/n8n/trading-agent.json`
60+
- `examples/templates/yaml/agent-retrieval.yaml` + `examples/templates/n8n/agent-retrieval.json`
61+
- `examples/templates/yaml/image-generation.yaml` + `examples/templates/n8n/image-generation.json`
6062

61-
Use `uv run lumilake job preview examples/templates/yaml/agent-retrieval-mini.yaml --format yaml --input Stock=AAPL` to validate and inspect a YAML schedule without dispatching runtime work.
63+
Use `lumilake job preview examples/templates/yaml/trading-agent.yaml --format yaml --input Stock=NVDA` to validate and inspect a YAML schedule without dispatching runtime work.

packages/deploy/src/lumilake_deploy/assets/.env.example

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@ LUMILAKE_BATCH_ACCUMULATION_SECONDS="0"
1818
LUMILAKE_OPTIMIZER_SUBPROCESS_TIMEOUT_SECONDS="60"
1919
LUMILAKE_CPU_WORKER_GROUP_SIZE="1"
2020
LUMILAKE_GPU_WORKER_GROUP_SIZE="1"
21-
# GPU devices for FlowMesh workers (one worker per device). "all"
22-
# expands to every nvidia-smi-detected GPU; "0,2" selects a subset;
23-
# leave blank to skip GPU worker creation.
24-
CUDA_VISIBLE_DEVICES="all"
21+
# GPU devices Lumilake assigns to FlowMesh workers, one worker per
22+
# device. Distinct from ``.env.flowmesh``'s ``CUDA_VISIBLE_DEVICES``,
23+
# which scopes what the FlowMesh server container itself sees. Set to a
24+
# specific index (e.g. "0") or a comma-separated subset ("0,2") for the
25+
# free GPUs on your host; "all" expands to every nvidia-smi-detected
26+
# GPU. Leave blank to skip GPU worker creation.
27+
LUMILAKE_GPU_DEVICES=""
2528

2629
# ── Runtime (FlowMesh) ────────────────────────────────────────────────────────
2730
# The orchestrator URL the server dispatches workflows to. When using the

packages/deploy/src/lumilake_deploy/doctor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ def _add(self, level: FindingLevel, message: str) -> None:
103103
"HARDWARE_MEMORY_REQUIREMENT",
104104
"HARDWARE_GPU_REQUIREMENT",
105105
"HARDWARE_GPU_MEMORY_REQUIREMENT",
106-
"CUDA_VISIBLE_DEVICES",
106+
"LUMILAKE_GPU_DEVICES",
107107
)
108108

109109
_KNOWN_KEYS: frozenset[str] = frozenset(

packages/deploy/src/lumilake_deploy/flowmesh.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def _load_env(ef: Path) -> None:
106106
)
107107

108108

109-
def _env_ports(env_file: str | Path) -> dict[str, int]:
109+
def env_ports(env_file: str | Path) -> dict[str, int]:
110110
"""Extract configured ports from env file for collision checking."""
111111
env = parse_env_file(Path(env_file))
112112
return {
@@ -163,7 +163,7 @@ def stack_up(env_file: str | Path) -> None:
163163
_make_workdir()
164164
env_path = Path(env_file).resolve()
165165

166-
errors = check_ports(_env_ports(env_file))
166+
errors = check_ports(env_ports(env_file))
167167
if errors:
168168
for e in errors:
169169
_error(e)
@@ -308,7 +308,7 @@ def create_workers(env_file: str | Path, cpu_count: int, gpu_devices: str) -> No
308308
if not device_ids:
309309
_info(
310310
"No GPUs detected; skipping GPU worker creation. "
311-
"Set CUDA_VISIBLE_DEVICES to a comma-separated list "
311+
"Set LUMILAKE_GPU_DEVICES to a comma-separated list "
312312
"of indices to force-create."
313313
)
314314
else:
@@ -326,7 +326,7 @@ def create_workers(env_file: str | Path, cpu_count: int, gpu_devices: str) -> No
326326

327327

328328
def _resolve_gpu_device_ids(gpu_devices: str) -> list[str]:
329-
"""Resolve a CUDA_VISIBLE_DEVICES-style string into explicit device ids."""
329+
"""Resolve a LUMILAKE_GPU_DEVICES-style string into explicit device ids."""
330330
normalized = gpu_devices.strip()
331331
if normalized and normalized.lower() != "all":
332332
return [tok.strip() for tok in normalized.split(",") if tok.strip()]

packages/deploy/src/lumilake_deploy/setup.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,25 @@ def load_project_env(project_root: Path) -> None:
5151
importlib.reload(envs)
5252

5353

54+
def _check_port_collisions(project_root: Path, layout: "_InfraLayout") -> None:
55+
"""Pre-flight: surface any host-port conflicts before docker compose starts."""
56+
ports: dict[str, int] = {
57+
"lumilake-server": int(envs.LUMILAKE_SERVER_PORT or 9000),
58+
}
59+
if layout.deploy_fm:
60+
env_fm = project_root / FLOWMESH_ENV_FILE_NAME
61+
if env_fm.is_file():
62+
ports.update(fm_mod.env_ports(env_fm))
63+
errors = fm_mod.check_ports(ports)
64+
if errors:
65+
for err in errors:
66+
info(err)
67+
raise DeployError(
68+
"Port collision detected. Free the listed ports (or change them "
69+
"in .env / .env.flowmesh) and retry."
70+
)
71+
72+
5473
def _resolve_infra_layout(project_root: Path) -> _InfraLayout:
5574
"""Decide which sibling infra to bring up alongside the server."""
5675
return _InfraLayout(deploy_fm=(project_root / FLOWMESH_ENV_FILE_NAME).is_file())
@@ -203,14 +222,16 @@ def run_setup(project_root: Path, options: SetupOptions) -> None:
203222
if options.reset:
204223
_reset_stack(project_root)
205224

225+
_check_port_collisions(project_root, layout)
226+
206227
if layout.deploy_fm:
207228
env_fm = project_root / FLOWMESH_ENV_FILE_NAME
208229
fm_mod.stack_pull(env_fm)
209230
fm_mod.stack_up(env_fm)
210231
if not fm_mod.wait_healthy(env_fm, timeout=120):
211232
raise DeployError("FlowMesh stack did not become healthy.")
212233
cpu_count = int(envs.LUMILAKE_CPU_WORKER_GROUP_SIZE or 0)
213-
gpu_devices = os.environ.get("CUDA_VISIBLE_DEVICES", "")
234+
gpu_devices = os.environ.get("LUMILAKE_GPU_DEVICES", "")
214235
fm_mod.create_workers(env_fm, cpu_count=cpu_count, gpu_devices=gpu_devices)
215236

216237
if options.no_server:

0 commit comments

Comments
 (0)