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

277 lines
11 KiB
Python

#!/usr/bin/env python3
"""Benchmark lazy media decode throughput: local (LazyColumn) vs Ray (map_batches).
Measures batch iteration speed for audio features with lazy=True across three paths:
1. eager_local all files decoded upfront into numpy arrays, then iterated (best-case baseline)
2. lazy_local — PandasDataset + LazyColumn, decode per-batch via ThreadPoolExecutor
3. lazy_ray — RayDataset + _with_lazy_decode, decode per-batch via Ray map_batches
Run:
python scripts/benchmark_lazy_decode.py [--n-samples N] [--batch-size B] [--epochs E]
Requirements: soundfile, ray[data], torchaudio (already installed in the Ludwig environment).
"""
import argparse
import os
import sys
import tempfile
import time
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
def _write_wav_files(dest_dir: str, n: int, duration_s: float = 0.5, sample_rate: int = 16_000) -> list[str]:
"""Write N silent WAV files and return their paths."""
import torch
import torchaudio
os.makedirs(dest_dir, exist_ok=True)
paths = []
n_samples = int(duration_s * sample_rate)
silence = torch.zeros(1, n_samples) # (channels, samples)
for i in range(n):
p = os.path.join(dest_dir, f"audio_{i:05d}.wav")
torchaudio.save(p, silence, sample_rate)
paths.append(p)
return paths
def _make_lazy_audio_metadata(feature_dim: int = 8, max_length: int = 23) -> dict:
return {
"lazy": True,
"reshape": None,
"lazy_audio_params": {
"audio_feature_dict": {
"type": "fbank",
"window_length_in_s": 0.04,
"window_shift_in_s": 0.02,
"num_filter_bands": feature_dim,
},
"feature_dim": feature_dim,
"max_length": max_length,
"padding_value": 0.0,
"normalization_type": None,
},
}
def _make_features(proc_col: str, feature_name: str) -> dict:
return {proc_col: {"name": feature_name, "column": feature_name, "type": "audio"}}
# ---------------------------------------------------------------------------
# Benchmark helpers
# ---------------------------------------------------------------------------
def bench_eager_local(paths: list[str], batch_size: int, epochs: int, feature_dim: int, max_length: int) -> float:
"""Pre-decode all files, then iterate batches from a plain numpy array."""
from ludwig.features.audio_feature import AudioFeatureMixin
# Build decode function and decode everything upfront
meta = _make_lazy_audio_metadata(feature_dim, max_length)["lazy_audio_params"]
decode_fn = AudioFeatureMixin._make_lazy_decode_fn(
audio_feature_dict=meta["audio_feature_dict"],
feature_dim=meta["feature_dim"],
max_length=meta["max_length"],
padding_value=meta["padding_value"],
normalization_type=meta["normalization_type"],
)
from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=min(16, len(paths))) as ex:
decoded = np.stack(list(ex.map(decode_fn, paths))) # (N, f, t)
total_batches = 0
t0 = time.perf_counter()
for _ in range(epochs):
for start in range(0, len(decoded), batch_size):
batch = decoded[start : start + batch_size]
_ = batch.sum() # simulate minimal usage
total_batches += 1
elapsed = time.perf_counter() - t0
return len(paths) * epochs / elapsed # samples/sec
def bench_lazy_local(paths: list[str], batch_size: int, epochs: int, feature_dim: int, max_length: int) -> float:
"""PandasDataset + LazyColumn decode-per-batch."""
from ludwig.data.dataset.pandas import PandasDataset
proc_col = "audio_proc"
feature_name = "audio_0"
features = _make_features(proc_col, feature_name)
training_set_metadata = {feature_name: _make_lazy_audio_metadata(feature_dim, max_length)}
dataset_dict = {proc_col: np.array(paths, dtype=object)}
ds = PandasDataset(dataset_dict, features, data_cache_fp=None, training_set_metadata=training_set_metadata)
total_batches = 0
t0 = time.perf_counter()
for _ in range(epochs):
with ds.initialize_batcher(batch_size=batch_size, should_shuffle=False) as batcher:
while not batcher.last_batch():
batch = batcher.next_batch()
_ = batch[proc_col].sum()
total_batches += 1
elapsed = time.perf_counter() - t0
return len(paths) * epochs / elapsed
def bench_eager_ray(paths: list[str], batch_size: int, epochs: int, feature_dim: int, max_length: int) -> float:
"""Ray dataset with pre-decoded tensors (lazy=False baseline) — no decode overhead at batch time."""
from concurrent.futures import ThreadPoolExecutor
import pandas as pd
import ray
from ludwig.data.dataset.ray import RayDataset
from ludwig.features.audio_feature import AudioFeatureMixin
proc_col = "audio_proc"
feature_name = "audio_0"
# Decode everything upfront (simulates lazy=False behaviour)
meta = _make_lazy_audio_metadata(feature_dim, max_length)["lazy_audio_params"]
decode_fn = AudioFeatureMixin._make_lazy_decode_fn(
audio_feature_dict=meta["audio_feature_dict"],
feature_dim=meta["feature_dim"],
max_length=meta["max_length"],
padding_value=meta["padding_value"],
normalization_type=meta["normalization_type"],
)
with ThreadPoolExecutor(max_workers=min(16, len(paths))) as ex:
decoded = list(ex.map(decode_fn, paths))
features = {proc_col: {"name": feature_name, "column": feature_name, "type": "audio"}}
# No lazy in metadata → _with_lazy_decode is a no-op
training_set_metadata = {feature_name: {"lazy": False, "reshape": (feature_dim, max_length)}}
df = pd.DataFrame({proc_col: decoded})
ray_ds = RayDataset.__new__(RayDataset)
ray_ds.ds = ray.data.from_pandas(df)
ray_ds.features = features
ray_ds.training_set_metadata = training_set_metadata
ray_ds.data_cache_fp = None
ray_ds.data_parquet_fp = None
total_batches = 0
t0 = time.perf_counter()
for _ in range(epochs):
with ray_ds.initialize_batcher(batch_size=batch_size, should_shuffle=False) as batcher:
while not batcher.last_batch():
batch = batcher.next_batch()
_ = batch[proc_col].sum()
total_batches += 1
elapsed = time.perf_counter() - t0
return len(paths) * epochs / elapsed
def bench_lazy_ray(paths: list[str], batch_size: int, epochs: int, feature_dim: int, max_length: int) -> float:
"""RayDataset + _with_lazy_decode decode-per-batch."""
import pandas as pd
import ray
from ludwig.data.dataset.ray import RayDataset
proc_col = "audio_proc"
feature_name = "audio_0"
features = _make_features(proc_col, feature_name)
training_set_metadata = {feature_name: _make_lazy_audio_metadata(feature_dim, max_length)}
df = pd.DataFrame({proc_col: paths})
ray_ds = RayDataset.__new__(RayDataset)
ray_ds.ds = ray.data.from_pandas(df)
ray_ds.features = features
ray_ds.training_set_metadata = training_set_metadata
ray_ds.data_cache_fp = None
ray_ds.data_parquet_fp = None
total_batches = 0
t0 = time.perf_counter()
for _ in range(epochs):
with ray_ds.initialize_batcher(batch_size=batch_size, should_shuffle=False) as batcher:
while not batcher.last_batch():
batch = batcher.next_batch()
_ = batch[proc_col].sum()
total_batches += 1
elapsed = time.perf_counter() - t0
return len(paths) * epochs / elapsed
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--n-samples", type=int, default=200, help="Number of audio files to generate")
parser.add_argument("--batch-size", type=int, default=32, help="Batch size for iteration")
parser.add_argument("--epochs", type=int, default=3, help="Number of full passes over the dataset")
parser.add_argument("--feature-dim", type=int, default=8, help="Audio feature dim (num_filter_bands)")
parser.add_argument("--max-length", type=int, default=23, help="Audio max length (frames)")
args = parser.parse_args()
with tempfile.TemporaryDirectory() as tmpdir:
print(f"Generating {args.n_samples} WAV files ...", flush=True)
paths = _write_wav_files(tmpdir, args.n_samples)
print(" done.\n")
print("Initialising Ray ...", flush=True)
import ray
if not ray.is_initialized():
ray.init(ignore_reinit_error=True, num_cpus=4, include_dashboard=False)
print(" done.\n")
configs = [
("eager_local", bench_eager_local),
("lazy_local ", bench_lazy_local),
("eager_ray ", bench_eager_ray),
("lazy_ray ", bench_lazy_ray),
]
results = {}
for name, fn in configs:
print(f"Running {name.strip()} ...", flush=True)
try:
sps = fn(paths, args.batch_size, args.epochs, args.feature_dim, args.max_length)
results[name] = sps
print(f" {sps:,.0f} samples/sec")
except Exception as e:
results[name] = None
print(f" FAILED: {e}")
print("\n--- Summary ---")
eager_ray_sps = results.get("eager_ray ")
for name, sps in results.items():
if sps is None:
print(f" {name}: FAILED")
else:
print(f" {name}: {sps:>10,.0f} sps")
print()
if eager_ray_sps and results.get("lazy_ray "):
overhead_pct = (eager_ray_sps - results["lazy_ray "]) / eager_ray_sps * 100
print(f" lazy_ray overhead vs eager_ray: {overhead_pct:.0f}%")
print(" (this is the decode-per-batch cost; scales linearly with file I/O speed)")
print()
print("Notes:")
print(" eager_local: pre-decoded in-memory arrays — pure numpy, no I/O, not a fair comparison")
print(" lazy_local: PandasDataset + LazyColumn — decode per batch via ThreadPoolExecutor")
print(" eager_ray: pre-decoded tensors in Ray object store — Ray overhead without decode")
print(" lazy_ray: RayDataset + _with_lazy_decode — decode per batch via map_batches")
print()
print(" eager_ray vs lazy_ray shows the cost of decode-per-batch in the Ray pipeline.")
print(" lazy_local vs lazy_ray shows the extra overhead of Ray vs direct numpy access.")
print(" In distributed training, each worker decodes in parallel, so effective")
print(" throughput scales with number of workers.")
ray.shutdown()
if __name__ == "__main__":
main()