Skip to content

Commit f205b90

Browse files
Address second review round: clearer docs, eager check in SqliteITarEntryReader
- data_prep.md: replace "consumed sequentially" with the precise constraint ("not used as auxiliary or mounted"). - SqliteIndexReader: expose has_sample_tables as a constructor-time attribute (mirrors db_has_sample_parts); drop the per-method _check_samples_table guard. - SqliteITarEntryReader: raise MissingSamplesTableError at __init__ when the samples table is missing — fail fast at the boundary that actually requires it. - Test updated to assert at SqliteITarEntryReader construction. Signed-off-by: Pei Li <pei.li@kaiko.ai>
1 parent 88245f2 commit f205b90

4 files changed

Lines changed: 28 additions & 40 deletions

File tree

docs/source/basic/data_prep.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -688,13 +688,13 @@ The `media_metadata` table is used to store the media metadata for the selected
688688

689689
#### Skipping the samples tables for very large datasets
690690

691-
For very large datasets (100M+ samples), populating the `samples` and `sample_parts` tables can dominate `energon prepare` runtime. If the dataset will only be consumed sequentially (not as an auxiliary dataset), you can skip these tables:
691+
For very large datasets (100M+ samples), populating the `samples` and `sample_parts` tables can dominate `energon prepare` runtime. If the dataset will not be used as an auxiliary dataset or mounted, you can skip these tables:
692692

693693
```sh
694694
> energon prepare --no-sample-tables /path/to/dataset
695695
```
696696

697-
A dataset prepared this way cannot be used as an [auxiliary dataset](aux-data).
697+
A dataset prepared this way cannot be used as an [auxiliary dataset](aux-data) or with `energon mount`.
698698

699699
(data-on-disk-jsonl)=
700700
## Dataset Format on Disk for JSONL Datasets

src/megatron/energon/flavors/webdataset/indexing.py

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,6 @@ class SqliteIndexReader:
397397

398398
sqlite_path: EPath
399399
db: ThreadLocalSqlite
400-
_samples_table_checked: bool
401400

