Skip to content

Commit ce674f5

Browse files
committed
feat(corpus): publish verified pairs concurrently
1 parent b927ec8 commit ce674f5

4 files changed

Lines changed: 133 additions & 8 deletions

File tree

docs/CORPUS_V2_PILOT_RUNBOOK.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,15 +191,18 @@ the successful publish gate permanently excludes them.
191191
```bash
192192
venv/bin/leoma corpus v2 publish \
193193
--workdir "$WORK" \
194-
--prefix corpus-v2/leoma-ti2v-pilot-10k-v1
194+
--prefix corpus-v2/leoma-ti2v-pilot-10k-v1 \
195+
--workers 64
195196
```
196197

197198
Every MP4 and PNG is uploaded under its own content digest, then checked by size and
198199
SHA-256 object metadata. A ledger row advances to `published` only after both objects
199200
verify. The default removes both local files only after that checkpoint. An
200201
interrupted command resumes the already-approved batch without requiring the same
201202
200 audited rows to remain unpublished. Use `--keep-local` only when NVMe capacity
202-
has been planned.
203+
has been planned. Concurrent publishing keeps a bounded set of pairs in flight;
204+
successful uploads remain content-addressed and resumable even if another worker
205+
fails before they are checkpointed.
203206

204207
For a non-production smoke test with fewer than 200 total clips, the QA threshold can
205208
be explicitly reduced. Never use this override for the real corpus:

