Module reference — lavender_2/
Every class, function, and training loop the training-pipeline notebook uses lives in this package. The notebook itself is orchestration-only (per docs/AGENTS.md §2) so each module evolves independently without churning notebook cells.
Source is under lavender_2/ in the repo.
Dependency graph (import-time only)
┌──────────┐
│ utils │ Heartbeat, seeding, memory probes, fingerprints
└────┬─────┘
│
┌────┴─────┐
│ cache │ CACHE_DIR, read_meta / write_meta
└────┬─────┘
│
┌──────────┼──────────┐
▼ ▼ ▼
tokenizer templates ingest
│ │
└──────────┬──────────┘
▼
data
│
▼ ┌──────────────────┐
model ◄──────┤ checkpoints, │
│ │ inference │
▼ └──────────────────┘
trainingLeaf modules (utils, templates) have no internal imports beyond stdlib + numpy/torch/tqdm. Higher modules import from lower ones but never reverse; import cycles are physically impossible.
lavender_2.utils
What: building-block primitives used across every other module.
Public API:
set_seed(seed=42)— seed Python, NumPy, and PyTorch.get_device()— returntorch.device("cuda")if available else"cpu".class Heartbeat— background keep-alive context manager. Parameterslabel,every_sec=60,status=callable,quiet_exit=False. Prints viatqdm.write. Use around any blocking call that can exceed 60 s.log_memory(label="memory")— print RSS / available / VRAM.check_memory_budget(ceiling_gb=128.0, warn_at=0.9, label)— warn at 90%, raiseMemoryBudgetExceededat 100%. Returns the current RSS in GB.class MemoryBudgetExceeded(RuntimeError)— raised when RSS crosses the hard ceiling.tokenizer_sha(tokenizer) -> str— short sha of the tokenizer'sto_str()serialisation; cache-key input.texts_fingerprint(texts) -> dict— content fingerprint of a list of strings (n,total_chars, head/tail sha16).raw_ds_fingerprint(raw_ds) -> dict— same for a list of{conversation, source, weight}dicts.
Why it exists: keeps the 60-second visibility rule and the 128 GB budget rule in one file, so the rest of the package doesn't have to re-implement them.
lavender_2.cache
What: cache filesystem layout + atomic JSON meta IO.
Constants:
CACHE_DIR = "data/cache"— gitignored shard directory.DATA_CACHE_INTERVAL_SEC = 1800— 30 min between partial flushes during tokenisation.WRITE_BATCH_PRETRAIN = 2000,WRITE_BATCH_SFT = 500,WRITE_BATCH_DPO = 500— docs/conversations per flush.
Functions:
read_meta(path, expected_key) -> dict | None— returns meta on key match,Noneon miss / corruption / mismatch. Caller rebuilds onNone.write_meta(path, key, **extra)— atomic write viatmp+os.replace. Disk-full errors caught and logged.
lavender_2.tokenizer
What: tiktoken o200k_base + 4 chat specials, wrapped.
Public API:
TIKTOKEN_BASE = "o200k_base"— GPT-4o encoding.CHAT_SPECIAL_TOKENS = ("<|im_start|>", "<|im_sep|>", "<|im_end|>", "<|pad|>").build_encoding() -> tiktoken.Encoding— base + specials combined; safe to call repeatedly.build_tokenizer() -> TiktokenAdapter— one-shot helper.class TiktokenAdapter— HF-tokenizers-shape API:encode(text).ids,decode(ids),token_to_id(s),get_vocab_size(),save(path),from_file(path),to_str(). Frozen allow-list of special tokens = the six documented above.
Why it exists: the rest of the pipeline calls the HF API; this module is the bridge.
lavender_2.templates
What: chat-format rendering + weighted upsampling.
Public API:
conversation_to_text(example) -> str— flatten one normalised conversation into a ChatML-rendered string.upsample_by_weight(train_data) -> list[str]— weighted expansion + shuffle (with Heartbeat around the shuffle).effective_mix(train_data) -> list[(name, count, pct)]— post-weighting mix for the printed summary.
Template per turn: <|im_start|>{role}<|im_sep|>{content}<|im_end|>, no whitespace between turns.
lavender_2.ingest
What: HuggingFace dataset registry, per-source normalisers, combined loader.
Public API:
DATASETS: dict— 12-source registry; each entry carriespath/parquet+split+weight+ optionalgated.NORMALIZERS: dict[str, callable]— dispatch table keyed by registry name._norm_*(sample) -> list | None— per-source normalisers; emit[{role, content}, ...]orNoneif the sample fails minimum length / content checks. All 12 are public-by-convention (no leading underscore in the table; leading underscore on the function name follows an older convention and is kept for git-diff continuity).load_all_datasets(sample_limit=-1) -> (list[dict], Counter)— iterate every registry entry, normalise, return combinedtrain_dataand per-source counts.print_preflight()— probe metadata + print download / on-disk / RAM / disk totals before the long ingest loop.
lavender_2.data
What: three torch.utils.data.Dataset subclasses — pretrain, SFT, DPO — all backed by memory-mapped binary shards.
Public API:
class PretrainDataset(texts, tokenizer, max_len, eos_id)— one flatint32token stream inpretrain_tokens.bin; chunked into(max_len + 1)windows at__getitem__.class SFTDataset(raw_ds, tokenizer, max_len, pad_id, limit=-1)— parallelsft_ids.bin(int32) +sft_mask.bin(int8); loss mask is 1 only on assistant turns.class DPODataset(raw_ds, tokenizer, max_len, pad_id, limit=-1)— paralleldpo_ids.bin(int32) +dpo_mask.bin(int8) with rows laid out asconcat(chosen, rejected).
All three: resumable per-conversation, 30-min flush cadence, atomic meta updates. Peak construction RAM is O(write batch).
lavender_2.model
What: OpenMythos backbone wrapped to the notebook's legacy interface.
Public API:
class MoETransformerLM(**kwargs)— adapter overopen_mythos.main.OpenMythos. Legacy kwargs (d_model,d_ff,num_experts,top_k,n_layers) are mapped toMythosConfigfields. Returns(logits, aux_loss);aux_lossis always0.0because OpenMythos folds routing loss insideMoEFFN.class KVCache— thin adapter exposingseq_lenon top of OpenMythos's dict cache.demo_cfg(vocab_size) -> dict— single-GPU / CPU config.production_32b_cfg(vocab_size) -> dict— 32B target config; requires 8× A100-80GB + FSDP / DeepSpeed ZeRO-3.build_model(cfg, device) -> MoETransformerLM— instantiate under aHeartbeatand move todevice.
lavender_2.training
What: one epoch runner per training regime + the DPO loss helpers.
Public API:
pretrain_epoch(model, dataloader, optimizer, *, device, pad_id, scaler, use_amp, …) -> (ce, aux)sft_train_epoch(model, dataloader, optimizer, *, device, …) -> lossdpo_train_epoch(policy, ref_model, dataloader, optimizer, *, device, …) -> (loss, acc)sequence_log_probs(model, input_ids, labels, loss_mask) -> Tensordpo_loss(policy, ref_model, …, beta=0.1) -> (loss, margin, accuracy)
Each epoch runner: mininterval=1.0 + miniters=1 tqdm bar + first-batch timing print + slow-batch log (>60 s) + outer Heartbeat with a live batch N/total, loss=… status. Matches LLM_CHECK §1.5's "progress at least once per minute" contract.
lavender_2.checkpoints (rewritten v0.2.4)
What: per-run, timestamp-named checkpointing with latest-N retention and Bark milestone pushes on save / load.
Public API:
class CheckpointManager(model, tokenizer, model_cfg, optimizer=None, ckpt_dir="checkpoints", interval_sec=1800, retention=3, logger=None, notify=True). Createscheckpoints/run-<YYYY-MM-DD_HH-MM-SS>/on construction; saves land inside it..set_optimizer(optimizer)— attach / switch optimizer..maybe_save(stage, **extra)— save if ≥interval_secsince last save. Safe to call every batch..save(stage, **extra) -> str— write<stage>-<HH-MM-SS>-<counter>.ptunderrun_dir, enforce retention, logSAVE+RETAINlines vialogger, firenotify_milestone(...)with the filename + ETA / loss extras. Wrapstorch.saveinHeartbeat..load(path, device="cpu", *, notify=True) -> dict— static; single-file load underHeartbeat; firesnotify_milestoneon success. No copy-fallback scan (v0.2.3 had one; retired with the_copysuffix)..list_runs(ckpt_dir) -> list[str]— static; everyrun-<ts>subdir, chronological..list_checkpoints(where) -> list[str]— static; pass a run directory or the root. Union of nested + flat (so pre-v0.2.4 legacy flat files are still discoverable for migration).
Module-level constants: CKPT_DIR = "checkpoints", CKPT_INTERVAL_SEC = 1800, CKPT_RETENTION = 3.
See docs/versions/0.2.4.md for the layout rewrite rationale and the pre-/post-v0.2.4 API migration table.
lavender_2.logging_setup (v0.2.5)
What: per-run stdlib :mod:logging configuration with level-aware retention.
Public API:
setup_logging(level=DEBUG, name="lavender_2", log_dir="logs") -> str— installs a single :class:DebugRetentionFileHandleron the root logger, returns the per-run file path. Idempotent: re-running the imports cell strips prior handlers first.get_logger(suffix=None) -> logging.Logger— child logger under thelavender_2.<suffix>namespace; canonical for every module.class DebugRetentionFileHandler— file handler that drops DEBUG records older thanmax_debug_age_sec(default 3 h). Pruning runs at most once perprune_interval_sec(default 5 min); INFO+ records are kept indefinitely.- Constants:
LOGS_DIR = "logs",DEBUG_RETENTION_SEC = 3 h,PRUNE_INTERVAL_SEC = 5 min,LOG_FORMAT,DATE_FORMAT.
Why it exists: the v0.2.3 TrainingLogger deduped pings inline to fit a 1 KB cap. v0.2.5 trades the byte budget for level-awareness — INFO breadcrumbs persist forever, DEBUG fades after 3 h, and the rest of the package speaks the same :mod:logging API as every other Python codebase.
lavender_2.logs (deprecated v0.2.5)
Forwards every call to the package's stdlib logger so old notebook cells that imported TrainingLogger still parse. log() → INFO; log_heartbeat_start/end() → DEBUG. New code should import from :mod:lavender_2.logging_setup directly.
lavender_2.notify (v0.2.3, extended v0.2.4)
What: Bark push-notification client + background swap-memory watchdog + milestone helper.
Public API:
send_bark(message, *, root=".", timeout_sec=5.0, silent=True) -> (ok, fail)— URL-encodemessage, substitute into every*.bark.envtemplate's$0, append?level=passive&isArchive=0whensilent=True. Returns counts; network errors are printed as one short line and counted asfail.notify_milestone(message, *, silent=True, prefix="[lavender-2] ", root=".") -> (ok, fail)— (v0.2.4) wrapper oversend_barkthat prepends a fixed[lavender-2]prefix (with a trailing space) for grep-ability. Called from the notebook at every stage boundary and from insideCheckpointManager.save/.load.class SwapWatchdog(interval_sec=600, threshold=0.5, logger=None)— daemon poller..start()fires one immediate poll then spins up the background thread..stop()joins with a 1 s timeout. Firessend_bark(...)whenswap.used / swap.total >= threshold.BARK_ENV_GLOB = "*.bark.env"— the config-file glob matched atroot.
Why it exists: long training runs shouldn't require the owner to be watching the terminal — the moment the host starts paging RAM to disk, every subsequent tokenisation / training step slows to a crawl, so a silent push lets them intervene before hours are wasted.
lavender_2.logs (v0.2.3)
What: per-run append-only training log with a hard 1 KB size cap.
Public API:
class TrainingLogger(path=None, max_bytes=1024, header=None)— createslogs/run-<ts>.log; seeded with a one-line header + an optional user header..log(msg)— free-form timestamped line..log_heartbeat_start(label)/.log_heartbeat_end(label, duration_sec)— persists START / END markers forHeartbeatblocks. Intermediate pings are deliberately not persisted.- Module constants:
LOGS_DIR = "logs",MAX_LOG_BYTES = 1024.
Overflow behavior: when an append would exceed max_bytes, the logger rewrites the file with head + "# [...compacted N lines, round M]" + tail. Verified to stay under cap under 100-write synthetic load.
Why it exists: the owner's brief: persistent, timestamped trace of each run that survives kernel restarts, that fits in email / ticket quotes without cropping, and that does not interfere with terminal visibility (which stays byte-identical to pre-v0.2.3).
lavender_2.inference
What: autoregressive generation, paged KV cache, speculative decoding, interactive chat.
Public API:
generate(model, prompt_ids, eos_id, im_end_id, *, max_new_tokens, temperature, top_k, progress=True) -> Tensor— KV-cached sampling; stops on eitherim_end_idoreos_id.class PagedKVCache(block_size, max_blocks, n_layers, n_kv_heads, head_dim, device)— block-structured KV pool.build_draft_model(cfg, device) -> MoETransformerLM— small dense variant of the target config for speculative decoding.speculative_decode(target, draft, prompt_ids, eos_id, *, …) -> Tensor— draft-and-verify loop with a tqdm bar + outer Heartbeat.chat(model, tokenizer, device, eos_id, im_end_id, max_seq_len, …)— stdin-driven ChatML chat loop.
Editing workflow
When a training knob changes (say, you want AdamW → SOAP in pretraining):
- Edit
lavender_2/training.py. - Re-run the pretrain cell in the notebook. No notebook code change.
- Commit with the usual
feat(training): .../fix(training): ...per-commit message style.
When a new component lands (e.g. an evaluation harness):
- Add a new module
lavender_2/eval.py. - Re-export in
lavender_2/__init__.py. - Add a single orchestration cell to the notebook.
- Document the module in this file.