402401
def __init__(self, sqlite_path: EPath):
403402
"""Initialize the SQLite database reader.
@@ -413,25 +412,20 @@ def __init__(self, sqlite_path: EPath):
413412
path = f"file:{path}?mode=ro&immutable=1"
414413

415414
self.db = ThreadLocalSqlite(path, is_uri=True)
416-
self._samples_table_checked = False
417415

418-
def _check_samples_table(self) -> None:
419-
"""Verify the ``samples`` table is present, raising a clear error if not.
416+
def db_has_sample_tables(self) -> bool:
417+
"""Check if the database has a samples table.
420418
421-
Called by every method that queries the ``samples`` / ``sample_parts`` tables, so callers
422-
accessing a dataset prepared with ``--no-sample-tables`` get a descriptive error instead
423-
of a raw ``sqlite3.OperationalError: no such table: samples``. The check runs once per
424-
reader instance.
419+
Returns:
420+
True if samples table exists, False otherwise.
425421
"""
426-
if self._samples_table_checked:
427-
return
428422
assert self.db is not None, "Database is closed"
429-
row = self.db.select_one(
423+
424+
db_exists = self.db.select_one(
430425
"SELECT name FROM sqlite_master WHERE type='table' AND name='samples'"
431426
)
432-
if row is None:
433-
raise MissingSamplesTableError(self.sqlite_path)
434-
self._samples_table_checked = True
427+
self.db.thread_close()
428+
return db_exists is not None
435429

436430
def db_has_sample_parts(self) -> bool:
437431
"""Check if the database has a sample_parts table.
@@ -466,7 +460,6 @@ def list_all_samples(self) -> Generator[Tuple[str, int, int], None, None]:
466460
"""
467461

468462
assert self.db is not None, "Database is closed"
469-
self._check_samples_table()
470463

471464
for row in self.db.select_all("SELECT sample_key, byte_size, tar_file_id FROM samples"):
472465
yield row[0], row[1], row[2]
@@ -479,7 +472,6 @@ def list_all_sample_parts(self) -> Generator[Tuple[str, int, int], None, None]:
479472
"""
480473

481474
assert self.db is not None, "Database is closed"
482-
self._check_samples_table()
483475

484476
# Select all parts (sorted by tar_file_id, sample_index) but joined with the sample_key names
485477
for row in self.db.select_all(
@@ -505,7 +497,6 @@ def list_sample_parts(self, sample_key: str) -> Generator[Tuple[str, int, int],
505497
"""
506498

507499
assert self.db is not None, "Database is closed"
508-
self._check_samples_table()
509500

510501
# Select all parts (sorted by tar_file_id, sample_index) but joined with the sample_key names
511502
for row in self.db.select_all(
@@ -525,15 +516,13 @@ def list_sample_parts(self, sample_key: str) -> Generator[Tuple[str, int, int],
525516
def get_total_size(self) -> int:
526517
"""Get the total size of all samples in the database."""
527518
assert self.db is not None, "Database is closed"
528-
self._check_samples_table()
529519

530520
count = self.db.select_one("SELECT SUM(byte_size) FROM samples")
531521
return count[0] if count else 0
532522

533523
def get_sample_count(self) -> int:
534524
"""Get the total number of samples in the database."""
535525
assert self.db is not None, "Database is closed"
536-
self._check_samples_table()
537526

538527
count = self.db.select_one("SELECT COUNT(*) FROM samples")
539528
return count[0] if count else 0
@@ -549,7 +538,6 @@ def get_sample_part(self, key: str, part_name: str) -> ITarRawSamplePartPointer:
549538
Pointer to the sample part raw data.
550539
"""
551540
assert self.db is not None, "Database is closed"
552-
self._check_samples_table()
553541

554542
row = self.db.select_one(
555543
"SELECT sp.tar_file_id, sp.content_byte_offset, sp.content_byte_size "
@@ -579,7 +567,6 @@ def get_sample_pointer_by_key(self, key: str) -> ITarSamplePointer:
579567
Tuple of (tar_file_id, sample_key, sample_index, byte_offset, byte_size)
580568
"""
581569
assert self.db is not None, "Database is closed"
582-
self._check_samples_table()
583570

584571
sample = self.db.select_one(
585572
"SELECT tar_file_id, sample_key, sample_index, byte_offset, byte_size "

src/megatron/energon/flavors/webdataset/itar_reader.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,8 @@ class SqliteITarEntryReader(ITarReader[str]):
484484
"""
485485

486486
sqlite_reader: SqliteIndexReader
487-
db_has_sample_parts: int
487+
db_has_sample_parts: bool
488+
db_has_sample_tables: bool
488489

489490
def __init__(
490491
self,
@@ -496,7 +497,10 @@ def __init__(
496497
disable_cache: bool = False,
497498
):
498499
from megatron.energon.flavors.webdataset.config import MAIN_FOLDER_NAME
499-
from megatron.energon.flavors.webdataset.indexing import SqliteIndexReader
500+
from megatron.energon.flavors.webdataset.indexing import (
501+
MissingSamplesTableError,
502+
SqliteIndexReader,
503+
)
500504

501505
# shard_name_to_info_idx = {name: i for i, name in enumerate(wds_meta.info_shard_files)}
502506
tar_filenames = get_info_shard_files(base_path)
@@ -507,6 +511,10 @@ def __init__(
507511
self.sqlite_reader = SqliteIndexReader(sqlite_path)
508512

509513
self.db_has_sample_parts = self.sqlite_reader.db_has_sample_parts()
514+
self.db_has_sample_tables = self.sqlite_reader.db_has_sample_tables()
515+
516+
if not self.db_has_sample_tables:
517+
raise MissingSamplesTableError(sqlite_path)
510518

511519
self.key_is_full_entryname = key_is_full_entryname
512520

tests/test_dataset.py

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
from megatron.energon.edataclass import edataclass
5151
from megatron.energon.flavors import BaseWebdatasetFactory
5252
from megatron.energon.flavors.webdataset.config import MAIN_FOLDER_NAME
53+
from megatron.energon.flavors.webdataset.itar_reader import SqliteITarEntryReader
5354
from megatron.energon.task_encoder.base import stateless
5455
from megatron.energon.tools.analyze_debug import command as analyze_debug_command
5556
from megatron.energon.tools.info import command as info_command
@@ -1862,25 +1863,19 @@ def test_prepare_dataset_no_sample_tables(self):
18621863
result.stdout + (result.stderr or "")
18631864
) or isinstance(result.exception, click.UsageError)
18641865

1865-
# Sample-key operations against the SQLite reader must raise a descriptive error,
1866-
# not the raw `sqlite3.OperationalError: no such table: samples`.
1866+
# SqliteIndexReader exposes db_has_sample_tables() as part of its public surface.
18671867
from megatron.energon.epathlib import EPath
18681868
from megatron.energon.flavors.webdataset.indexing import (
18691869
MissingSamplesTableError,
18701870
SqliteIndexReader,
18711871
)
18721872

1873-
reader = SqliteIndexReader(EPath(str(sqlite_path)))
1874-
try:
1875-
with self.assertRaises(MissingSamplesTableError) as ctx:
1876-
reader.get_sample_pointer_by_key("any-key")
1877-
assert "no-sample-tables" in str(ctx.exception)
1878-
with self.assertRaises(MissingSamplesTableError):
1879-
reader.get_sample_count()
1880-
with self.assertRaises(MissingSamplesTableError):
1881-
reader.get_sample_part("any-key", "txt")
1882-
finally:
1883-
reader.close()
1873+
index_reader = SqliteIndexReader(EPath(str(sqlite_path)))
1874+
assert index_reader.db_has_sample_tables() is False
1875+
1876+
with self.assertRaises(MissingSamplesTableError) as ctx:
1877+
SqliteITarEntryReader(EPath(str(self.dataset_path)))
1878+
assert "no-sample-tables" in str(ctx.exception)
18841879

18851880
def test_prepare_dataset_no_sample_tables_save_restore(self):
18861881
"""Resume after a mid-iteration checkpoint must reach the same samples in the
@@ -1894,8 +1889,6 @@ def test_prepare_dataset_no_sample_tables_save_restore(self):
18941889
a save/restore round-trip against a reference run.
18951890
"""
18961891

1897-
from megatron.energon import get_savable_loader, get_train_dataset
1898-
18991892
runner = CliRunner()
19001893
result = runner.invoke(
19011894
prepare_command,

0 commit comments

Comments
 (0)