diff --git a/book/quarto/contents/vol1/data_selection/data_selection.qmd b/book/quarto/contents/vol1/data_selection/data_selection.qmd index 141b088f51..2bf2c52159 100644 --- a/book/quarto/contents/vol1/data_selection/data_selection.qmd +++ b/book/quarto/contents/vol1/data_selection/data_selection.qmd @@ -1848,7 +1848,7 @@ The economic implications are substantial. In production settings, labeling cost # │ Imports: mlsysim.book (fmt) # │ Exports: n_unlabeled_str, cost_saving_str, speedup_str, etc. # └───────────────────────────────────────────────────────────────────────────── -from mlsysim.fmt import fmt_percent, fmt, check, fmt_time +from mlsysim.fmt import fmt_percent, fmt, check, fmt_time, fmt_count, fmt_usd class ActiveLearningRoi: """Medical imaging active learning: 20× speedup, $4.75M savings vs. naive labeling.""" @@ -1876,7 +1876,7 @@ class ActiveLearningRoi: check(speedup_value > 1.0, "Active learning must require fewer labels than naive labeling.") # ┌── 4. OUTPUT (Formatting) ───────────────────────────────────────────── - n_unlabeled_str = fmt(n_unlabeled_value/MILLION, precision=0, suffix=" Million") + n_unlabeled_str = fmt_count(n_unlabeled_value, scale="Million", precision=0) cost_per_label_str = fmt(cost_per_label_value, precision=0, commas=False) budget_str = fmt(budget_value, precision=0, commas=True) cost_all_str = fmt(cost_all_value, precision=0, commas=True) @@ -1886,7 +1886,7 @@ class ActiveLearningRoi: cost_active_str = fmt(cost_active_value, precision=0, commas=True) cost_active_pct_str = fmt_percent(cost_active_pct_value/100, precision=0, commas=False, style='prose') speedup_str = fmt(speedup_value, precision=0, commas=False) - cost_saving_str = fmt(cost_saving_value/MILLION, precision=2, suffix=" Million") + cost_saving_str = fmt_usd(cost_saving_value, precision=2, commas=False, scale="Million") over_budget_ratio_str = fmt(over_budget_ratio_value, precision=0, commas=False) deadline_months_str = fmt_time(deadline_months_value, 'month', precision=0, commas=False, style='word') ``` @@ -1908,7 +1908,7 @@ class ActiveLearningRoi: 3. **Training speed**: With `{python} ActiveLearningRoi.speedup_str`$\times$ less data, each training epoch is `{python} ActiveLearningRoi.speedup_str`$\times$ faster. 4. **Active learning outcome**: Empirical studies suggest that these `{python} ActiveLearningRoi.n_active_str` "high-information" samples often achieve higher accuracy than `{python} ActiveLearningRoi.n_random_str` random samples. -**Systems insight**: Data selection functions as a `{python} ActiveLearningRoi.speedup_str`$\times$ compute accelerator and a \$`{python} ActiveLearningRoi.cost_saving_str` cost-saving measure, delivering gains that compound with every training iteration. +**Systems insight**: Data selection functions as a `{python} ActiveLearningRoi.speedup_str`$\times$ compute accelerator and a `{python} ActiveLearningRoi.cost_saving_str` cost-saving measure, delivering gains that compound with every training iteration. ::: diff --git a/book/quarto/contents/vol1/ml_systems/ml_systems.qmd b/book/quarto/contents/vol1/ml_systems/ml_systems.qmd index 37b9530585..b530c3db7c 100644 --- a/book/quarto/contents/vol1/ml_systems/ml_systems.qmd +++ b/book/quarto/contents/vol1/ml_systems/ml_systems.qmd @@ -793,7 +793,7 @@ plt.close() # │ .kws_size_kb_str # └───────────────────────────────────────────────────────────────────────────── from mlsysim import Models -from mlsysim.fmt import fmt_int, fmt, fmt_qty, check, MarkdownStr +from mlsysim.fmt import fmt_int, fmt, fmt_qty, check, MarkdownStr, fmt_count # ┌── LEGO ─────────────────────────────────────────────── class LighthouseModels: @@ -822,14 +822,14 @@ class LighthouseModels: # ┌── 4. OUTPUT (Formatting) ────────────────────────────────────────────── resnet_gflops_str = fmt_qty(m_resnet.inference_flops, GFLOPs, precision=1) - resnet_params_m_str = fmt(resnet_params_m, precision=1, suffix=" million parameters") + resnet_params_m_str = fmt_count(m_resnet.parameters.m_as(param), scale="million", precision=1, label="parameter") resnet_fp32_mb_str = fmt(resnet_fp32_mb, precision=1, suffix=" MB") - gpt2_params_b_str = fmt(gpt2_params_b, precision=1, suffix=" billion parameters") + gpt2_params_b_str = fmt_count(m_gpt2.parameters.m_as(param), scale="billion", precision=1, label="parameter") llama_range_str = MarkdownStr("7 to 70 billion parameters") dlrm_embedding_str = fmt(dlrm_embedding_gb, precision=0, suffix=" GB") mobilenet_flops_reduction_str = fmt_multiple(mobilenet_flops_reduction, precision=1) mobile_tdp_range_str = MarkdownStr("2 to 5 W") - kws_params_str = fmt(kws_params, precision=0, commas=False, suffix=" K parameters") + kws_params_str = fmt_count(m_kws.parameters.m_as(param), scale="K", precision=0, commas=False, label="parameter") kws_flops_m_str = fmt_qty(m_kws.inference_flops, MFLOPs, precision=0) kws_size_kb_str = fmt(kws_size_kb, precision=0, suffix=" KB") ``` @@ -1000,7 +1000,7 @@ A quantitative comparison applies this analysis to ResNet-50 inference on a data # │ ResnetSetup.resnet_int8_mb_str # └───────────────────────────────────────────────────────────────────────────── from mlsysim import Models -from mlsysim.fmt import fmt, fmt_qty +from mlsysim.fmt import fmt, fmt_qty, fmt_count # ┌── LEGO ─────────────────────────────────────────────── class ResnetSetup: @@ -1017,7 +1017,7 @@ class ResnetSetup: # ┌── 4. OUTPUT (Formatting) ───────────────────────────────────────────── resnet_gflops_str = fmt_qty(m.inference_flops, GFLOPs, precision=1, commas=False) - resnet_params_m_str = fmt(resnet_params_m_value, precision=1, commas=False, suffix=" million parameters") + resnet_params_m_str = fmt_count(m.parameters.m_as(param), scale="million", precision=1, commas=False, label="parameter") resnet_fp32_mb_str = fmt_qty(resnet_fp32_bytes_value, MB, precision=1, commas=False) resnet_fp16_mb_str = fmt_qty(resnet_fp16_bytes_value, MB, precision=1, commas=False) resnet_int8_mb_str = fmt_qty(resnet_int8_bytes_value, MB, precision=1, commas=False) @@ -3064,7 +3064,7 @@ TinyML's milliwatt-scale power consumption represents a six-order-of-magnitude r # │ EnergyInference.batt_cap_mah_str, EnergyInference.batt_volt_str # └───────────────────────────────────────────────────────────────────────────── from mlsysim import Models, Hardware, Engine -from mlsysim.fmt import fmt_int, fmt, check, MarkdownStr +from mlsysim.fmt import fmt_int, fmt, check, MarkdownStr, fmt_count # ┌── LEGO ─────────────────────────────────────────────── class EnergyInference: @@ -3112,7 +3112,7 @@ class EnergyInference: q_resnet_cloud_str = fmt_count(round(batt_energy_j.m_as("J") / (e_resnet_cloud_mj * 1e-3)), commas=True, label="query") q_resnet_edge_str = fmt_count(round(batt_energy_j.m_as("J") / (e_resnet_edge_mj * 1e-3)), commas=True, label="query") q_mobilenet_str = fmt_count(round(batt_energy_j.m_as("J") / (e_mobilenet_mj * 1e-3)), commas=True, label="query") - q_kws_str = fmt(q_kws.m_as('') / MILLION, precision=0, commas=False, suffix=" million queries") + q_kws_str = fmt_count(round(q_kws.m_as('') / MILLION) * MILLION, scale="million", precision=0, commas=False, label="query") batt_cap_mah_str = fmt(Scenarios.PhoneBattery.CapacityMah.m_as('mAh'), precision=0, commas=False, suffix=" mAh") batt_volt_str = fmt(Scenarios.PhoneBattery.VoltageV.m_as('V'), precision=1, commas=False, suffix=" V") diff --git a/book/quarto/contents/vol1/model_serving/model_serving.qmd b/book/quarto/contents/vol1/model_serving/model_serving.qmd index 858923178d..cc7de45076 100644 --- a/book/quarto/contents/vol1/model_serving/model_serving.qmd +++ b/book/quarto/contents/vol1/model_serving/model_serving.qmd @@ -4844,7 +4844,7 @@ class LlmServingCalc: total_decode_s_str = fmt_qty(total_decode_s, second, precision=2, commas=False) kv_cache_gb_str = fmt_qty(kv_cache_reserved, GB, precision=0, commas=False) kv_per_token_mb_str = fmt_qty(kv_per_token, MB, precision=3, commas=False) - kv_capacity_tokens_str = fmt(kv_capacity_tokens_value/MILLION, precision=1, commas=False, suffix=" million tokens") + kv_capacity_tokens_str = fmt_count(round(kv_capacity_tokens_value), scale="million", precision=1, commas=False, label="token") tokens_per_req_str = fmt_count(total_tokens, label="token") concurrent_batch_str = fmt_count(concurrent_batch_value, commas=False, label="request") req_time_s_str = fmt_qty(req_time_s, second, precision=2, commas=False) @@ -4854,7 +4854,7 @@ class LlmServingCalc: prefill_req_per_s_str = fmt_rate(prefill_req_per_s_value, 'req/s', precision=0, commas=False) decode_req_capacity_str = fmt_rate(round(decode_req_capacity_value), 'req/s', precision=0, commas=False) hourly_cost_str = fmt_usd(hourly_cost_value, precision=0, commas=False) - tokens_per_hour_m_str = fmt(tokens_per_hour_value/MILLION, precision=1, commas=False, suffix=" million tokens/hour") + tokens_per_hour_m_str = fmt_rate(tokens_per_hour_value, "tokens/hour", scale="million", precision=1, commas=False) cost_per_m_tokens_str = fmt_usd(cost_per_m_tokens_value, precision=3, commas=False) remaining_vram_gb_str = fmt_int(remaining_vram_gb_value, commas=False, suffix=" GB") ``` diff --git a/book/quarto/contents/vol1/nn_computation/nn_computation.qmd b/book/quarto/contents/vol1/nn_computation/nn_computation.qmd index a741131594..8b8acd3ae7 100644 --- a/book/quarto/contents/vol1/nn_computation/nn_computation.qmd +++ b/book/quarto/contents/vol1/nn_computation/nn_computation.qmd @@ -1731,7 +1731,7 @@ else: # │ Exports: HistoricalScale.gpt3_params_b_str, # │ HistoricalScale.gpt4_params_t_str # └───────────────────────────────────────────────────────────────────────────── -from mlsysim.fmt import fmt_int, fmt, check +from mlsysim.fmt import fmt_int, fmt, fmt_count, check # ┌── LEGO ─────────────────────────────────────────────── class HistoricalScale: @@ -4481,7 +4481,7 @@ class AdamMemoryOverhead: check(state_gb == 56, f"Expected 56 GB Adam state, got {state_gb}") # ┌── 4. OUTPUT (Formatting) ───────────────────────────────────────────── - params_b_str = fmt(params_b, precision=0, commas=False, suffix="-billion") + params_b_str = fmt_count(params_b * BILLION, scale="billion", precision=0, commas=False, attributive=True) state_gb_str = fmt(state_gb, precision=0, commas=False, suffix=" GB") ``` diff --git a/book/quarto/contents/vol1/training/training.qmd b/book/quarto/contents/vol1/training/training.qmd index 5ad761efbe..afe0299298 100644 --- a/book/quarto/contents/vol1/training/training.qmd +++ b/book/quarto/contents/vol1/training/training.qmd @@ -490,10 +490,9 @@ class GPT2Compute: attn_pair_billion_str = fmt_int(flops_attn_pair/BILLION, commas=False) out_proj_billion_str = fmt_int(flops_out_proj/BILLION, commas=False) - total_per_layer_b = flops_layer_fwd/BILLION - total_layer_str = fmt(total_per_layer_b, precision=1, commas=False, suffix=" billion FLOPs") + total_layer_str = fmt_qty(flops_layer_fwd * flop, GFLOPs, precision=1, commas=False, unit_label="billion FLOPs") - per_step_t_str = fmt(flops_step_total/TRILLION, precision=1, commas=False, suffix=" trillion FLOPs") + per_step_t_str = fmt_qty(flops_step_total * flop, TFLOPs, precision=1, commas=False, unit_label="trillion FLOPs") total_peta_str = fmt_qty(flops_training_total * flop, PFLOPs, precision=1, commas=False) v100_time_str = fmt_time(v100_time_s, 'second', precision=1, commas=False) @@ -2465,7 +2464,7 @@ Applying this throughput analysis to our GPT-2 Lighthouse Model reveals where th # │ pcie_gen3_str, transfer_ms_str, n_cpu_workers_str, # │ parallel_tokenization_ms_str # └───────────────────────────────────────────────────────────────────────────── -from mlsysim.fmt import fmt_int, fmt, check, fmt_time +from mlsysim.fmt import fmt_int, fmt, check, fmt_time, fmt_count, fmt_rate # ┌── LEGO ─────────────────────────────────────────────── class GPT2DataPipeline: @@ -2502,8 +2501,8 @@ class GPT2DataPipeline: parallel_token_ms = tokenization_ms/workers # ┌── 4. OUTPUT (Formatting) ────────────────────────────────────────────── - tokens_per_batch_str = fmt_int(tokens_per_batch // THOUSAND, commas=False, suffix="K tokens") - token_rate_str = fmt_int(token_rate // THOUSAND, commas=False, suffix="K tokens/s") + tokens_per_batch_str = fmt_count(tokens_per_batch, scale="K", precision=1, commas=False, label="token") + token_rate_str = fmt_rate(token_rate, "tokens/s", scale="K", precision=0, commas=False) tokenization_ms_str = fmt_time(tokenization_ms, 'millisecond', precision=1, commas=False) batch_kb_str = fmt(batch_kb, precision=1, commas=False, suffix=' KB') pcie_gen3_str = fmt(pcie_bw, precision=2, commas=False, suffix=' GB/s') diff --git a/book/tools/audit/fmt/AUDIT_LEDGER.md b/book/tools/audit/fmt/AUDIT_LEDGER.md index 49595b6b7f..93e8580411 100644 --- a/book/tools/audit/fmt/AUDIT_LEDGER.md +++ b/book/tools/audit/fmt/AUDIT_LEDGER.md @@ -50,8 +50,41 @@ Status: - `git diff --check` PASS - `PYTHONPATH=mlsysim python3 -m pytest mlsysim/tests/test_fmt.py book/tests/test_codemod_fmt.py book/tests/test_fmt_prose_contract.py book/tests/test_audit_prose_semantics.py book/tests/test_visible_text.py -q -o addopts=''` PASS, 157 tests - `PYTHONPATH=mlsysim python3 book/tools/audit/fmt/fmt_prose_contract.py --root book/quarto/contents` PASS, 0 violations - - `PYTHONPATH=mlsysim python3 book/tools/audit/fmt/codemod_fmt.py queue --root book/quarto/contents` PASS, `by kind: {}` - - `PYTHONPATH=mlsysim python3 book/tools/audit/fmt/audit_prose_semantics.py --root book/quarto/contents` PASS, 0 findings +- `PYTHONPATH=mlsysim python3 book/tools/audit/fmt/codemod_fmt.py queue --root book/quarto/contents` PASS, `by kind: {}` +- `PYTHONPATH=mlsysim python3 book/tools/audit/fmt/audit_prose_semantics.py --root book/quarto/contents` PASS, 0 findings + +## 2026-05-31 — Compound-scale suffix lane + +Change type: typed formatter relocation with three deliberate adjudicated +visible/value changes. Cleared all 14 `compound_scale` suffix sites: +word-scale counts moved to direct `fmt_count(..., scale="million"|"billion")`, +scaled count rates moved to `fmt_rate(..., scale=...)`, the `7-billion` +modifier moved to `fmt_count(..., attributive=True)`, and word-scale FLOP prose +now uses `fmt_qty(..., unit_label="billion FLOPs")` / `"trillion FLOPs"` so Pint +still validates the value. + +Touched chapters and equivalence: + +| Chapter file | Result | +|---|---| +| `vol1/data_selection/data_selection.qmd` | `ActiveLearningRoi.cost_saving_str` value changed from `4.75 Million` to `\$4.75 Million`; substituted prose stayed identical because the external `\$` moved into `fmt_usd`. | +| `vol1/ml_systems/ml_systems.qmd` | Word-scale parameter/query values stayed identical except the user-approved compact scale style changed `200 K parameters` to `200K parameters`; surrounding sentence was read and remains correct. | +| `vol1/model_serving/model_serving.qmd` | `2.2 million tokens` and `45.2 million tokens/hour` stayed byte-identical. | +| `vol1/nn_computation/nn_computation.qmd` | `7-billion` stayed byte-identical through `fmt_count(..., attributive=True)`. | +| `vol1/training/training.qmd` | FLOP phrases stayed byte-identical; `32K tokens` changed to `32.8K tokens` because the exact batch count is 32,768 and `fmt_count` correctly refused to hide that at precision 0. | + +Validation details: + +- `audit_fmt_usage.py` reports no remaining `compound_scale` suffix bucket; + remaining suffix bucket is `physical_unit` 1,126. +- `assess_equiv.py baseline --ref HEAD` / `snapshot` checked touched chapters: + `data_selection` 250 values + 128 prose lines; `ml_systems` 296 + 143; + `model_serving` 365 + 172; `nn_computation` 200 + 107; `training` 453 + 214. +- Verification: `git diff --check` PASS; `python3 -m py_compile + mlsysim/mlsysim/fmt.py book/tools/audit/fmt/audit_fmt_usage.py` PASS; + focused pytest suite PASS, 182 tests; `fmt_prose_contract.py` PASS, 0 + violations; `codemod_fmt.py queue` PASS, `by kind: {}`; `./book/binder check + math` PASS; `audit_prose_semantics.py` PASS, 0 findings across 81 files. ## 2026-05-31 — Currency denominator relocation diff --git a/book/tools/audit/fmt/CODEX_HANDOFF.md b/book/tools/audit/fmt/CODEX_HANDOFF.md index 8631870c56..a6cc042f01 100644 --- a/book/tools/audit/fmt/CODEX_HANDOFF.md +++ b/book/tools/audit/fmt/CODEX_HANDOFF.md @@ -258,16 +258,14 @@ across 132 value exports and 75 prose lines. `fmt_semantic_suffix` and its test now flag `suffix=" pp"` as `pp_in_suffix`; `audit_fmt_usage.py` now classifies percentage-point suffixes separately and reports `fmt_pp` calls at 21. -**A19 — Scale-word and compound-scale formatter design: TODO.** -Audit still reports 8 exact `scale_word` suffixes and grep finds compound -suffixes such as `"million parameters"`, `"million queries"`, -`"million tokens/hour"`, and `"billion FLOPs"`. Do not blindly convert these -to `M`/`B` glyph style because word-scale prose may be intentional. Recommended -next design: add a word-scale mode to `fmt_count`, a structured counted-rate -path for tokens/hour, and either `fmt_qty`/Pint or a thin `fmt_ops` wrapper for -FLOP-count prose. Then update `audit_fmt_usage.py` and `fmt_semantic_suffix` so -compound scale suffixes do not fall through as `physical_unit`; add tests for -word-scale output and checker coverage. +**A19 — Scale-word and compound-scale formatter design: DONE.** +`fmt_count` now accepts direct word scales such as `scale="million"` / +`scale="billion"` while preserving compact glyph scales such as `scale="M"` / +`scale="B"`. `fmt_rate` now accepts the same checked scale argument for +counted rates such as `tokens/hour`, and word-scale FLOP prose uses +`fmt_qty(..., unit_label="billion FLOPs")` to preserve visible wording while +still converting through Pint. `fmt_count(..., scale_style="word")` remains for +compatibility, but new QMD should prefer the clearer direct word-scale spelling. **A20 — ML ops time suffixes: DONE.** All 19 `time_unit` suffix sites in `vol1/ml_ops/ml_ops.qmd` moved to @@ -357,7 +355,7 @@ pytest suite PASS (174 tests); `fmt_prose_contract.py` 0; `codemod_fmt.py queue` `by kind: {}`; `./book/binder check math` PASS; `audit_prose_semantics.py` CLEAN across 81 files. -**A27 — Exact scale-word suffix lane: DONE, API naming still open.** +**A27 — Exact scale-word suffix lane: DONE.** Added `fmt_count(..., scale_style="word")` and migrated the 8 exact `suffix=" million"` / `suffix=" billion"` QMD sites to typed count formatting: `data_selection` (1), `introduction` (2), `ml_ops` (1), `training` (1), @@ -371,9 +369,9 @@ wrapped keyword arguments, after pre-commit exposed the false positive on `scale_style="word"`. Verification: `git diff --check` PASS; py_compile PASS; focused pytest suite PASS (176 tests); `fmt_prose_contract.py` 0; `codemod_fmt.py queue` `by kind: {}`; `./book/binder check math` PASS; -`audit_prose_semantics.py` CLEAN across 81 files. User flagged -`scale="B", scale_style="word"` as unclear; discuss a clearer public spelling -such as `scale="billion"` before final API lock. +`audit_prose_semantics.py` CLEAN across 81 files. The later compound-scale lane +resolved the source spelling concern: new word-scale QMD should use +`scale="billion"` rather than `scale="B", scale_style="word"`. **A28 — Residual plain count-label suffix lane: DONE.** Migrated 17 remaining plain count-noun suffixes (`errors`, `steps`, `photos`, @@ -491,13 +489,30 @@ Values and substituted prose were byte-identical across `hw_acceleration`, `introduction`, `ml_systems`, `ml_workflow`, `nn_computation`, `training`, and `edge_intelligence`. `audit_fmt_usage.py` now reports no `unit_rate_or_denominator` suffix bucket and `fmt_qty` at 291. Remaining suffix -buckets: `physical_unit` 1,126 and `compound_scale` 14. Verification: `git diff +buckets before A37: `physical_unit` 1,126 and `compound_scale` 14. Verification: `git diff --check` PASS; py_compile PASS; focused pytest suite PASS (181 tests); `fmt_prose_contract.py` 0; `codemod_fmt.py queue` `by kind: {}`; `./book/binder check math` PASS; `audit_prose_semantics.py` CLEAN across 81 files. -### B. WS4 — unit-suffix lane (~2,393 sites: `GB`/`ms`/`W`/`GB/s`/…) ← the big one +**A37 — Compound-scale suffix lane: DONE.** +Cleared all 14 `compound_scale` suffix sites. `fmt_count` now supports direct +word scales (`scale="million"`, `scale="billion"`) plus +`attributive=True` for `7-billion` noun modifiers; `fmt_rate` supports checked +scaled count rates including `tokens/hour`; and word-scale FLOP phrases moved +to `fmt_qty(..., unit_label="billion FLOPs")` / `"trillion FLOPs"` so Pint still +dimension-checks the value. Most values were byte-identical. Intentional +adjudicated changes: `cost_saving_str` now owns the escaped dollar sign through +`fmt_usd` while substituted prose stayed identical; `200 K parameters` became +the user-approved compact `200K parameters`; and the old floored +`32K tokens` display for 32,768 tokens now renders as `32.8K tokens`. +`audit_fmt_usage.py` now reports no `compound_scale` suffix bucket. Remaining +suffix bucket: `physical_unit` 1,126. Verification: `git diff --check` PASS; +py_compile PASS; focused pytest suite PASS (182 tests); `fmt_prose_contract.py` +0; `codemod_fmt.py queue` `by kind: {}`; `./book/binder check math` PASS; +`audit_prose_semantics.py` CLEAN across 81 files. + +### B. WS4 — unit-suffix lane (remaining 1,126 physical-unit suffixes: `GB`/`MB`/`W`/`GB/s`/…) ← the big one **Risk: LOW** (a unit label can't cause a 0–1↔0–100 / 100× error). **Effort: HIGH** and NOT a clean codemod, because ~1,938 of the args are plain floats (e.g. `weights_gb`), not Pint Quantities, and `fmt_qty` requires a Pint Quantity to diff --git a/book/tools/audit/fmt/NIGHT_RESUME.md b/book/tools/audit/fmt/NIGHT_RESUME.md index 7bb2ee6e2d..26838cdf2e 100644 --- a/book/tools/audit/fmt/NIGHT_RESUME.md +++ b/book/tools/audit/fmt/NIGHT_RESUME.md @@ -37,13 +37,13 @@ python3 -m pytest mlsysim/tests/test_fmt.py book/tests/test_codemod_fmt.py book/ ## NOW / NEXT (update before every commit) -**STATUS: Time-unit suffix migration and exact scale-word suffix migration complete; remaining suffix work is WS4 physical units, compound scale/rate/ops design, MarkdownStr review, render/PDF audit, API clarity, and final lock.** +**STATUS: Compound-scale suffix migration complete; remaining suffix work is WS4 physical units, MarkdownStr review, render/PDF audit, API cleanup, and final lock.** **State:** multiplier + percent + scale 100% migrated; pp → typed fmt_pp (14 byte-identical sites + grammar fixes, plus the user-approved A2 benchmarking edits); 4 dangerous glyph stragglers killed; NEW semantic scanner gate (+ unit-dup bug fixes). 81/81 chapters exec clean; prose-contract 0; semantic scanner 0; -codemod queue empty; 176 focused tests pass. User ruled for no-space scaled +codemod queue empty; 182 focused tests pass. User ruled for no-space scaled counts, so `run_scale_style_lane.py` migrated the 44 queued scale sites to `fmt_count` and one manual `fmt(...) + "B"` blind spot. User also approved A2, so `benchmarking` now renders `0.9 percentage-point drop`, `below 1 @@ -75,12 +75,11 @@ HTML-render-verified for `benchmarking`. The time-unit lane is now complete: `run_time_lane.py` migrated the remaining 522 exact `time_unit` suffix sites to `fmt_time(...)`, QMD `fmt_time` calls now use full unit-name strings in source, and `audit_fmt_usage.py` reports no remaining `time_unit` suffix bucket. -The exact scale-word lane is also complete: `fmt_count` now supports word-scale -output, the 8 exact `suffix=" million"` / `suffix=" billion"` QMD sites were -migrated byte-identically, and `audit_fmt_usage.py` reports no `scale_word` -suffix bucket. User flagged `fmt_count(..., scale="B", scale_style="word")` as -unclear source spelling; `PLAN_OF_RECORD.md` records the API-clarity follow-up -to discuss a cleaner spelling such as `scale="billion"` before final lock. +The exact scale-word lane is also complete, and the later compound-scale lane +resolved the API spelling: compact scales use `scale="B"` / `scale="M"` while +word scales use `scale="billion"` / `scale="million"`. The older +`scale_style="word"` form remains compatible but should not be added in new +QMD edits. Residual plain count labels are also reduced: 17 suffixes such as `errors`, `steps`, `photos`, `requests`, `servers`, `workers`, `stages`, `V100s`, and `link tiers` now use `fmt_count(..., label=...)`, byte-identical across 9 @@ -88,7 +87,7 @@ chapters. The time codemod now also recognizes word-form `microseconds` and `milliseconds`; the three remaining exact word-form time suffixes moved to `fmt_time(..., style="word")` byte-identically. -`audit_fmt_usage.py` now splits the remaining suffix inventory into actionable +`audit_fmt_usage.py` split the remaining suffix inventory into actionable sub-buckets: physical units (1,126), resource-time labels (19), unit rates/denominators (16), compound scale (14), operation counts (12), and time compounds (8). One more plain count label (`epochs`) was moved to @@ -107,18 +106,21 @@ Exact FLOP-count suffixes are also migrated: 12 `GFLOPs`/`MFLOPs`/`KFLOPs`/ `PFLOPs` sites now use `fmt_qty(...)` with Pint FLOP units, byte-identical across 5 chapters. No separate `fmt_ops` wrapper was added because Pint already provides the unit check; word-scale FLOP phrases (`billion FLOPs`, `trillion -FLOPs`) remain in `compound_scale` pending wording/API decisions. +FLOPs`) now use `fmt_qty(..., unit_label=...)` so visible wording stays intact. The four `time_compound` suffixes are gone too: `ms latency` / `ms round-trip` now keep the unit in `fmt_time(...)`, and `ms+` uses checked `fmt_time(..., marker="+")`. The 16 `unit_rate_or_denominator` suffixes are now also migrated through `fmt_qty(...)`, using checked `unit_label=` where Pint's -compact label did not match visible house style. Remaining small suffix bucket -is `compound_scale` (14), plus 1,126 `physical_unit` suffixes. Remaining: the -rest of WS4/WS3 and later PDF/lock phases. Nothing is half-done or broken. +compact label did not match visible house style. Compound-scale suffixes are now +gone too: direct word scales, checked scaled rates, attributive count modifiers, +and `fmt_qty(unit_label=...)` cleared all 14 sites. Intentional visible fixes: +`200 K parameters`→`200K parameters` and floored `32K tokens`→`32.8K tokens` +for the exact 32,768-token batch. Remaining suffix bucket is only 1,126 +`physical_unit` suffixes. Remaining: the rest of WS4/WS3 and later PDF/lock +phases. Nothing is half-done or broken. **If continuing:** Continue with semantic lanes in `PLAN_OF_RECORD.md`. Good next -targets are compound scale suffixes, then physical-unit suffixes, MarkdownStr -sites, and the API-clarity pass. +targets are physical-unit suffixes, MarkdownStr sites, and the API-cleanup pass. For physical Quantity-backed sites, continue WS4 with `PYTHONPATH=mlsysim python3 book/tools/audit/fmt/run_unit_lane.py --write ` one chapter at a time. The latest all-chapter run migrated the remaining @@ -520,6 +522,16 @@ drop. The adjacent prose/table text was updated to `1 percentage-point threshold and `(drop of 6.8 percentage points)`. No user-decision items remain open. ## Session commit log (newest first) +- Codex compound-scale lane: cleared all 14 `compound_scale` suffix sites by + adding direct word-scale support (`scale="million"` / `"billion"`), + checked scaled `fmt_rate(..., scale=...)`, and `fmt_count(..., + attributive=True)` for `7-billion` modifiers; word-scale FLOP phrases use + `fmt_qty(..., unit_label=...)`. Intentional adjudicated changes: + `200 K parameters`→`200K parameters`, `32K tokens`→`32.8K tokens`, and + `cost_saving_str` now owns `\$` through `fmt_usd` while substituted prose is + unchanged. No `compound_scale` suffix bucket remains; remaining suffix bucket + is only `physical_unit` 1,126; contract 0, semantic 0, queue empty, targeted + suite 182 passing. - Codex unit-rate/denominator lane: added checked `fmt_qty(unit_label=...)` and migrated all 16 `TFLOP/s per W`, `kg/kWh`, `MWh/household-year`, `FLOP/byte`, `GB/day`, `GB per day`, `MB/photo`, `KB/patient`, `MWh/year`, diff --git a/book/tools/audit/fmt/PLAN_OF_RECORD.md b/book/tools/audit/fmt/PLAN_OF_RECORD.md index f69874d187..5b428abc3c 100644 --- a/book/tools/audit/fmt/PLAN_OF_RECORD.md +++ b/book/tools/audit/fmt/PLAN_OF_RECORD.md @@ -81,32 +81,26 @@ For compact lower-bound notation that is intentionally written as a trailing plus, use checked `fmt_time(..., marker="+")` so the plus cannot become an unvalidated suffix. -### Scale-word and compound-scale backlog +### Scale-word and compound-scale status -The audit still has a small `scale_word` bucket plus compound suffix blind -spots such as `"million parameters"`, `"million queries"`, `"million tokens/hour"`, -and `"billion FLOPs"`. Do not blindly convert these to `M`/`B` glyph style: -phrases like "60 million parameters" may be intentional prose. +The exact `scale_word` and `compound_scale` suffix buckets are migrated. The +public spelling is now direct: -Recommended implementation: +- `fmt_count(n, scale="B")` renders compact glyph style, e.g. `70B`. +- `fmt_count(n, scale="billion", label="parameter")` renders word style, e.g. + `70 billion parameters`. +- `fmt_count(n, scale="billion", attributive=True)` renders hyphenated noun + modifiers, e.g. `7-billion`. +- `fmt_rate(tokens_per_hour, "tokens/hour", scale="million")` renders checked + counted rates such as `45.2 million tokens/hour`. +- Word-scale FLOP prose keeps Pint validation through + `fmt_qty(flops * flop, GFLOPs, unit_label="billion FLOPs")`. -- extend `fmt_count` with a word-scale mode, e.g. - `fmt_count(n, scale="M", scale_style="word", label="parameter")` renders - `60 million parameters`; -- add a structured rate form for word-scale counted rates such as tokens/hour; -- decide whether FLOP-count prose should be `fmt_qty` with Pint FLOP units or a - thin `fmt_ops` wrapper; -- teach `audit_fmt_usage.py` and `fmt_semantic_suffix` to classify compound - scale suffixes instead of falling through to `physical_unit`; -- add tests for word-scale output, label pluralization, rate denominators, - FLOP handling, and checker coverage for exact and compound scale suffixes. - -Status: the exact `scale_word` bucket (`suffix=" million"` / -`suffix=" billion"`) is migrated through `fmt_count(..., scale_style="word")`. -Before the final API lock, revisit the source readability of that spelling. -The user flagged calls such as `scale="B", scale_style="word"` as unclear; the -candidate cleaner API is to let `scale="billion"` render the word form, while -`scale="B"` keeps the compact glyph form. +The old `fmt_count(..., scale="B", scale_style="word")` compatibility path +still works, but new QMD should prefer `scale="billion"` because it is clearer +at the call site. The compound migration also found one correctness fix: +`32K tokens` from `32 * 1024` hid 768 tokens, so it now renders as +`32.8K tokens`. ## 4. Formatter defaults @@ -169,9 +163,9 @@ FLOP counts are already Pint quantities in this repo (`flop`, `KFLOPs`, `MFLOPs`, `GFLOPs`, `PFLOPs`, ...), so exact FLOP-count suffixes should use `fmt_qty(quantity, FLOP_unit)`. Do not add a separate `fmt_ops` wrapper unless a later audit shows repeated call-site mistakes that the wrapper would prevent. -Word-scale phrases such as `billion FLOPs` and `trillion FLOPs` are a separate -compound-scale/prose decision because converting them to `GFLOPs`/`TFLOPs` -changes visible wording. +Word-scale phrases such as `billion FLOPs` and `trillion FLOPs` keep their +visible wording with `fmt_qty(..., unit_label="billion FLOPs")` after converting +through the corresponding Pint FLOP unit. When Pint's compact label does not match the book's house style, use `fmt_qty(..., unit_label="...")` rather than falling back to `suffix=`. The diff --git a/mlsysim/mlsysim/fmt.py b/mlsysim/mlsysim/fmt.py index 126007c679..6823ef7d42 100644 --- a/mlsysim/mlsysim/fmt.py +++ b/mlsysim/mlsysim/fmt.py @@ -169,11 +169,57 @@ _COUNT_LABEL_DENYLIST = { "GB/s", "MB/s", "TB/s", "Gb/s", "TFLOP/s", "PFLOP/s", "W", "kW", "MW", "J", "mJ", "Wh", "kWh", "MWh", "ms", "s", "min", "h", "ns", "us", "µs", "μs", - "QPS", "FPS", "tokens/s", "img/s", "images/s", "req/s", "samples/s", + "QPS", "FPS", "tokens/s", "tokens/hour", "img/s", "images/s", "req/s", + "samples/s", "percent", "percentage point", "percentage points", } +_DECIMAL_SCALE_FACTORS = {"K": 1e3, "M": 1e6, "B": 1e9, "T": 1e12} +_DECIMAL_SCALE_WORDS = { + "K": "thousand", + "M": "million", + "B": "billion", + "T": "trillion", +} +_DECIMAL_SCALE_SYMBOL_BY_WORD = { + word: symbol for symbol, word in _DECIMAL_SCALE_WORDS.items() +} + + +def _resolve_decimal_scale(scale, *, what, style="symbol", attributive=False): + """Return ``(divisor, suffix)`` for checked K/M/B/T decimal scales.""" + if style not in {"symbol", "word"}: + raise ValueError( + f"{what} style must be 'symbol' or 'word', got {style!r}." + ) + if scale is None: + if attributive: + raise ValueError(f"{what} attributive=True requires scale=.") + return 1, "" + if not isinstance(scale, str): + raise TypeError( + f"{what} must be a string or None, got {type(scale).__name__}." + ) + if scale in _DECIMAL_SCALE_FACTORS: + if attributive and style != "word": + raise ValueError( + f"{what} attributive=True requires a word scale, got {scale!r}." + ) + divisor = _DECIMAL_SCALE_FACTORS[scale] + if style == "word": + joiner = "-" if attributive else " " + return divisor, f"{joiner}{_DECIMAL_SCALE_WORDS[scale]}" + return divisor, scale + word_key = scale.lower() + if word_key in _DECIMAL_SCALE_SYMBOL_BY_WORD: + symbol = _DECIMAL_SCALE_SYMBOL_BY_WORD[word_key] + joiner = "-" if attributive else " " + return _DECIMAL_SCALE_FACTORS[symbol], f"{joiner}{scale}" + allowed = sorted(_DECIMAL_SCALE_FACTORS) + sorted(_DECIMAL_SCALE_SYMBOL_BY_WORD) + raise ValueError(f"{what} must be one of {allowed}, got {scale!r}.") + + def _validate_count_label(label, *, what="label") -> str: """Validate a count noun label before pluralization.""" label = _clean_text_atom(label, what=what) @@ -342,7 +388,6 @@ def fmt_int( ) -_USD_SCALES = {"K": 1e3, "M": 1e6, "B": 1e9, "T": 1e12} _USD_MARKERS = {"*"} @@ -378,6 +423,8 @@ def fmt_usd( per="year") # "~\\$1,234/year" fmt_usd(gpt3_cost, precision=1, scale="M") # "\\$4.6M" + fmt_usd(4_750_000, precision=2, + scale="million") # "\\$4.75 million" fmt_usd(rate_per_gb, precision=2, commas=False, per="GB") # "\\$0.09/GB" @@ -392,9 +439,10 @@ def fmt_usd( commas: Thousands separators (default ``True`` — currency usually groups: ``\\$15,000``). Pass ``False`` for small rates. approx: When ``True``, prepend ``~`` before the dollar sign. - scale: Optional currency scale glyph (``"K"``, ``"M"``, ``"B"``, - ``"T"``). The raw dollar amount is divided by the scale here, so - the magnitude and display glyph cannot drift apart. + scale: Optional currency scale (``"K"``, ``"M"``, ``"B"``, ``"T"`` or + word forms such as ``"million"``). The raw dollar amount is divided + by the scale here, so the magnitude and display suffix cannot drift + apart. per: Optional rate denominator (``"month"``, ``"GB"``, ``"kWh"``, or a Pint unit such as ``GB``). Pass without a leading slash. marker: Optional checked table marker appended after the currency value. @@ -417,13 +465,12 @@ def fmt_usd( raise ValueError("Use suffix= or structured scale=/per=/marker=, not both.") structured_suffix = "" if scale is not None: - if scale not in _USD_SCALES: - raise ValueError( - f"fmt_usd scale must be one of {sorted(_USD_SCALES)}, " - f"got {scale!r}." - ) - amount = amount / _USD_SCALES[scale] - structured_suffix += scale + divisor, scale_suffix = _resolve_decimal_scale( + scale, + what="fmt_usd scale", + ) + amount = amount / divisor + structured_suffix += scale_suffix structured_suffix += _denominator_suffix( per, what="fmt_usd per", @@ -616,15 +663,6 @@ def fmt_multiple(factor, precision=1, commas=False): return fmt(v, precision=precision, commas=commas) -_COUNT_SCALES = {"K": 1e3, "M": 1e6, "B": 1e9, "T": 1e12} -_COUNT_SCALE_WORDS = { - "K": "thousand", - "M": "million", - "B": "billion", - "T": "trillion", -} - - def fmt_count( value, scale=None, @@ -637,6 +675,7 @@ def fmt_count( lower_bound=False, allow_fractional=False, scale_style="symbol", + attributive=False, ): """ Format a **count** (a dimensionless tally of things), optionally with a @@ -650,7 +689,11 @@ def fmt_count( fmt_count(5_300_000, scale="M", precision=1) # "5.3M" fmt_count(5_300_000, scale="M", scale_style="word", precision=1) # "5.3 million" + fmt_count(5_300_000, scale="million", + precision=1) # "5.3 million" fmt_count(70e9, scale="B") # "70B" (e.g. params) + fmt_count(7e9, scale="billion", + attributive=True) # "7-billion" fmt_count(8192) # "8,192" (no scale) fmt_count(1024, label="GPU") # "1,024 GPUs" fmt_count(1024, label="GPU", approx=True) # "~1,024 GPUs" @@ -667,7 +710,7 @@ def fmt_count( ``fmt_qty``. ``fmt_count`` is for pure tallies only. """ raw_v = _numeric_magnitude(value) - require_integer = label is not None or plural_label is not None + require_integer = label is not None or plural_label is not None or attributive _validate_count_value( raw_v, allow_fractional=allow_fractional, @@ -675,6 +718,11 @@ def fmt_count( ) if suffix and (label is not None or plural_label is not None): raise ValueError("Use suffix= or structured label=, not both.") + if attributive and (suffix or label is not None or plural_label is not None): + raise ValueError( + "fmt_count attributive=True formats only the scaled modifier; " + "put the count noun in prose." + ) if scale_style not in {"symbol", "word"}: raise ValueError( "fmt_count scale_style must be 'symbol' or 'word', " @@ -682,19 +730,18 @@ def fmt_count( ) if scale is None and scale_style != "symbol": raise ValueError("fmt_count scale_style='word' requires scale=.") + if scale is None and attributive: + raise ValueError("fmt_count attributive=True requires scale=.") v = raw_v glyph = "" if scale is not None: - if scale not in _COUNT_SCALES: - raise ValueError( - f"fmt_count scale must be one of {sorted(_COUNT_SCALES)} or " - f"None, got {scale!r}." - ) - v = v / _COUNT_SCALES[scale] - if scale_style == "word": - glyph = f" {_COUNT_SCALE_WORDS[scale]}" - else: - glyph = scale + divisor, glyph = _resolve_decimal_scale( + scale, + what="fmt_count scale", + style=scale_style, + attributive=attributive, + ) + v = v / divisor suffix = suffix or _label_suffix(raw_v, label, plural_label) return fmt( v, @@ -707,7 +754,8 @@ def fmt_count( _RATE_UNITS = { - "QPS", "FPS", "tokens/s", "img/s", "images/s", "req/s", "samples/s", + "QPS", "FPS", "tokens/s", "tokens/hour", "img/s", "images/s", "req/s", + "samples/s", } @@ -717,6 +765,7 @@ def fmt_rate( *, precision=0, commas=True, + scale=None, approx=False, lower_bound=False, allow_negative=False, @@ -725,7 +774,8 @@ def fmt_rate( Physical rates (``GB/s``, ``TFLOP/s``, ``W``) remain ``fmt_qty`` values. This helper is for named service/data rates whose numerator is a counted - event rather than a Pint physical unit. + event rather than a Pint physical unit. ``scale=`` accepts the same checked + decimal scales as ``fmt_count``. """ unit = _clean_text_atom(unit, what="rate unit") if unit not in _RATE_UNITS: @@ -738,11 +788,18 @@ def fmt_rate( f"fmt_rate expects a non-negative rate, got {v}. Pass " f"allow_negative=True if a signed rate is genuinely intended." ) + scale_suffix = "" + if scale is not None: + divisor, scale_suffix = _resolve_decimal_scale( + scale, + what="fmt_rate scale", + ) + v = v / divisor return fmt( v, precision=precision, commas=commas, - suffix=f" {unit}", + suffix=f"{scale_suffix} {unit}", approx=approx, lower_bound=lower_bound, ) @@ -920,14 +977,12 @@ def fmt_count_range( hi_count = hi_raw glyph = "" if scale is not None: - if scale not in _COUNT_SCALES: - raise ValueError( - f"fmt_count_range scale must be one of {sorted(_COUNT_SCALES)} " - f"or None, got {scale!r}." - ) - lo_raw = lo_raw / _COUNT_SCALES[scale] - hi_raw = hi_raw / _COUNT_SCALES[scale] - glyph = scale + divisor, glyph = _resolve_decimal_scale( + scale, + what="fmt_count_range scale", + ) + lo_raw = lo_raw / divisor + hi_raw = hi_raw / divisor a = fmt(lo_raw, precision=precision, commas=commas) b = fmt(hi_raw, precision=precision, commas=commas) suffix = "" diff --git a/mlsysim/tests/test_fmt.py b/mlsysim/tests/test_fmt.py index 87a74a7a59..9ab54ce786 100644 --- a/mlsysim/tests/test_fmt.py +++ b/mlsysim/tests/test_fmt.py @@ -177,6 +177,14 @@ class TestFmtUsd: def test_structured_scale_and_denominator(self): assert fmt_usd(4_600_000, precision=1, commas=False, scale="M") == "\\$4.6M" + assert ( + fmt_usd(4_750_000, precision=2, commas=False, scale="million") + == "\\$4.75 million" + ) + assert ( + fmt_usd(4_750_000, precision=2, commas=False, scale="Million") + == "\\$4.75 Million" + ) assert fmt_usd(0.09, precision=2, commas=False, per="GB") == "\\$0.09/GB" assert fmt_usd(12_000, commas=False, scale="K", per="year") == "\\$12K/year" assert fmt_usd(8000, approx=True, marker="*") == "~\\$8,000*" @@ -220,6 +228,20 @@ class TestFmtRate: def test_formats_allowlisted_service_rates(self): assert fmt_rate(2500, "QPS") == "2,500 QPS" assert fmt_rate(1200, "tokens/s") == "1,200 tokens/s" + assert ( + fmt_rate(500_000, "tokens/s", scale="K", commas=False) + == "500K tokens/s" + ) + assert ( + fmt_rate( + 45_200_000, + "tokens/hour", + scale="million", + precision=1, + commas=False, + ) + == "45.2 million tokens/hour" + ) assert fmt_rate(2500, "QPS", commas=False) == "2500 QPS" assert fmt_rate(60, "FPS") == "60 FPS" @@ -489,11 +511,12 @@ class TestFmtCount: def test_scale_words(self): assert fmt_count(1_000_000, scale="M", scale_style="word") == "1 million" + assert fmt_count(1_000_000, scale="million") == "1 million" + assert fmt_count(1_000_000, scale="Million") == "1 Million" assert ( fmt_count( 60_000_000, - scale="M", - scale_style="word", + scale="million", label="parameter", ) == "60 million parameters" @@ -501,8 +524,7 @@ class TestFmtCount: assert ( fmt_count( 70_000_000_000, - scale="B", - scale_style="word", + scale="billion", precision=0, commas=False, label="parameter", @@ -510,6 +532,27 @@ class TestFmtCount: == "70 billion parameters" ) + def test_scale_word_attributive_modifier(self): + assert ( + fmt_count( + 7_000_000_000, + scale="billion", + precision=0, + commas=False, + attributive=True, + ) + == "7-billion" + ) + with pytest.raises(ValueError, match="requires scale"): + fmt_count(7, attributive=True) + with pytest.raises(ValueError, match="only the scaled modifier"): + fmt_count( + 7_000_000_000, + scale="billion", + label="parameter", + attributive=True, + ) + def test_scale_inherits_precision_guard(self): # 5.3M at precision=0 would silently hide the .3 — guard refuses. with pytest.raises(ValueError, match="not integer-like"): @@ -614,6 +657,16 @@ class TestTypedRanges: ) == "1M\u20132M parameters" ) + assert ( + fmt_count_range( + 1_000_000, + 2_000_000, + scale="million", + label="parameter", + commas=False, + ) + == "1 million\u20132 million parameters" + ) def test_usd_range_supports_scale_and_denominator(self): assert (