leoma/delivery/commands.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -568,14 +568,27 @@ def corpus_v2_qa_retry_rejected(workdir):
568568
@click.option("--workdir", default="corpus-v2-pilot", show_default=True)
569569
@click.option("--prefix", required=True, help="Versioned bucket prefix, e.g. corpus-v2/pilot-10k")
570570
@click.option("--limit", type=click.IntRange(min=1), default=None)
571+
@click.option(
572+
"--workers",
573+
type=click.IntRange(min=1, max=128),
574+
default=1,
575+
show_default=True,
576+
help="Concurrent verified sample-pair uploads",
577+
)
571578
@click.option("--qa-min-reviews", type=click.IntRange(min=0), default=200, show_default=True)
572579
@click.option(
573580
"--qa-min-pass-rate", type=click.FloatRange(min=0.0, max=1.0),
574581
default=0.95, show_default=True,
575582
)
576583
@click.option("--delete-local-after-upload/--keep-local", default=True, show_default=True)
577584
def corpus_v2_publish(
578-
workdir, prefix, limit, qa_min_reviews, qa_min_pass_rate, delete_local_after_upload,
585+
workdir,
586+
prefix,
587+
limit,
588+
workers,
589+
qa_min_reviews,
590+
qa_min_pass_rate,
591+
delete_local_after_upload,
579592
):
580593
"""Upload captioned clip/first-frame pairs and checkpoint each verified sample."""
581594
from leoma.bootstrap import SOURCE_BUCKET, emit_header, emit_log
@@ -596,6 +609,7 @@ def corpus_v2_publish(
596609
ledger=ledger,
597610
prefix=prefix,
598611
limit=limit,
612+
workers=workers,
599613
qa_min_reviews=qa_min_reviews,
600614
qa_min_pass_rate=qa_min_pass_rate,
601615
delete_local_after_upload=delete_local_after_upload,

leoma/infra/corpus_v2.py

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1290,6 +1290,7 @@ def publish_captioned_samples(
12901290
ledger: PilotLedger,
12911291
prefix: str,
12921292
limit: Optional[int] = None,
1293+
workers: int = 1,
12931294
qa_min_reviews: int = 0,
12941295
qa_min_pass_rate: float = 0.0,
12951296
delete_local_after_upload: bool = False,
@@ -1299,6 +1300,8 @@ def publish_captioned_samples(
12991300
clean_prefix = prefix.strip("/")
13001301
if not clean_prefix:
13011302
raise ValueError("a versioned publish prefix is required")
1303+
if workers < 1:
1304+
raise ValueError("publish workers must be at least one")
13021305
ledger.approve_publish_batch(
13031306
min_reviews=qa_min_reviews,
13041307
min_pass_rate=qa_min_pass_rate,
@@ -1311,7 +1314,8 @@ def publish_captioned_samples(
13111314
params = (int(limit),)
13121315
with ledger.connect() as db:
13131316
approved_rows = list(db.execute(query, params))
1314-
for row in approved_rows:
1317+
1318+
def upload_pair(row: sqlite3.Row) -> tuple[str, str, str, str, str]:
13151319
clip_path = str(row["clip_path"])
13161320
first_frame_path = str(row["first_frame_path"])
13171321
if not os.path.isfile(clip_path):
@@ -1385,14 +1389,49 @@ def publish_captioned_samples(
13851389
f"s3://{bucket}/{first_frame_key}"
13861390
)
13871391

1388-
ledger.mark_published(row["sample_id"], clip_key, first_frame_key)
1392+
return (
1393+
str(row["sample_id"]),
1394+
clip_key,
1395+
first_frame_key,
1396+
clip_path,
1397+
first_frame_path,
1398+
)
1399+
1400+
def checkpoint(result: tuple[str, str, str, str, str]) -> None:
1401+
nonlocal published
1402+
sample_id, clip_key, first_frame_key, clip_path, first_frame_path = result
1403+
ledger.mark_published(sample_id, clip_key, first_frame_key)
13891404
if delete_local_after_upload:
13901405
Path(clip_path).unlink(missing_ok=True)
13911406
Path(first_frame_path).unlink(missing_ok=True)
13921407
published += 1
1393-
log(
1394-
f"published {row['sample_id']} -> {clip_key} + {first_frame_key}"
1395-
)
1408+
log(f"published {sample_id} -> {clip_key} + {first_frame_key}")
1409+
1410+
if workers == 1:
1411+
for row in approved_rows:
1412+
checkpoint(upload_pair(row))
1413+
else:
1414+
from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait
1415+
1416+
# Keep the in-flight set bounded. If a worker fails, at most twice the
1417+
# configured concurrency can have uploaded but not yet checkpointed.
1418+
# Resumption safely verifies and reuses those content-addressed objects.
1419+
pending = set()
1420+
with ThreadPoolExecutor(
1421+
max_workers=workers,
1422+
thread_name_prefix="corpus-v2-publish",
1423+
) as pool:
1424+
for row in approved_rows:
1425+
pending.add(pool.submit(upload_pair, row))
1426+
if len(pending) < workers * 2:
1427+
continue
1428+
completed, pending = wait(pending, return_when=FIRST_COMPLETED)
1429+
for future in completed:
1430+
checkpoint(future.result())
1431+
while pending:
1432+
completed, pending = wait(pending, return_when=FIRST_COMPLETED)
1433+
for future in completed:
1434+
checkpoint(future.result())
13961435
return {**ledger.stats(), "published_this_run": published}
13971436

13981437

tests/unit/test_corpus_v2.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,75 @@ def fput_object(self, bucket, key, path, *, content_type, metadata):
574574
self.objects[(bucket, key)] = (Path(path).read_bytes(), stored)
575575

576576

577+
class _ConcurrentMemoryObjectStore(_MemoryObjectStore):
578+
def __init__(self):
579+
super().__init__()
580+
self._active = 0
581+
self._lock = threading.Lock()
582+
self.max_active = 0
583+
584+
def fput_object(self, bucket, key, path, *, content_type, metadata):
585+
with self._lock:
586+
self._active += 1
587+
self.max_active = max(self.max_active, self._active)
588+
try:
589+
time.sleep(0.01)
590+
super().fput_object(
591+
bucket,
592+
key,
593+
path,
594+
content_type=content_type,
595+
metadata=metadata,
596+
)
597+
finally:
598+
with self._lock:
599+
self._active -= 1
600+
601+
602+
def test_publish_workers_upload_concurrently_and_checkpoint_before_delete(tmp_path):
603+
ledger = PilotLedger(tmp_path / "pilot.sqlite3", _spec())
604+
_bind_caption(ledger)
605+
local_paths = []
606+
for index in range(6):
607+
original = _sample(
608+
tmp_path,
609+
sample_id=f"{index + 500:064x}",
610+
truth="sha256:" + f"{index + 600:064x}",
611+
)
612+
sample = PreparedSample(**{
613+
**original.__dict__,
614+
"source_key": f"raw/concurrent-{index}.mp4",
615+
"source_sha256": "sha256:" + f"{index + 700:064x}",
616+
"source_start_ms": index * 10_000,
617+
})
618+
local_paths.extend((Path(sample.clip_path), Path(sample.first_frame_path)))
619+
ledger.add_sample(sample)
620+
task = ledger.claim_caption("gpu-0")
621+
ledger.finish_caption(
622+
task.sample_id,
623+
caption="A person walks through a room while the camera follows behind.",
624+
model="org/caption-model",
625+
revision="f" * 40,
626+
worker="gpu-0",
627+
frame_count=16,
628+
)
629+
630+
store = _ConcurrentMemoryObjectStore()
631+
result = publish_captioned_samples(
632+
store,
633+
"bucket",
634+
ledger=ledger,
635+
prefix="corpus-v2/concurrent",
636+
workers=3,
637+
delete_local_after_upload=True,
638+
)
639+
640+
assert result["published_this_run"] == 6
641+
assert result["samples"] == {"published": 6}
642+
assert store.max_active >= 2
643+
assert not any(path.exists() for path in local_paths)
644+
645+
577646
def test_approved_publish_batch_resumes_without_requiring_the_audit_twice(tmp_path):
578647
ledger = PilotLedger(tmp_path / "pilot.sqlite3", _spec())
579648
_bind_caption(ledger)

0 commit comments

Comments
 (0)