Files
ludwig-ai--ludwig/scripts/benchmark_lazy_preprocessing.py
wehub-resource-sync 593b94c120
pytest / Unit Tests (push) Waiting to run
pytest / Integration (integration_tests_a) (push) Waiting to run
pytest / Integration (integration_tests_b) (push) Waiting to run
pytest / Integration (integration_tests_c) (push) Waiting to run
pytest / Integration (integration_tests_d) (push) Waiting to run
pytest / Integration (integration_tests_e) (push) Waiting to run
pytest / Integration (integration_tests_f) (push) Waiting to run
pytest / Integration (integration_tests_g) (push) Waiting to run
pytest / Integration (integration_tests_h) (push) Waiting to run
pytest / Integration (integration_tests_i) (push) Waiting to run
pytest / Integration (integration_tests_j) (push) Waiting to run
pytest / Distributed (distributed_a) (push) Waiting to run
pytest / Distributed (distributed_b) (push) Waiting to run
pytest / Distributed (distributed_c) (push) Waiting to run
pytest / Distributed (distributed_d) (push) Waiting to run
pytest / Distributed (distributed_e) (push) Waiting to run
pytest / Distributed (distributed_f) (push) Waiting to run
pytest / Minimal Install (push) Waiting to run
pytest / Event File (push) Waiting to run
pytest (slow) / py-slow (push) Waiting to run
Publish JSON Schema / publish-schema (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 12:49:20 +08:00

331 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Benchmark: lazy vs eager preprocessing for image and audio features.
Each mode (eager, lazy) runs in full isolation — separate CSV copies so
Ludwig cannot reuse any preprocessed cache between modes.
Measures per mode:
1. Preprocessing-only time + peak heap (model.preprocess on copy A)
2. Full-pipeline time + peak heap (model.train on copy B — independent)
3. Training throughput derived from (2) - (1)
Run:
python scripts/benchmark_lazy_preprocessing.py [--n-samples N]
Requires torch, torchaudio (via Ludwig venv)
"""
from __future__ import annotations
import argparse
import csv
import json
import os
import shutil
import struct
import tempfile
import time
import tracemalloc
import wave
from pathlib import Path
import numpy as np
# ---------------------------------------------------------------------------
# Synthetic data generators
# ---------------------------------------------------------------------------
def _make_png_rgb(path: Path, width: int = 64, height: int = 64, seed: int = 0) -> None:
import zlib
rng = np.random.default_rng(seed)
pixels = rng.integers(0, 256, (height, width, 3), dtype=np.uint8)
def _chunk(t: bytes, d: bytes) -> bytes:
crc = zlib.crc32(t + d) & 0xFFFFFFFF
return struct.pack(">I", len(d)) + t + d + struct.pack(">I", crc)
sig = b"\x89PNG\r\n\x1a\n"
ihdr = _chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0))
raw = b"".join(b"\x00" + row.tobytes() for row in pixels)
idat = _chunk(b"IDAT", zlib.compress(raw))
iend = _chunk(b"IEND", b"")
path.write_bytes(sig + ihdr + idat + iend)
def _make_wav_mono(path: Path, duration_s: float = 2.0, sample_rate: int = 16000, seed: int = 0) -> None:
rng = np.random.default_rng(seed)
n = int(duration_s * sample_rate)
freq = 200.0 + rng.integers(0, 600)
t = np.linspace(0, duration_s, n, endpoint=False)
samples = (np.sin(2 * np.pi * freq * t) * 32767).astype(np.int16)
with wave.open(str(path), "w") as wf:
wf.setnchannels(1)
wf.setsampwidth(2)
wf.setframerate(sample_rate)
wf.writeframes(samples.tobytes())
def _write_csv(path: str, rows: list[dict], fieldnames: list[str]) -> None:
with open(path, "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(rows)
def create_image_dataset(root: Path, n: int, tag: str = "") -> str:
"""Return CSV path; creates N 64×64 PNG images under root/images_{tag}/."""
img_dir = root / f"images_{tag}"
img_dir.mkdir(parents=True, exist_ok=True)
labels = ["cat", "dog", "bird", "fish"]
rows = []
for i in range(n):
p = img_dir / f"img_{i:05d}.png"
_make_png_rgb(p, seed=i)
rows.append({"image_path": str(p), "label": labels[i % len(labels)]})
csv_path = str(root / f"images_{tag}.csv")
_write_csv(csv_path, rows, ["image_path", "label"])
return csv_path
def create_audio_dataset(root: Path, n: int, tag: str = "", duration_s: float = 2.0) -> str:
"""Return CSV path; creates N WAV files under root/audio_{tag}/."""
audio_dir = root / f"audio_{tag}"
audio_dir.mkdir(parents=True, exist_ok=True)
labels = ["yes", "no", "up", "down"]
rows = []
for i in range(n):
p = audio_dir / f"clip_{i:05d}.wav"
_make_wav_mono(p, duration_s=duration_s, seed=i)
rows.append({"audio_path": str(p), "label": labels[i % len(labels)]})
csv_path = str(root / f"audio_{tag}.csv")
_write_csv(csv_path, rows, ["audio_path", "label"])
return csv_path
# ---------------------------------------------------------------------------
# Memory + timing helpers
# ---------------------------------------------------------------------------
def _tracked(fn):
"""Run fn(); return (result, peak_heap_mb, elapsed_s)."""
tracemalloc.start()
t0 = time.perf_counter()
result = fn()
elapsed = time.perf_counter() - t0
_, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
tracemalloc.clear_traces()
return result, peak / 1024 / 1024, elapsed
# ---------------------------------------------------------------------------
# Ludwig wrappers
# ---------------------------------------------------------------------------
def _image_config(lazy: bool, batch_size: int) -> dict:
return {
"input_features": [
{
"name": "image_path",
"type": "image",
"preprocessing": {"lazy": lazy, "num_channels": 3, "height": 64, "width": 64},
"encoder": {
"type": "stacked_cnn",
"conv_layers": [{"num_filters": 8, "filter_size": 3}],
"fc_layers": [{"output_size": 16}],
},
}
],
"output_features": [{"name": "label", "type": "category"}],
"trainer": {"epochs": 1, "batch_size": batch_size},
"combiner": {"type": "concat", "fc_layers": [{"output_size": 16}]},
}
def _audio_config(lazy: bool, batch_size: int) -> dict:
return {
"input_features": [
{
"name": "audio_path",
"type": "audio",
"preprocessing": {
"lazy": lazy,
"audio_feature": {"type": "fbank", "num_filter_bands": 80},
"audio_file_length_limit_in_s": 2.0,
"norm": None,
},
"encoder": {
"type": "stacked_cnn",
"conv_layers": [{"num_filters": 8, "filter_size": 3}],
"fc_layers": [{"output_size": 16}],
},
}
],
"output_features": [{"name": "label", "type": "category"}],
"trainer": {"epochs": 1, "batch_size": batch_size},
"combiner": {"type": "concat", "fc_layers": [{"output_size": 16}]},
}
def _run_preprocess(csv_path: str, config: dict, out_dir: str) -> None:
import ludwig.api
model = ludwig.api.LudwigModel(config=config, logging_level=40)
model.preprocess(dataset=csv_path, output_directory=out_dir, skip_save_processed_input=False)
def _run_train(csv_path: str, config: dict, out_dir: str) -> None:
import ludwig.api
model = ludwig.api.LudwigModel(config=config, logging_level=40)
model.train(
dataset=csv_path,
output_directory=out_dir,
skip_save_model=True,
skip_save_progress=True,
skip_save_log=True,
skip_save_processed_input=False,
)
# ---------------------------------------------------------------------------
# Per-modality benchmark
# ---------------------------------------------------------------------------
def benchmark_modality(modality: str, n_samples: int, batch_size: int, tmpdir: Path) -> dict:
print(f"\n{'=' * 64}")
print(f" {modality.upper()} · {n_samples} samples · batch_size={batch_size}")
print("=" * 64)
cfg_fn = _image_config if modality == "image" else _audio_config
t0 = time.perf_counter()
# Create THREE independent copies so there is zero shared cache between runs
if modality == "image":
csv_prep_eager = create_image_dataset(tmpdir / modality, n_samples, tag="prep_eager")
csv_prep_lazy = create_image_dataset(tmpdir / modality, n_samples, tag="prep_lazy")
csv_full_eager = create_image_dataset(tmpdir / modality, n_samples, tag="full_eager")
csv_full_lazy = create_image_dataset(tmpdir / modality, n_samples, tag="full_lazy")
else:
csv_prep_eager = create_audio_dataset(tmpdir / modality, n_samples, tag="prep_eager")
csv_prep_lazy = create_audio_dataset(tmpdir / modality, n_samples, tag="prep_lazy")
csv_full_eager = create_audio_dataset(tmpdir / modality, n_samples, tag="full_eager")
csv_full_lazy = create_audio_dataset(tmpdir / modality, n_samples, tag="full_lazy")
print(f" Dataset copies generated in {time.perf_counter() - t0:.1f}s")
results: dict = {}
for lazy in [False, True]:
mode = "lazy" if lazy else "eager"
config = cfg_fn(lazy=lazy, batch_size=batch_size)
csv_prep = csv_prep_lazy if lazy else csv_prep_eager
csv_full = csv_full_lazy if lazy else csv_full_eager
# ── Preprocessing only ────────────────────────────────────────
prep_out = str(tmpdir / f"{modality}_{mode}_prep")
_, prep_heap_mb, prep_s = _tracked(lambda _c=csv_prep, _cfg=config, _o=prep_out: _run_preprocess(_c, _cfg, _o))
if os.path.exists(prep_out):
shutil.rmtree(prep_out)
# ── Full pipeline (preprocess + train) — completely isolated ──
full_out = str(tmpdir / f"{modality}_{mode}_full")
_, full_heap_mb, full_s = _tracked(lambda _c=csv_full, _cfg=config, _o=full_out: _run_train(_c, _cfg, _o))
if os.path.exists(full_out):
shutil.rmtree(full_out)
# Training-only time = full pipeline preprocessing-only
train_s = max(0.0, full_s - prep_s)
throughput = n_samples / train_s if train_s > 0 else float("inf")
print(f"\n [{mode.upper()}]")
print(f" Preprocessing : {prep_s:7.2f}s peak heap: {prep_heap_mb:7.1f} MB")
print(f" Full pipeline : {full_s:7.2f}s peak heap: {full_heap_mb:7.1f} MB")
print(f" Training est. : {train_s:7.2f}s throughput: {throughput:.1f} samples/s")
results[mode] = {
"preprocessing_s": round(prep_s, 3),
"preprocessing_peak_heap_mb": round(prep_heap_mb, 1),
"full_pipeline_s": round(full_s, 3),
"full_pipeline_peak_heap_mb": round(full_heap_mb, 1),
"training_s_est": round(train_s, 3),
"training_throughput_samples_per_s": round(throughput, 1) if throughput != float("inf") else None,
}
# ── Summary ───────────────────────────────────────────────────────
e, la = results["eager"], results["lazy"]
def _ratio(a, b, key):
return round(a[key] / b[key], 2) if b.get(key) and b[key] > 0 else None
prep_speedup = _ratio(e, la, "preprocessing_s")
prep_mem_ratio = _ratio(e, la, "preprocessing_peak_heap_mb")
full_speedup = _ratio(e, la, "full_pipeline_s")
lazy_tp = la.get("training_throughput_samples_per_s") or 0
eager_tp = e.get("training_throughput_samples_per_s") or 0
tp_ratio = round(lazy_tp / eager_tp, 2) if eager_tp > 0 else None
print(f"\n SUMMARY ({modality}):")
print(f" Preprocessing speedup (lazy vs eager): {prep_speedup}x faster")
print(f" Preprocessing heap ratio (eager/lazy): {prep_mem_ratio}x less heap with lazy")
print(f" Full-pipeline speedup (eager/lazy): {full_speedup}x")
print(f" Training throughput ratio (lazy/eager): {tp_ratio}x")
results["_summary"] = {
"preprocessing_speedup_lazy_over_eager": prep_speedup,
"preprocessing_heap_reduction_eager_over_lazy": prep_mem_ratio,
"full_pipeline_speedup_eager_over_lazy": full_speedup,
"training_throughput_ratio_lazy_over_eager": tp_ratio,
}
return results
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--n-samples", type=int, default=1000)
parser.add_argument("--batch-size", type=int, default=32)
parser.add_argument("--output", default="scripts/benchmark_results.json")
parser.add_argument("--modalities", nargs="+", default=["image", "audio"], choices=["image", "audio"])
args = parser.parse_args()
try:
import torch # noqa: F401
except ImportError:
print("ERROR: torch not available — run from the Ludwig venv")
return
print("Ludwig lazy-preprocessing benchmark")
print(f" n_samples={args.n_samples} batch_size={args.batch_size} modalities={args.modalities}")
results: dict = {"config": vars(args)}
with tempfile.TemporaryDirectory(prefix="ludwig_lazy_bench_") as td:
tmpdir = Path(td)
for modality in args.modalities:
try:
results[modality] = benchmark_modality(modality, args.n_samples, args.batch_size, tmpdir)
except Exception as exc:
import traceback
print(f"\nERROR in {modality}: {exc}")
traceback.print_exc()
results[modality] = {"error": str(exc)}
print(f"\n{'=' * 64}")
print("Full results:")
print(json.dumps(results, indent=2))
with open(args.output, "w") as f:
json.dump(results, f, indent=2)
print(f"\nSaved to {args.output}")
if __name__ == "__main__":
main()