altamt 1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. altamt-1.0/LICENSE +15 -0
  2. altamt-1.0/PKG-INFO +126 -0
  3. altamt-1.0/README.md +361 -0
  4. altamt-1.0/README_pypi.md +77 -0
  5. altamt-1.0/pyproject.toml +76 -0
  6. altamt-1.0/setup.cfg +4 -0
  7. altamt-1.0/setup.py +4 -0
  8. altamt-1.0/src/altamt/__init__.py +70 -0
  9. altamt-1.0/src/altamt/benchmarking/__init__.py +27 -0
  10. altamt-1.0/src/altamt/benchmarking/evaluator.py +243 -0
  11. altamt-1.0/src/altamt/benchmarking/report_generator.py +140 -0
  12. altamt-1.0/src/altamt/cli.py +799 -0
  13. altamt-1.0/src/altamt/config.py +749 -0
  14. altamt-1.0/src/altamt/data/__init__.py +45 -0
  15. altamt-1.0/src/altamt/data/dataset.py +939 -0
  16. altamt-1.0/src/altamt/data/preprocessor.py +150 -0
  17. altamt-1.0/src/altamt/data/segmenter.py +552 -0
  18. altamt-1.0/src/altamt/data/tokenizer.py +499 -0
  19. altamt-1.0/src/altamt/environment.py +323 -0
  20. altamt-1.0/src/altamt/inference/__init__.py +17 -0
  21. altamt-1.0/src/altamt/inference/engine.py +846 -0
  22. altamt-1.0/src/altamt/inference/quantize.py +238 -0
  23. altamt-1.0/src/altamt/models/__init__.py +52 -0
  24. altamt-1.0/src/altamt/models/lid.py +339 -0
  25. altamt-1.0/src/altamt/models/modules.py +290 -0
  26. altamt-1.0/src/altamt/models/transformer.py +633 -0
  27. altamt-1.0/src/altamt/training/__init__.py +33 -0
  28. altamt-1.0/src/altamt/training/trainer.py +1437 -0
  29. altamt-1.0/src/altamt/training/utils.py +313 -0
  30. altamt-1.0/src/altamt.egg-info/PKG-INFO +126 -0
  31. altamt-1.0/src/altamt.egg-info/SOURCES.txt +42 -0
  32. altamt-1.0/src/altamt.egg-info/dependency_links.txt +1 -0
  33. altamt-1.0/src/altamt.egg-info/entry_points.txt +2 -0
  34. altamt-1.0/src/altamt.egg-info/requires.txt +27 -0
  35. altamt-1.0/src/altamt.egg-info/top_level.txt +1 -0
  36. altamt-1.0/tests/test_attention_paths.py +106 -0
  37. altamt-1.0/tests/test_batching.py +224 -0
  38. altamt-1.0/tests/test_config.py +252 -0
  39. altamt-1.0/tests/test_environment.py +144 -0
  40. altamt-1.0/tests/test_inference.py +462 -0
  41. altamt-1.0/tests/test_model.py +134 -0
  42. altamt-1.0/tests/test_segmenter.py +181 -0
  43. altamt-1.0/tests/test_tokenizer.py +158 -0
  44. altamt-1.0/tests/test_trainer.py +131 -0
