Migrate compound scale suffixes

This commit is contained in:
Vijay Janapa Reddi
2026-05-31 12:45:31 -04:00
parent 8cd9290914
commit 8535ba27fe
11 changed files with 289 additions and 128 deletions
@@ -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.
:::
@@ -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")
@@ -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")
```
@@ -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")
```
@@ -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')
+35 -2
View File
@@ -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
+31 -16
View File
@@ -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 01↔0100 / 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
+27 -15
View File
@@ -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 <qmd>`
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`,
+20 -26
View File
@@ -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
+98 -43
View File
@@ -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 = ""
+57 -4
View File
@@ -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 (