altamt-1.0/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
altamt-1.0/PKG-INFO ADDED
@@ -0,0 +1,126 @@
1
+ Metadata-Version: 2.4
2
+ Name: altamt
3
+ Version: 1.0
4
+ Summary: ALTAMT: Advanced Lightweight Translation AI Model Transformer - bidirectional Kinyarwanda <-> English machine translation optimized for CPU inference.
5
+ Author: ALTAMT Contributors
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/altamt-ai/altamt
8
+ Project-URL: Documentation, https://github.com/altamt-ai/altamt#readme
9
+ Project-URL: Issues, https://github.com/altamt-ai/altamt/issues
10
+ Keywords: machine-translation,kinyarwanda,nlp,transformer,low-resource,onnx,quantization
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Classifier: Topic :: Text Processing :: Linguistic
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: torch>=2.1
26
+ Requires-Dist: sentencepiece>=0.1.99
27
+ Requires-Dist: numpy>=1.24
28
+ Requires-Dist: pandas>=2.0
29
+ Requires-Dist: pyarrow>=14.0
30
+ Requires-Dist: sacrebleu>=2.4
31
+ Requires-Dist: pyyaml>=6.0
32
+ Requires-Dist: tqdm>=4.66
33
+ Provides-Extra: onnx
34
+ Requires-Dist: onnx>=1.15; extra == "onnx"
35
+ Requires-Dist: onnxruntime>=1.17; extra == "onnx"
36
+ Provides-Extra: benchmark
37
+ Requires-Dist: transformers>=4.40; extra == "benchmark"
38
+ Requires-Dist: evaluate>=0.4; extra == "benchmark"
39
+ Requires-Dist: psutil>=5.9; extra == "benchmark"
40
+ Provides-Extra: dev
41
+ Requires-Dist: pytest>=7.4; extra == "dev"
42
+ Requires-Dist: ruff>=0.4; extra == "dev"
43
+ Requires-Dist: mypy>=1.8; extra == "dev"
44
+ Requires-Dist: build; extra == "dev"
45
+ Requires-Dist: twine; extra == "dev"
46
+ Provides-Extra: all
47
+ Requires-Dist: altamt[benchmark,dev,onnx]; extra == "all"
48
+ Dynamic: license-file
49
+
50
+ # altamt
51
+
52
+ **Bidirectional Kinyarwanda ⇄ English machine translation — fast on your CPU.**
53
+
54
+ `altamt` (Advanced Lightweight Translation AI Model Transformer) is a compact (~130–195M parameter) modern Transformer (RMSNorm · SwiGLU · Grouped-Query Attention · RoPE) that translates in **both directions with one model** and **auto-detects the input language**. No GPU required.
55
+
56
+ ## Installation
57
+
58
+ ```bash
59
+ pip install altamt
60
+ # optional extras
61
+ pip install "altamt[onnx]" # ONNX Runtime export & inference
62
+ pip install "altamt[benchmark]" # compare against NLLB / Opus-MT baselines
63
+ ```
64
+
65
+ ## Quickstart (Python)
66
+
67
+ ```python
68
+ from altamt import Translator
69
+
70
+ translator = Translator(model_path="path/to/checkpoint")
71
+
72
+ # Language auto-detection: just pass text.
73
+ result = translator.translate("Mwaramutse nshuti zanjye")
74
+ print(result)
75
+ # {'translated_text': 'Good morning my friends',
76
+ # 'detected_src': 'rw', 'tgt': 'en', 'latency_ms': 42.1}
77
+
78
+ # Or pin the direction explicitly:
79
+ translator.translate("How are you today?", src_lang="en", tgt_lang="rw")
80
+
81
+ # Batch translation — directions can even be mixed in one batch:
82
+ translator.translate_batch(["Mwaramutse", "Good morning"])
83
+ ```
84
+
85
+ ### Make it faster: INT8 quantization
86
+
87
+ ```python
88
+ translator = Translator("path/to/checkpoint", quantize=True, num_threads=8)
89
+ translator.warmup()
90
+ ```
91
+
92
+ INT8 dynamic quantization typically gives ~2× lower latency and ~4× smaller weight matrices on x86 CPUs, with a negligible quality drop.
93
+
94
+ ## Quickstart (CLI)
95
+
96
+ ```bash
97
+ altamt translate "Mwaramutse nshuti zanjye" --model path/to/checkpoint
98
+ # Good morning my friends
99
+ # [rw -> en | 42.1 ms]
100
+
101
+ altamt translate "Hello" --model path/to/checkpoint --tgt-lang rw --int8 --json
102
+
103
+ altamt benchmark --model path/to/checkpoint --test-file test.parquet --int8
104
+ altamt export-onnx --model path/to/checkpoint --output-dir ./onnx_model
105
+ altamt train --config config.yaml
106
+ ```
107
+
108
+ ## Why altamt?
109
+
110
+ - **One model, both directions** — a target-language tag (`<2en>` / `<2rw>`) steers the decoder, so RW→EN and EN→RW share all parameters and vocabulary.
111
+ - **Automatic language detection** — a built-in, microsecond-fast character n-gram detector picks the direction when you don't.
112
+ - **Built for CPU** — Grouped-Query Attention + full KV-cached decoding + INT8 quantization target <100 ms per sentence on a modern 8-core CPU (beam=1, short sentences).
113
+ - **Multi-format data** — training and benchmarking read `.json`, `.jsonl` and `.parquet` interchangeably.
114
+ - **Honest benchmarking** — `altamt benchmark` reports SacreBLEU, chrF++, latency, throughput and memory, and can run NLLB/Opus-MT baselines through the same harness, emitting Markdown and LaTeX tables.
115
+
116
+ ## Indicative CPU performance
117
+
118
+ Numbers depend on your hardware, sentence length, beam size and thread count; measure on your machine with `altamt benchmark`. On a modern 8-core x86 CPU (beam=1, ~20-token sentences, `num_threads=8`), the INT8 `base` model targets **<100 ms/sentence**, with fp32 roughly 2× slower and ~780 MB peak RAM.
119
+
120
+ ## Documentation
121
+
122
+ Full architecture details, data format specs, training and paper-publishing guides live in the [GitHub README](https://github.com/altamt-ai/altamt#readme).
123
+
124
+ ## License
125
+
126
+ Apache 2.0
altamt-1.0/README.md ADDED
@@ -0,0 +1,361 @@
1
+ # ALTAMT — Advanced Lightweight Translation AI Model Transformer
2
+
3
+ **Multilingual machine translation centered on Kinyarwanda ⇄ English, engineered for sub-100ms CPU inference.**
4
+
5
+ One shared model can now serve any set of language pairs (e.g. `rw-en`, `en-rw`, `en-fr`, `fr-en`), auto-detect the source language at inference, and transparently **pivot** through a bridge language (default English) for pairs it was never trained on (`rw→fr` via `rw→en→fr`). Training scales from a laptop to multi-node clusters via **DDP** and **DeepSpeed**.
6
+
7
+ This is the developer README (architecture, data, training, benchmarking, publishing). For the user-facing quickstart, see [`README_pypi.md`](README_pypi.md).
8
+
9
+ ---
10
+
11
+ ## 1. Architecture
12
+
13
+ ALTAMT is a modern encoder–decoder Transformer. Every architectural decision below exists either to raise quality per parameter (low-resource setting) or to cut CPU decoding latency.
14
+
15
+ ### 1.1 Components
16
+
17
+ | Component | Choice | Why |
18
+ |---|---|---|
19
+ | Normalization | **RMSNorm**, pre-norm | Cheaper than LayerNorm (no mean/bias), more stable deep stacks |
20
+ | FFN | **SwiGLU** | Consistently better quality/param than ReLU/GELU FFNs |
21
+ | Attention | **Grouped-Query Attention** (12 Q heads / 4 KV heads in `base`) | 3× smaller KV projections and KV cache → the decode loop is memory-bandwidth-bound on CPU, so this directly cuts latency |
22
+ | Positions | **RoPE** in encoder & decoder *self*-attention | No learned position table, clean length extrapolation. Cross-attention is position-free (standard) |
23
+ | Embeddings | **Three-way weight tying** (encoder emb = decoder emb = LM head) | Saves ~24M params at 32k vocab; regularizes the low-resource setting |
24
+ | Kernel | `torch.scaled_dot_product_attention` + **FlashAttention** | Causality is declared via `is_causal=True` (training) or omitted entirely (single-token cached steps, unpadded batches) instead of materializing mask tensors — mask-free calls are what makes the flash kernel eligible; masked encoder/cross attention uses the fused memory-efficient kernel. Combined with GQA via `enable_gqa=True`. A test suite pins all paths (full pass, cached steps, chunked prefill, padded/unpadded) to identical numerics |
25
+ | Decoding | **Full KV cache** (growing self-attn cache + one-shot cross-attn cache) | O(T) decoding instead of O(T²) forwards — the single biggest CPU win |
26
+ | Compression | **INT8 dynamic quantization** + **ONNX export** | ~2× speedup / ~4× smaller weights on x86; portable runtime |
27
+
28
+ ### 1.2 One model, many directions
29
+
30
+ Instead of one model per pair, ALTAMT trains a **single shared model** tagged on **both** sides (NLLB style):
31
+
32
+ ```
33
+ encoder input : <2rw> SP(src_text) </s> # source-language tag
34
+ decoder input : <2en> SP(tgt_text) # forced-BOS target-language tag
35
+ labels : SP(tgt_text) </s>
36
+ ```
37
+
38
+ The **target** tag is the decoder's forced first token: generation simply *starts* from the right tag, so a **single batch can mix arbitrary directions** (each row carries its own start tag) — which `Translator.translate_batch` exploits, including for pivot-routed rows.
39
+
40
+ The **source** tag matters once you have more than two languages. Without it the encoder must infer the input language from content alone, and that is precisely where same-script pairs lose accuracy: English and French share the alphabet, punctuation, digits, proper nouns and thousands of cognates, so a short or entity-heavy sentence is genuinely ambiguous. Both tags come from the same `<2xx>` piece inventory — position disambiguates them — so no tokenizer change is needed. The setting lives in `data.add_src_lang_tag` and is **recorded in the checkpoint's `vocab_config.json`**, so `Translator` configures itself to match how the model was trained; a silent mismatch here degrades quality with no error, which is why it is never left to the caller to remember.
41
+
42
+ Language tags are **dynamic**: one `<2xx>` piece per language in `data.languages` is reserved as a SentencePiece user-defined symbol at contiguous ids starting at 4, and both tokenizer and model self-discover the language set from the vocabulary at load time — nothing about `rw`/`en` is hard-coded.
43
+
44
+ All languages share one 32k SentencePiece BPE vocabulary (`byte_fallback=True`, `character_coverage=1.0`), so cognates, named entities and numbers share subwords, and no input can ever fail to encode. The pair inventory a checkpoint was actually trained on is recorded in its bundled `vocab_config.json` (`trained_pairs`) and drives pivot routing at inference — with direct `rw↔fr` bitext in the corpus, that pair is trained directly and no pivot (and no compounding of pivot errors) is involved.
45
+
46
+ ### 1.3 Presets & parameter budget
47
+
48
+ | Preset | d_model | layers (enc/dec) | heads (Q/KV) | d_ff | Params |
49
+ |---|---|---|---|---|---|
50
+ | `small` (`configs/small.yaml`) | 640 | 10 / 10 | 10 / 2 | 2048 | ~130M |
51
+ | `base` (`configs/base.yaml`) | 768 | **16 / 8** | 12 / 4 | 3072 | ~245M |
52
+ | `tiny` (tests/CI only) | 64 | 2 / 2 | 4 / 2 | 128 | ~0.2M |
53
+
54
+ `base` uses a **deep encoder and a shallow decoder** rather than a symmetric stack. For translation this is close to free quality: encoder depth is where translation accuracy comes from, while autoregressive decoding cost scales with *decoder* depth × output length. Moving layers from the decoder to the encoder therefore raises quality **and** keeps decoding fast — 16/8 at `d_ff=3072` decodes at roughly the same speed as the old symmetric 12/12 at `d_ff=2048` while carrying substantially more encoder capacity.
55
+
56
+ `small` is deliberately a good **distillation student**: a strong recipe is to distill from NLLB-200-1.3B/3.3B teacher translations of monolingual Kinyarwanda text (sequence-level KD), then fine-tune on clean bitext.
57
+
58
+ ### 1.4 Language identification
59
+
60
+ `altamt.models.lid.LanguageDetector` is a **dependency-free** character-trigram Naive Bayes classifier with a function-word prior. It ships with built-in seed profiles for `rw`, `en` and `fr` (works out of the box, microsecond latency) and can be retrained on your corpora:
61
+
62
+ ```python
63
+ from altamt.models import LanguageDetector
64
+ det = LanguageDetector.train({"rw": ["mono.rw.txt"], "en": ["mono.en.txt"]})
65
+ det.save("lid_profile.json")
66
+ ```
67
+
68
+ It powers **source-language auto-detection at inference**: when `src_lang` is omitted, the `Translator` restricts the detector to the model's supported languages (minus the requested target) via `detect(text, allowed=...)`, and prints a stderr notice such as `[altamt] auto-detected source language: 'rw'`. It is also used (optionally) as a **bitext quality filter** in preprocessing (`clean_corpus(lid_filter=True)`), dropping pairs whose source text clearly isn't in the declared source language.
69
+
70
+ ---
71
+
72
+ ## 2. Repository layout
73
+
74
+ ```
75
+ altamt/
76
+ ├── pyproject.toml # packaging (setuptools, src layout)
77
+ ├── configs/ # ready-to-edit training configs
78
+ ├── src/altamt/
79
+ │ ├── cli.py # altamt {train,translate,translate-doc,benchmark,export-onnx,quantize,train-tokenizer,doctor}
80
+ │ ├── config.py # typed dataclasses + YAML I/O + presets
81
+ │ ├── data/ # dataset.py (json/jsonl/parquet), preprocessor.py, tokenizer.py
82
+ │ ├── models/ # modules.py (RMSNorm/SwiGLU/RoPE/GQA), transformer.py, lid.py
83
+ │ ├── training/ # trainer.py (AMP, cosine LR, checkpoints), utils.py
84
+ │ ├── inference/ # engine.py (Translator), quantize.py (INT8 + ONNX)
85
+ │ └── benchmarking/ # evaluator.py (BLEU/chrF++/latency/RAM), report_generator.py
86
+ └── tests/ # pytest suite (tiny model, trains a 300-piece tokenizer)
87
+ ```
88
+
89
+ ## 3. Developer setup
90
+
91
+ ```bash
92
+ git clone https://github.com/altamt-ai/altamt && cd altamt
93
+ python -m venv .venv && source .venv/bin/activate
94
+ pip install -e ".[all]" # core + onnx + benchmark + dev
95
+ pytest # full suite runs on CPU in <1 min
96
+ ```
97
+
98
+ ## 4. Data format specification
99
+
100
+ All loaders accept **`.json`**, **`.jsonl`** and **`.parquet`** interchangeably (mixable within one run). Required columns/keys:
101
+
102
+ | key | type | values |
103
+ |---|---|---|
104
+ | `src_lang` | str | any code in `data.languages` (e.g. `"rw"`, `"en"`, `"fr"`) |
105
+ | `tgt_lang` | str | any code in `data.languages` |
106
+ | `src_text` | str | source sentence |
107
+ | `tgt_text` | str | reference translation |
108
+
109
+ The language inventory is declared once in the config (`data.languages: [rw, en, fr]`); rows in other languages are rejected at load time, and mixing many pairs in one corpus (or across several `train_files`) is fully supported — each row is its own direction.
110
+
111
+ **Every pair is trained both ways.** By default (`data.ensure_bidirectional: true`) any direction whose reverse is missing gets flipped rows added automatically at load time — a corpus with `en->fr` rows but no `fr->en` rows trains `fr->en` too (you'll see a `[altamt] added missing reverse direction(s) fr-en ...` notice). Pairs that already exist in both directions are never duplicated, unlike `add_reverse_direction: true`, which flips *every* row (use that when your corpus deliberately lists each pair only once). The guarantee applies to the validation set as well, so eval metrics and sample blocks cover both directions of every pair.
112
+
113
+ **JSONL** (recommended for large corpora):
114
+ ```json
115
+ {"src_lang": "rw", "tgt_lang": "en", "src_text": "Mwaramutse", "tgt_text": "Good morning"}
116
+ ```
117
+
118
+ **JSON** — either a top-level array of such records, or `{"data": [...]}`.
119
+
120
+ **Parquet** — same four columns; written e.g. via `df.to_parquet("train.parquet")`.
121
+
122
+ If your corpus lists each pair only once, set `data.add_reverse_direction: true` to train both directions from it.
123
+
124
+ ### Recommended public sources
125
+ Digital Umuganda / Mbaza datasets, JW300 (rw–en), Flores-200 dev/devtest (evaluation), TICO-19, plus back-translated monolingual Kinyarwanda news/wiki text. Always hold Flores-200 devtest out for reporting.
126
+
127
+ ### Cleaning
128
+ ```python
129
+ from altamt.data import load_parallel_files, clean_corpus
130
+ df = load_parallel_files(["raw.parquet", "extra.jsonl"])
131
+ df, stats = clean_corpus(df, max_len_ratio=3.0, lid_filter=True)
132
+ print(stats.summary())
133
+ df.to_parquet("data/train.parquet")
134
+ ```
135
+
136
+ ## 5. Training locally
137
+
138
+ ```bash
139
+ # 1. Train the shared 32k tokenizer straight from dataset files.
140
+ # --output may be a DIRECTORY: all artifacts (altamt.model, altamt.vocab,
141
+ # vocab_config.json) are written directly into it.
142
+ altamt train-tokenizer --files data/train.parquet --output tokenizer/ \
143
+ --languages rw en fr
144
+
145
+ # ...or drive it entirely from the config's tokenizer: section
146
+ # (corpus = data.train_files, output = data.tokenizer_path,
147
+ # languages = data.languages):
148
+ altamt train-tokenizer --config configs/base.yaml
149
+
150
+ # 2. Edit configs/base.yaml (paths, batch size), then:
151
+ altamt train --config configs/base.yaml
152
+ ```
153
+
154
+ **Terminal overrides.** Every configuration value can be overridden on the command line — CLI values take precedence over the YAML file, are type-checked against the config schema, re-validated exactly like YAML values, and echoed at startup (`[altamt] config override: training.lr: 0.0005 -> 0.0001`). Two equivalent syntaxes, freely mixable:
155
+
156
+ ```bash
157
+ altamt train --config configs/base.yaml \
158
+ --set training.lr=1e-4 -s training.batch_size=32 -s data.languages=rw,en,fr
159
+
160
+ altamt train --config configs/base.yaml \
161
+ --training.lr 1e-4 --training.batch_size 32 --data.languages rw,en,fr
162
+ ```
163
+
164
+ Bare field names work when unique across sections (`-s lr=1e-4`); ambiguous ones (`vocab_size` exists in both `model` and `tokenizer`) must be qualified. Lists accept CSV or YAML (`rw,en,fr` / `"[rw, en, fr]"`), booleans accept `true/false/yes/no/on/off/1/0`, and `null` clears optional values (`-s training.finetune_from=null`). Typos get did-you-mean suggestions. `altamt train-tokenizer` supports the same mechanism (`--tokenizer.vocab_size 16000`), with or without `--config`.
165
+
166
+ The most common data/output settings also have **dedicated flags** — usable for fresh training, `--resume-from` runs, and `--finetune-from` runs alike, and taking precedence over `--set`:
167
+
168
+ ```bash
169
+ altamt train --config configs/base.yaml \
170
+ --train-files data/a.parquet data/b.jsonl \
171
+ --valid-files data/val.jsonl \
172
+ --tokenizer-path tokenizer/altamt.model \
173
+ --languages rw en fr \
174
+ --output-dir runs/exp2
175
+ ```
176
+
177
+ File lists and languages accept space- or comma-separated values; `--valid-files none` disables validation for the run. Like every override, dedicated flags are validated against the config schema, echoed at startup, and baked into the `config.yaml` written to the run directory and its checkpoints.
178
+
179
+ **Adding languages to an existing tokenizer.** `--extend-from old_tokenizer/` retrains on the new corpus while preserving the original language-tag order and appending new `<2xx>` tags, so existing tag ids stay stable; `build_embedding_remap(old, new)` then maps shared pieces so pretrained embeddings can be transplanted.
180
+
181
+ **Fine-tuning.** Set `training.finetune_from: checkpoints/altamt-base/best` (or pass `--finetune-from`) to initialize from a pretrained checkpoint: weights are loaded, a fresh optimizer/scheduler is built, `global_step` restarts at 0, and regularization knobs (`dropout`, `attention_dropout`, `label_smoothing`) are taken from *your* config while the architecture comes from the checkpoint. If your tokenizer differs from the checkpoint's (e.g. you extended it with French), embeddings are automatically remapped and resized — new pieces get fresh init, shared pieces keep their trained rows. This is exactly how you add a language pair to an existing bilingual model.
182
+
183
+ **Resuming interrupted training.** Any checkpoint directory saved by the trainer is fully resumable — just point `--resume-from` (or `training.resume_from`) at it:
184
+
185
+ ```bash
186
+ # resume from a checkpoint directory (best/ or last/, or a shared copy of one)
187
+ altamt train --config configs/base.yaml --resume-from runs/base/best
188
+
189
+ # resume from a rotating step checkpoint, or the run dir (auto-picks the newest)
190
+ altamt train --config configs/base.yaml --resume-from runs/base/step-24000.pt
191
+ altamt train --config configs/base.yaml --resume-from runs/base
192
+
193
+ # DeepSpeed runs resume from their ZeRO-sharded engine checkpoints
194
+ altamt train --config configs/base.yaml --resume-from runs/base/ds_checkpoints
195
+ altamt train --config configs/base.yaml --resume-from runs/base/ds_checkpoints@step-24000
196
+ ```
197
+
198
+ Resume restores everything mid-run: model weights, optimizer and LR-scheduler state, the AMP grad scaler, `global_step`, the best validation loss, and the accumulated **total training time** (logs and `metrics.jsonl` keep counting from where the previous run stopped; the startup notice shows the prior training time). Checkpoint directories from older versions of this code lack `training_state.pt` (weights only); resuming from one produces an error naming the correct step checkpoint. Use `--finetune-from` instead of `--resume-from` when you want to start a *fresh* run (new schedule, step 0) initialized from a checkpoint's weights.
199
+
200
+ **Distributed training.** `training.distributed: auto` (default) picks up `torchrun`/`deepspeed` launch environments automatically; `none`, `ddp` and `deepspeed` force a mode. Gradients sync via DDP or DeepSpeed ZeRO (`deepspeed_stage: 2` default, stage 3 supported, or point `deepspeed_config` at your own JSON); data is sharded with a `DistributedSampler`, validation losses are all-reduced, and logging/samples/checkpoint writes happen on rank 0 (ZeRO-3 weight consolidation is handled collectively).
201
+
202
+ ```bash
203
+ # single node, 4 GPUs
204
+ torchrun --nproc-per-node 4 -m altamt.cli train --config configs/base.yaml
205
+
206
+ # 2 nodes × 8 GPUs
207
+ torchrun --nnodes 2 --nproc-per-node 8 --rdzv-backend c10d \
208
+ --rdzv-endpoint $MASTER:29500 -m altamt.cli train --config configs/base.yaml
209
+
210
+ # DeepSpeed ZeRO
211
+ deepspeed --num_gpus 8 -m altamt.cli train --config configs/base.yaml
212
+ ```
213
+
214
+ **Multi-GPU efficiency.** Gradients are all-reduced **once per optimizer step** (non-final accumulation micro-batches run under `DDP.no_sync()`), buffer broadcasts are disabled (all buffers are deterministic caches), gradients live directly in the allreduce buckets (`gradient_as_bucket_view`), and the NCCL communicator is bound to each rank's GPU explicitly (`device_id` in `init_process_group` — this also removes the *"Guessing device ID based on global rank"* warning). The **quality eval is sharded across ranks**: every rank decodes a disjoint slice of the selected validation sentences and the shards are merged with one `all_gather`, so all GPUs work on the eval *and* no rank ever idles at a barrier while another decodes alone — the latter is what triggers NCCL's 10-minute watchdog (*"Watchdog caught collective operation timeout"*) and kills the whole run. As a backstop the collective timeout is configurable (`training.collective_timeout_min`, default 120). The startup header proves the rank→GPU mapping, e.g. `device cuda:0 + cuda:1 (2× NVIDIA H200)`. Every progress log line reports **`data-wait %`** — the fraction of time the GPU sat idle waiting for batches; if it exceeds ~30% you'll get a one-time hint to raise `data.num_workers` (workers are persistent and prefetch 4 batches each). Two heartbeats print at the start of every run: the first-step time (includes pipeline warmup, so its projection is pessimistic) and, at step 10, the **steady-state s/step with a warmup-free ETA** — trust the second one. If per-step time stays high with low data-wait on a multi-GPU cloud box, the interconnect is the limit: check `nvidia-smi topo -m` (PCIe-only pairs without P2P pay heavily per allreduce; larger `grad_accum_steps` amortizes it further).
215
+
216
+ **If the startup header says `device cpu` on a GPU machine, stop the run.** It means `torch.cuda.is_available()` returned False — training a full-size model on CPU takes minutes per optimizer step and looks frozen. altamt now handles this end to end with the **environment doctor**:
217
+
218
+ ```bash
219
+ altamt doctor # diagnose: wheel CUDA vs driver CUDA, GPUs, status, exact fix
220
+ altamt doctor --fix # reinstall the torch wheel matching your driver (asks first; -y to skip)
221
+ altamt train ... --auto-fix-torch # single-process runs: fix + relaunch automatically
222
+ ```
223
+
224
+ The doctor distinguishes the cases precisely: `driver_too_old` (e.g. `torch 2.13.0+cu130` on a CUDA-12.8 driver — the classic silent-CPU trap), `cpu_build` (CPU-only torch on a GPU machine), and `mismatch` (compatible wheel but CUDA still blocked — container without `--gpus`, `CUDA_VISIBLE_DEVICES`, etc.; diagnosed but never blindly reinstalled). It computes the newest published wheel your driver can run (cu126/cu128/cu130/...) and prints the exact `pip` commands. `--fix` verifies success in a fresh interpreter afterwards. Safety rails: the fix never runs from inside a multi-rank `torchrun`/`deepspeed` launch (N ranks would race pip — run `altamt doctor --fix` once standalone instead), and `--auto-fix-torch` re-execs the training command after reinstalling so the new build is actually used.
225
+
226
+ **CPU machines and CPU inference stay first-class and quiet.** A box with no GPUs gets status `ok_cpu` — no warnings, no fix suggestions; `Translator` defaults to `device="cpu"` silently (that's the optimized path). Requesting `device="cuda"` at inference when CUDA is unavailable raises a clear error containing the fix; `device="auto"` falls back to CPU, adding a single stderr note only when GPUs are physically present but unusable (suppressed with `verbose=False`). Set `training.device: cuda` (or pass `--training.device cuda`) to make silent CPU fallback a hard error during training.
227
+
228
+ Tokenizer hyper-parameters (`vocab_size`, `model_type`, `character_coverage`, `byte_fallback`, `hard_vocab_limit`, ...) live in the `tokenizer:` section of the YAML config (`altamt.config.TokenizerConfig`); CLI flags like `--vocab-size` override it. If SentencePiece reports *"Vocabulary size too high (32000). Please set it to a value <= N"*, your corpus is too small for that many pieces — lower `tokenizer.vocab_size` to ≤ N, add more data, or pass `--no-hard-vocab-limit` (config: `hard_vocab_limit: false`) to auto-shrink to the largest achievable vocabulary. `altamt train` then reconciles `model.vocab_size` with the trained tokenizer automatically.
229
+
230
+ **Batching: token budget, not sentence count.** The corpus spans 3–350 words per sentence, and a padded batch is as wide as its longest member — so a fixed *sentence* count spends most of every tensor on padding. On this corpus, 64-sentence random batches are only **~24 % real tokens**. `training.max_tokens` switches to length-grouped, token-budget batching (`TokenBucketBatchSampler`: shuffle → chunk → sort by length within a chunk → fill to the token budget → shuffle batch order), which reaches **~97 % real tokens**. At the same peak tensor size that is **~4× less padded compute per epoch** — the largest quality-per-hour lever in this trainer. `batch_size` then acts only as an upper bound on sentences per batch, and step counts are equalized across ranks so DDP can never deadlock on a ragged tail.
231
+
232
+ **Data is tokenized once.** Both sides of the corpus are encoded up front into flat `int32` arrays and cached on disk (`.altamt_cache/`, keyed on file mtimes + tokenizer + options), so `__getitem__` is a pure array slice. SentencePiece leaves the hot loop entirely (`data-wait %` goes to ~0 with `num_workers: 2`), exact token lengths are known before training, and over-long pairs can be **dropped** rather than truncated — `length_policy: drop`. This matters: truncating a target teaches the model to stop mid-sentence, which later shows up as unfinished translations.
233
+
234
+ The trainer gives you bf16/fp16 autocast, gradient accumulation (effective batch = `max_tokens × grad_accum_steps × world_size`), linear-warmup + cosine LR, label smoothing 0.1, grad clipping, JSONL metric logs (`metrics.jsonl`), rotating step checkpoints (`step-N.pt`), and **fully self-contained, shareable** `best/` and `last/` checkpoint directories bundling: `model.pt` (weights) + `training_state.pt` (optimizer, LR scheduler, grad scaler, step counter, best val loss, cumulative training time) + `config.json` + `config.yaml` + the SentencePiece tokenizer + `vocab_config.json` (languages, tag ids, trained pairs) + `training_summary.json` (provenance). One directory is everything anyone needs to **serve** (`Translator(path)`), **resume** (`--resume-from path`), or **fine-tune** (`--finetune-from path`) — delete `training_state.pt` if you want a smaller inference-only copy. (DeepSpeed runs shard optimizer state across ranks, so they resume from `ds_checkpoints` instead.)
235
+
236
+ Terminal logging shows **total training time** (cumulative across resumed runs), progress %, loss/ppl/lr/grad-norm/tokens-per-sec, `data-wait %`, and a live ETA on every log line. Time is tracked as a first-class metric alongside ETA: each progress line leads with the labeled time trained so far and ends with the projected total wall time, e.g.
237
+
238
+ ```
239
+ [1d 02:13 trained] step 20,000/100,000 20.0% | loss 2.0000 | ppl 7.4 | lr 5.00e-04 | grad 0.50 | 24,000 tok/s | data-wait 5% | eta 04:26:40 | total ≈ 1d 06:40
240
+ ```
241
+
242
+ so "we've been training for a day, ~4.5 hours to go, ~1d 6h total" is readable at a glance (and stays correct across `--resume-from`, since elapsed time is cumulative). The same figures land in `metrics.jsonl` — `elapsed`/`elapsed_sec`, `eta`/`eta_sec`, `projected_total`/`projected_total_sec`, and `time_progress_pct` (share of the projected total already completed) — and total training time is also persisted in every step checkpoint (so it keeps accumulating after `--resume-from`) and in a `training_summary.json` inside `best/` and `last/`. Each validation pass reports `val_loss`/`val_ppl` and prints a per-direction quality table covering **every trained language pair** — the `eval_gen_samples` generation budget is split evenly across directions, so no pair is ever missing from the metrics:
243
+
244
+ ```
245
+ [02:14:09] eval @ step 24,000 val_loss 2.3141 | val_ppl 10.1 | eval took 42s | total training time 02:14:09 | ✓ new best
246
+ direction BLEU chrF++ sentences
247
+ en -> fr 21.10 44.02 85
248
+ en -> rw 25.56 48.92 85
249
+ rw -> en 28.04 46.29 86
250
+ ```
251
+
252
+ BLEU is corpus-level **SacreBLEU** and chrF++ is `CHRF(word_order=2)`; both are 0–100, higher is better (a legend prints under the first table). The same numbers are stored in `metrics.jsonl` as a readable nested record: `{"quality": {"rw-en": {"bleu": 28.04, "chrf++": 46.29, "sentences": 86}, ...}}`.
253
+
254
+ **Reading the eval numbers honestly.** Three settings decide whether the reported BLEU reflects the model or the measurement:
255
+
256
+ * `eval_gen_samples` (default **-1 = the entire validation set**, the most faithful SacreBLEU; the eval pause grows with the set size). Set e.g. `1024` for a fast fixed random sample split evenly across directions — a few dozen sentences per direction makes corpus BLEU swing ±3–5 points between evals, so don't go below ~256 per direction when comparing checkpoints.
257
+ * `gen_max_new_tokens` / `eval_length_ratio`. The budget is `min(gen_max_new_tokens, src_tokens × eval_length_ratio + 16)` — computed per batch from the actual source length. A small *fixed* budget silently truncates long hypotheses, and BLEU's brevity penalty then reports a score far below the model's real quality. This is the single most common reason in-training BLEU looks disappointing.
258
+ * `eval_beam_size` (default 1). Greedy keeps the in-training curve cheap and comparable across steps; **beam 4 typically scores 1–2 BLEU higher**. Use `altamt benchmark` for the number you quote.
259
+
260
+ The scored sentences are a **seeded random sample** per direction (`eval_seed`), not the first N rows — the head of a validation file is usually one domain or one source corpus. References are the raw target strings, not tokenizer round-trips. The trainer also prints fixed sample translations (source / hypothesis / reference — the same sentences every time, so quality progress is directly comparable). Sample blocks cover **every training direction** by default (`training.sample_directions: all`), grouped with one labeled sub-block per direction, so all trained languages (rw/en/fr/...) appear in the logs; `num_samples` sentences are shown per direction. Restrict it with a single pair (`sample_directions: rw-en`), a list (`[rw-en, en-fr]`), or `mixed` for one arbitrary block. Sample sentences come from the validation set when it has the direction and **fall back to the training set otherwise** (with an explicit note), so directions like `fr->rw`/`rw->fr` still appear even when the validation file lacks direct rows for them. Sampling defaults to every eval (`sample_every: 0`); set an explicit step count to change the cadence. `suppress_warnings: true` hides Python/torch warnings and `clear_terminal: true` clears the screen when `altamt train` starts.
261
+
262
+ **Suggested recipe (base, 2× A100/H100):** `max_tokens: 8192` per GPU × `grad_accum_steps: 2` × 2 ranks ≈ 32k tokens/step, lr 7e-4, 4k warmup, 120k steps, then optional 10–20k fine-tuning steps on the cleanest subset at lr 1e-4. On a single GPU, raise `grad_accum_steps` to 4 to keep the same effective batch. If you hit OOM, halve `max_tokens` and double `grad_accum_steps` — the effective batch (and therefore the LR) is unchanged.
263
+
264
+ ## 6. Inference & compression
265
+
266
+ ```python
267
+ from altamt import Translator
268
+ t = Translator("checkpoints/altamt-base/best", quantize=True, num_threads="auto")
269
+ t.warmup()
270
+
271
+ # tgt_lang is REQUIRED; src_lang is optional and auto-detected when omitted
272
+ # (a stderr notice reports what was detected).
273
+ r = t.translate("Mwaramutse nshuti zanjye", tgt_lang="en")
274
+ print(r["translated_text"], r["detected_src"], r["route"]) # ... rw rw->en
275
+
276
+ # Untrained pairs route through the pivot language (default "en") automatically:
277
+ r = t.translate("Mwaramutse", tgt_lang="fr") # route: rw->en->fr, pivot: en
278
+ # Batches may mix directions (per-sentence tgt_lang) and mix direct/pivoted rows:
279
+ t.translate_batch(["Mwaramutse", "Good morning"], tgt_lang=["en", "rw"])
280
+ ```
281
+
282
+ ```bash
283
+ altamt translate "Mwaramutse" --model checkpoints/altamt-base/best \
284
+ --tgt-lang en # --src-lang optional (auto-detected); --json for full records
285
+ ```
286
+
287
+ Routing uses the checkpoint's recorded `trained_pairs`: direct pairs decode in one hop; otherwise the engine pivots (`pivot_lang="en"` by default, configurable) and reports `route` (`"rw->en->fr"`), `pivot` and the intermediate `pivot_text` in the result. If no route exists it raises a `ValueError` listing the trained pairs. Passing `src_lang == tgt_lang` is an error, and the target must always be given explicitly.
288
+
289
+ ### 6.1 Documents (multi-page input)
290
+
291
+ **Long input is handled automatically.** A sentence-level model given a five-page document as one sequence is far outside its training distribution: positions past `max_src_len` were never seen during training, and the generation budget truncates the output — exactly why a single paragraph used to translate well while a long document came back garbled. `translate()` now detects input beyond the model's trained sentence length and transparently routes it through the document pipeline (segment → batch-translate → reassemble); the caller just sees the translated result. `translate_document()` remains the explicit entry point with full control (`unwrap`, `max_segment_tokens`, `progress`).
292
+
293
+ ```python
294
+ doc = open("rapport.txt", encoding="utf-8").read() # 5–20 pages is fine
295
+ r = t.translate_document(doc, tgt_lang="en", progress=True)
296
+ print(r["translated_text"]) # layout preserved
297
+ print(r["segments"], r["chars_in"], r["chars_out"])
298
+
299
+ t.translate_file("rapport.txt", tgt_lang="en") # -> rapport.en.txt
300
+ ```
301
+
302
+ ```bash
303
+ altamt translate-doc rapport.txt --model checkpoints/altamt-base/best \
304
+ --tgt-lang en -o rapport.en.txt --beam-size 4
305
+ cat report.md | altamt translate-doc --model ... --tgt-lang rw > report.rw.md
306
+ ```
307
+
308
+ What it does (`altamt.data.segmenter`):
309
+
310
+ 1. **Blocks** — splits on blank lines, keeping the exact separators so paragraph spacing survives byte-for-byte.
311
+ 2. **Lines** — hard-wrapped prose (the usual shape of text extracted from PDF/DOCX, with a newline at every rendered line) is *unwrapped* into one logical unit, while headings, bullets, numbered items and table rows stay separate. `--unwrap never` keeps every source line as its own unit; `--unwrap always` joins every multi-line paragraph.
312
+ 3. **Markers** — a leading `- `, `1. `, `## `, `(a) ` is stripped before translation and re-attached after, so the model translates prose instead of reproducing scaffolding. Titles and initials (`M. Dupont`, `J. R. R. Tolkien`) are deliberately *not* treated as list markers.
313
+ 4. **Sentences** — abbreviation-, initial- and decimal-aware splitting tuned for en/fr/rw (`fig. 3`, `3.5`, `art. 5`, `Dr. Smith` do not split).
314
+ 5. **Packing** — sentences are packed into segments up to `--segment-tokens` (default: 80 % of the checkpoint's training `max_src_len`), so several short sentences keep their mutual context while nothing ever exceeds the trained length. A single over-long sentence is split at clause boundaries first, and only then at a word-aligned hard cut.
315
+ 6. **Pass-through** — rules, bare URLs and numeric-only rows are copied verbatim rather than hallucinated over.
316
+ 7. **Batching** — every segment of the whole document is translated in **length-sorted, token-budgeted batches**, so a 20-page document is one efficient batched pass, not thousands of one-sentence forward passes.
317
+
318
+ Reassembly restores the original paragraphs, line breaks, headings and markers exactly; with an identity translation the round-trip is byte-identical.
319
+
320
+ ### 6.2 Device efficiency
321
+
322
+ `num_threads="auto"` (default) pins PyTorch to the machine's *physical* cores and disables the inter-op pool. This matters on SMT machines: the PyTorch default of one thread per logical core over-subscribes the cores and makes short-sentence decoding measurably slower. Pass an integer to override, or `None` to leave the process defaults alone.
323
+
324
+ Generation batches are formed against a token budget (`batch_tokens`, defaulting to 4 096 on CPU and 32 768 on CUDA) and divided by the beam width, since beam search multiplies the effective batch. On GPU, generation runs under bf16/fp16 autocast automatically. Grouped-query attention uses PyTorch's native `enable_gqa=True` path when available (≥ 2.5), which avoids materializing a full-width key/value tensor on every decode step — the memory traffic GQA exists to avoid. Beam search does **not** permute the cross-attention KV cache: all beams of a sentence attend to the same encoder states, so the permutation is a no-op, and skipping it removes a per-layer, per-step copy that dominated beam search on long inputs.
325
+
326
+ ### 6.3 Compression & export
327
+
328
+ ```bash
329
+ altamt quantize --model checkpoints/altamt-base/best --output-dir int8_model
330
+ altamt export-onnx --model checkpoints/altamt-base/best --output-dir onnx_model
331
+ ```
332
+
333
+ Notes: the **PyTorch INT8 path keeps the KV cache** and is the recommended low-latency route; the ONNX export produces cache-free `encoder.onnx`/`decoder.onnx` graphs (maximum portability — use `altamt.inference.OnnxTranslator`). Latency targets assume single sentences of ≤ ~40 tokens on a modern 8-core x86 CPU with `num_threads="auto"`.
334
+
335
+ ## 7. Benchmarking
336
+
337
+ ```bash
338
+ altamt benchmark \
339
+ --model checkpoints/altamt-base/best \
340
+ --test-file flores200.devtest.parquet \
341
+ --int8 \
342
+ --baseline facebook/nllb-200-distilled-600M \
343
+ --baseline Helsinki-NLP/opus-mt-rw-en \
344
+ --output-dir benchmark_results --formats md latex
345
+ ```
346
+
347
+ Per direction and per system this reports **SacreBLEU** (13a), **chrF++** (`CHRF(word_order=2)` — the more reliable metric for morphologically rich Kinyarwanda), mean **latency (ms/sentence)**, **output tokens/sec**, **peak RSS memory** and **parameter count**, and writes `report.md` plus a booktabs `report.tex` you can `\input{}` directly into a paper. Baselines run through the *same* harness (`pip install altamt[benchmark]`), so numbers are directly comparable.
348
+
349
+ ## 8. Publishing
350
+
351
+ **PyPI:**
352
+ ```bash
353
+ python -m build
354
+ twine upload dist/* # README_pypi.md is the PyPI landing page
355
+ ```
356
+
357
+ **Paper guidelines:** report chrF++ alongside BLEU (always with SacreBLEU signatures), evaluate on Flores-200 devtest for comparability, report latency with hardware + thread count + beam size, include both fp32 and INT8 rows, and release the tokenizer + config with the checkpoint for full reproducibility. The generated LaTeX table follows ACL formatting conventions.
358
+
359
+ ## 9. License
360
+
361
+ Apache 2.0.
@@ -0,0 +1,77 @@
1
+ # altamt
2
+
3
+ **Bidirectional Kinyarwanda ⇄ English machine translation — fast on your CPU.**
4
+
5
+ `altamt` (Advanced Lightweight Translation AI Model Transformer) is a compact (~130–195M parameter) modern Transformer (RMSNorm · SwiGLU · Grouped-Query Attention · RoPE) that translates in **both directions with one model** and **auto-detects the input language**. No GPU required.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install altamt
11
+ # optional extras
12
+ pip install "altamt[onnx]" # ONNX Runtime export & inference
13
+ pip install "altamt[benchmark]" # compare against NLLB / Opus-MT baselines
14
+ ```
15
+
16
+ ## Quickstart (Python)
17
+
18
+ ```python
19
+ from altamt import Translator
20
+
21
+ translator = Translator(model_path="path/to/checkpoint")
22
+
23
+ # Language auto-detection: just pass text.
24
+ result = translator.translate("Mwaramutse nshuti zanjye")
25
+ print(result)
26
+ # {'translated_text': 'Good morning my friends',
27
+ # 'detected_src': 'rw', 'tgt': 'en', 'latency_ms': 42.1}
28
+
29
+ # Or pin the direction explicitly:
30
+ translator.translate("How are you today?", src_lang="en", tgt_lang="rw")
31
+
32
+ # Batch translation — directions can even be mixed in one batch:
33
+ translator.translate_batch(["Mwaramutse", "Good morning"])
34
+ ```
35
+
36
+ ### Make it faster: INT8 quantization
37
+
38
+ ```python
39
+ translator = Translator("path/to/checkpoint", quantize=True, num_threads=8)
40
+ translator.warmup()
41
+ ```
42
+
43
+ INT8 dynamic quantization typically gives ~2× lower latency and ~4× smaller weight matrices on x86 CPUs, with a negligible quality drop.
44
+
45
+ ## Quickstart (CLI)
46
+
47
+ ```bash
48
+ altamt translate "Mwaramutse nshuti zanjye" --model path/to/checkpoint
49
+ # Good morning my friends
50
+ # [rw -> en | 42.1 ms]
51
+
52
+ altamt translate "Hello" --model path/to/checkpoint --tgt-lang rw --int8 --json
53
+
54
+ altamt benchmark --model path/to/checkpoint --test-file test.parquet --int8
55
+ altamt export-onnx --model path/to/checkpoint --output-dir ./onnx_model
56
+ altamt train --config config.yaml
57
+ ```
58
+
59
+ ## Why altamt?
60
+
61
+ - **One model, both directions** — a target-language tag (`<2en>` / `<2rw>`) steers the decoder, so RW→EN and EN→RW share all parameters and vocabulary.
62
+ - **Automatic language detection** — a built-in, microsecond-fast character n-gram detector picks the direction when you don't.
63
+ - **Built for CPU** — Grouped-Query Attention + full KV-cached decoding + INT8 quantization target <100 ms per sentence on a modern 8-core CPU (beam=1, short sentences).
64
+ - **Multi-format data** — training and benchmarking read `.json`, `.jsonl` and `.parquet` interchangeably.
65
+ - **Honest benchmarking** — `altamt benchmark` reports SacreBLEU, chrF++, latency, throughput and memory, and can run NLLB/Opus-MT baselines through the same harness, emitting Markdown and LaTeX tables.
66
+
67
+ ## Indicative CPU performance
68
+
69
+ Numbers depend on your hardware, sentence length, beam size and thread count; measure on your machine with `altamt benchmark`. On a modern 8-core x86 CPU (beam=1, ~20-token sentences, `num_threads=8`), the INT8 `base` model targets **<100 ms/sentence**, with fp32 roughly 2× slower and ~780 MB peak RAM.
70
+
71
+ ## Documentation
72
+
73
+ Full architecture details, data format specs, training and paper-publishing guides live in the [GitHub README](https://github.com/altamt-ai/altamt#readme).
74
+
75
+ ## License
76
+
77
+ Apache 2.0