orthomni-codeco 0.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.
- orthomni_codeco-0.1.0/.gitignore +11 -0
- orthomni_codeco-0.1.0/LICENSE +21 -0
- orthomni_codeco-0.1.0/PKG-INFO +218 -0
- orthomni_codeco-0.1.0/README.md +181 -0
- orthomni_codeco-0.1.0/pyproject.toml +54 -0
- orthomni_codeco-0.1.0/src/orthomni_codeco/__init__.py +43 -0
- orthomni_codeco-0.1.0/src/orthomni_codeco/__main__.py +141 -0
- orthomni_codeco-0.1.0/src/orthomni_codeco/config.py +120 -0
- orthomni_codeco-0.1.0/src/orthomni_codeco/downloader.py +103 -0
- orthomni_codeco-0.1.0/src/orthomni_codeco/exceptions.py +20 -0
- orthomni_codeco-0.1.0/src/orthomni_codeco/export.py +171 -0
- orthomni_codeco-0.1.0/src/orthomni_codeco/model.py +265 -0
- orthomni_codeco-0.1.0/src/orthomni_codeco/py.typed +0 -0
- orthomni_codeco-0.1.0/src/orthomni_codeco/server.py +129 -0
- orthomni_codeco-0.1.0/src/orthomni_codeco/tokenizer.py +79 -0
- orthomni_codeco-0.1.0/tests/test_smoke.py +99 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nhat H. Pham
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: orthomni-codeco
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Vietnamese spelling correction (BARTpho seq2seq) on CPU with ONNX Runtime INT8 - no PyTorch needed
|
|
5
|
+
Project-URL: Homepage, https://huggingface.co/nhatho1999/orthomni-codeco
|
|
6
|
+
Author-email: "Nhat H. Pham" <nhatho1999.ai@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: bartpho,nlp,onnx,onnxruntime,spelling-correction,vietnamese
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Natural Language :: Vietnamese
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.8
|
|
19
|
+
Requires-Dist: huggingface-hub>=0.20
|
|
20
|
+
Requires-Dist: numpy>=1.21
|
|
21
|
+
Requires-Dist: onnxruntime>=1.16
|
|
22
|
+
Requires-Dist: sentencepiece>=0.1.99
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: build>=1.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
27
|
+
Requires-Dist: twine>=5.0; extra == 'dev'
|
|
28
|
+
Provides-Extra: export
|
|
29
|
+
Requires-Dist: optimum-onnx[onnxruntime]>=0.1.0; extra == 'export'
|
|
30
|
+
Requires-Dist: torch>=2.1; extra == 'export'
|
|
31
|
+
Requires-Dist: transformers<4.58,>=4.45; extra == 'export'
|
|
32
|
+
Provides-Extra: server
|
|
33
|
+
Requires-Dist: fastapi>=0.110; extra == 'server'
|
|
34
|
+
Requires-Dist: pydantic>=2.6; extra == 'server'
|
|
35
|
+
Requires-Dist: uvicorn[standard]>=0.29; extra == 'server'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# orthomni-codeco
|
|
39
|
+
|
|
40
|
+
Vietnamese spelling correction on **CPU** with **ONNX Runtime INT8**, **no PyTorch required**. The model is a Seq2Seq BARTpho/MBart (~400M parameters).
|
|
41
|
+
|
|
42
|
+
- **Lightweight:** the only dependencies are `onnxruntime`, `numpy`, `sentencepiece` and `huggingface-hub` (~120 MB, versus ~1.3 GB with torch + transformers).
|
|
43
|
+
- **Automatic model download:** the wheel contains no weights. On first run, the INT8 model (~1.1 GB) is downloaded from the Hugging Face Hub to `~/.cache/orthomni_codeco/` with a progress bar. Later runs work offline.
|
|
44
|
+
- **Batteries included:** FastAPI server, CLI and Dockerfile.
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install orthomni-codeco # library
|
|
50
|
+
pip install "orthomni-codeco[server]" # + FastAPI/uvicorn
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Requires Python >= 3.8.
|
|
54
|
+
|
|
55
|
+
## Quickstart
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from orthomni_codeco import OrthoMNIModel
|
|
59
|
+
|
|
60
|
+
model = OrthoMNIModel() # first run: downloads the model to the cache, then loads it (~2 s)
|
|
61
|
+
model.correct("hom nay troi dep qua") # 'Hôm nay trời đẹp quá.'
|
|
62
|
+
model.correct(["xin chao", "toi di hoc"]) # ['Xin chào.', 'Tôi đi học.']
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`predict()` and `generate()` are aliases of `correct()`. `Corrector` is the old name of `OrthoMNIModel`.
|
|
66
|
+
|
|
67
|
+
## Library usage
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from orthomni_codeco import OrthoMNIModel, CorrectorConfig, InputTooLongError, get_corrector, ensure_model
|
|
71
|
+
|
|
72
|
+
model = get_corrector() # per-process singleton, config read from ORTHOMNI_* env vars
|
|
73
|
+
model = OrthoMNIModel(CorrectorConfig(intra_op_threads=4))
|
|
74
|
+
model = OrthoMNIModel(CorrectorConfig(model_dir="/path/to/int8")) # use a local model, no download
|
|
75
|
+
|
|
76
|
+
await model.acorrect("xin chao") # async, does not block the event loop
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
model.correct(very_long_text)
|
|
80
|
+
except InputTooLongError as e:
|
|
81
|
+
print(e.indices, e.max_tokens) # which inputs exceed the limit -> split sentences first
|
|
82
|
+
|
|
83
|
+
ensure_model() # only download the model to the cache, returns its path
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- **Load once:** create `OrthoMNIModel` once and reuse it.
|
|
87
|
+
- **Thread-safe:** calls are serialized with a lock, since ONNX Runtime already uses all the threads it is given.
|
|
88
|
+
- **Lightweight import:** `import orthomni_codeco` does not load onnxruntime or numpy.
|
|
89
|
+
|
|
90
|
+
### Model source
|
|
91
|
+
|
|
92
|
+
In order of priority:
|
|
93
|
+
1. `model_dir` / `ORTHOMNI_MODEL_DIR`: a local model directory, nothing is downloaded.
|
|
94
|
+
2. `repo_id` + `revision` (`ORTHOMNI_REPO_ID`, `ORTHOMNI_REVISION`): downloaded from the Hugging Face Hub to `cache_dir` (`ORTHOMNI_CACHE_DIR`, default `~/.cache/orthomni_codeco/<org>--<repo>/<revision>/`).
|
|
95
|
+
|
|
96
|
+
Private repos use `HF_TOKEN` or `hf auth login`. For offline environments, pre-download with `orthomni download`, then set `HF_HUB_OFFLINE=1`.
|
|
97
|
+
|
|
98
|
+
## Mounting into an existing FastAPI app
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
import asyncio
|
|
102
|
+
from contextlib import asynccontextmanager
|
|
103
|
+
from fastapi import FastAPI
|
|
104
|
+
from orthomni_codeco import get_corrector
|
|
105
|
+
from orthomni_codeco.server import create_router
|
|
106
|
+
|
|
107
|
+
@asynccontextmanager
|
|
108
|
+
async def lifespan(app: FastAPI):
|
|
109
|
+
await asyncio.to_thread(get_corrector) # load the model at startup
|
|
110
|
+
yield
|
|
111
|
+
|
|
112
|
+
app = FastAPI(lifespan=lifespan)
|
|
113
|
+
app.include_router(create_router(), prefix="/spell") # -> POST /spell/predict, GET /spell/health
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## CLI and standalone server
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
orthomni download # download the model to the cache, print its path
|
|
120
|
+
orthomni correct "toi di hoc" "xin chao" # quick test
|
|
121
|
+
orthomni serve --port 8006 # workers/threads derived from the vCPU count
|
|
122
|
+
orthomni serve --workers 1 --threads 8 # lowest latency
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`python -m orthomni_codeco ...` is equivalent to `orthomni ...`.
|
|
126
|
+
|
|
127
|
+
| Method | Path | Description |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| GET | `/health` | `200 {"status":"ok"}` when the model is ready, `503` otherwise |
|
|
130
|
+
| POST | `/predict` | Body `{"text": str}` **or** `{"texts": list[str]}` |
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
curl -X POST localhost:8006/predict -H 'content-type: application/json' -d '{"text":"hom nay troi dep qua"}'
|
|
134
|
+
# {"result":"Hôm nay trời đẹp quá.","latency_ms":113.7}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Error codes: `422` invalid input (both `text` and `texts`, batch too large, empty text), `413` sentence longer than `MAX_INPUT_TOKENS` (the server does not truncate), `500` inference error.
|
|
138
|
+
|
|
139
|
+
## Configuration (environment variables)
|
|
140
|
+
|
|
141
|
+
| Variable | Default | Meaning |
|
|
142
|
+
|---|---|---|
|
|
143
|
+
| `ORTHOMNI_MODEL_DIR` | (empty) | Local model directory; when set, no download happens |
|
|
144
|
+
| `ORTHOMNI_REPO_ID` / `ORTHOMNI_REVISION` | see `config.py` | Hugging Face Hub repo and revision |
|
|
145
|
+
| `ORTHOMNI_CACHE_DIR` | `~/.cache/orthomni_codeco` | Model cache directory |
|
|
146
|
+
| `ORTHOMNI_INTRA_OP_THREADS` | `0` (= vCPU / workers) | ORT threads within one operator. **Biggest effect on latency** |
|
|
147
|
+
| `ORTHOMNI_INTER_OP_THREADS` | `1` | Parallelism across graph branches |
|
|
148
|
+
| `WEB_CONCURRENCY` | `vCPU / 4` | Number of uvicorn workers; each holds its own model copy (~1.7 GB RAM) |
|
|
149
|
+
| `ORTHOMNI_MAX_INPUT_TOKENS` | `256` | Maximum tokens per sentence |
|
|
150
|
+
| `ORTHOMNI_INFER_BATCH_SIZE` | `8` | Sub-batch size (sorted by length to reduce padding) |
|
|
151
|
+
| `ORTHOMNI_NEW_TOKENS_RATIO` / `_OFFSET` / `_CAP` | `1.3` / `8` / `320` | `max_new_tokens = min(CAP, len_input × RATIO + OFFSET)` |
|
|
152
|
+
| `ORTHOMNI_WARMUP` | `true` | Run one dummy inference on load |
|
|
153
|
+
| `ORTHOMNI_MAX_BATCH_SIZE` | `32` | Maximum number of `texts` per request |
|
|
154
|
+
| `ORTHOMNI_MAX_TEXT_CHARS` | `4000` | Maximum characters per text |
|
|
155
|
+
| `ORTHOMNI_MAX_CONCURRENCY` | `1` | Concurrent generate requests per worker |
|
|
156
|
+
|
|
157
|
+
**Avoid thread thrashing:** keep `workers × ORTHOMNI_INTRA_OP_THREADS ≤ vCPUs`. The `serve` command reads CPU limits from cgroups (`docker --cpus`, K8s limits). Fewer workers with more threads gives lower latency; more workers with fewer threads gives higher throughput.
|
|
158
|
+
|
|
159
|
+
## Docker
|
|
160
|
+
|
|
161
|
+
Requires the INT8 model in `./model` (see below):
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
docker build -t orthomni-codeco:cpu .
|
|
165
|
+
docker run --rm -p 8006:8006 --cpus 8 --memory 4g orthomni-codeco:cpu
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Development
|
|
169
|
+
|
|
170
|
+
### Environment setup
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
python -m venv .venv && . .venv/bin/activate
|
|
174
|
+
pip install -e ".[server,dev]"
|
|
175
|
+
ORTHOMNI_MODEL_DIR=./model pytest -q # model tests are skipped if ./model is missing
|
|
176
|
+
./run.sh # local server using ./model, port 7428
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Exporting a new model
|
|
180
|
+
|
|
181
|
+
The current INT8 model is quantized for **arm64** (see `model_info.json`). It still runs on x86 but not optimally, so re-export on a machine with the same CPU architecture as the serving machine:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
pip install torch --index-url https://download.pytorch.org/whl/cpu # CPU-only torch first
|
|
185
|
+
pip install -e ".[export]"
|
|
186
|
+
orthomni export --model_dir /path/to/checkpoint --output_dir ./model [--target avx512_vnni]
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Quantization is dynamic INT8 **per-channel**, applied only to `MatMul`; embeddings stay FP32. Per-tensor quantization produced incorrect sentences.
|
|
190
|
+
|
|
191
|
+
### Publishing the model to the Hugging Face Hub
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
hf auth login
|
|
195
|
+
python scripts/upload_model.py --model_dir ./model --repo_id <org>/orthomni-codeco
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
The script prints a commit hash. Update `DEFAULT_REPO_ID` and `DEFAULT_REVISION` in `src/orthomni_codeco/config.py` before releasing, so each package version always downloads the same set of weights.
|
|
199
|
+
|
|
200
|
+
### Build and publish
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
pip install -U build twine
|
|
204
|
+
rm -rf dist/ && python -m build # -> dist/*.whl + dist/*.tar.gz
|
|
205
|
+
twine check dist/* # check metadata and that the README renders on PyPI
|
|
206
|
+
unzip -l dist/*.whl # confirm no .onnx files are inside
|
|
207
|
+
|
|
208
|
+
# Try the wheel in a clean venv
|
|
209
|
+
python -m venv /tmp/t && /tmp/t/bin/pip install dist/*.whl
|
|
210
|
+
/tmp/t/bin/python -c "from orthomni_codeco import OrthoMNIModel; print(OrthoMNIModel().correct('xin chao'))"
|
|
211
|
+
|
|
212
|
+
twine upload --repository testpypi dist/* # try on TestPyPI first
|
|
213
|
+
twine upload dist/* # real PyPI
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## License
|
|
217
|
+
|
|
218
|
+
MIT
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# orthomni-codeco
|
|
2
|
+
|
|
3
|
+
Vietnamese spelling correction on **CPU** with **ONNX Runtime INT8**, **no PyTorch required**. The model is a Seq2Seq BARTpho/MBart (~400M parameters).
|
|
4
|
+
|
|
5
|
+
- **Lightweight:** the only dependencies are `onnxruntime`, `numpy`, `sentencepiece` and `huggingface-hub` (~120 MB, versus ~1.3 GB with torch + transformers).
|
|
6
|
+
- **Automatic model download:** the wheel contains no weights. On first run, the INT8 model (~1.1 GB) is downloaded from the Hugging Face Hub to `~/.cache/orthomni_codeco/` with a progress bar. Later runs work offline.
|
|
7
|
+
- **Batteries included:** FastAPI server, CLI and Dockerfile.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install orthomni-codeco # library
|
|
13
|
+
pip install "orthomni-codeco[server]" # + FastAPI/uvicorn
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Requires Python >= 3.8.
|
|
17
|
+
|
|
18
|
+
## Quickstart
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from orthomni_codeco import OrthoMNIModel
|
|
22
|
+
|
|
23
|
+
model = OrthoMNIModel() # first run: downloads the model to the cache, then loads it (~2 s)
|
|
24
|
+
model.correct("hom nay troi dep qua") # 'Hôm nay trời đẹp quá.'
|
|
25
|
+
model.correct(["xin chao", "toi di hoc"]) # ['Xin chào.', 'Tôi đi học.']
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`predict()` and `generate()` are aliases of `correct()`. `Corrector` is the old name of `OrthoMNIModel`.
|
|
29
|
+
|
|
30
|
+
## Library usage
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from orthomni_codeco import OrthoMNIModel, CorrectorConfig, InputTooLongError, get_corrector, ensure_model
|
|
34
|
+
|
|
35
|
+
model = get_corrector() # per-process singleton, config read from ORTHOMNI_* env vars
|
|
36
|
+
model = OrthoMNIModel(CorrectorConfig(intra_op_threads=4))
|
|
37
|
+
model = OrthoMNIModel(CorrectorConfig(model_dir="/path/to/int8")) # use a local model, no download
|
|
38
|
+
|
|
39
|
+
await model.acorrect("xin chao") # async, does not block the event loop
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
model.correct(very_long_text)
|
|
43
|
+
except InputTooLongError as e:
|
|
44
|
+
print(e.indices, e.max_tokens) # which inputs exceed the limit -> split sentences first
|
|
45
|
+
|
|
46
|
+
ensure_model() # only download the model to the cache, returns its path
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- **Load once:** create `OrthoMNIModel` once and reuse it.
|
|
50
|
+
- **Thread-safe:** calls are serialized with a lock, since ONNX Runtime already uses all the threads it is given.
|
|
51
|
+
- **Lightweight import:** `import orthomni_codeco` does not load onnxruntime or numpy.
|
|
52
|
+
|
|
53
|
+
### Model source
|
|
54
|
+
|
|
55
|
+
In order of priority:
|
|
56
|
+
1. `model_dir` / `ORTHOMNI_MODEL_DIR`: a local model directory, nothing is downloaded.
|
|
57
|
+
2. `repo_id` + `revision` (`ORTHOMNI_REPO_ID`, `ORTHOMNI_REVISION`): downloaded from the Hugging Face Hub to `cache_dir` (`ORTHOMNI_CACHE_DIR`, default `~/.cache/orthomni_codeco/<org>--<repo>/<revision>/`).
|
|
58
|
+
|
|
59
|
+
Private repos use `HF_TOKEN` or `hf auth login`. For offline environments, pre-download with `orthomni download`, then set `HF_HUB_OFFLINE=1`.
|
|
60
|
+
|
|
61
|
+
## Mounting into an existing FastAPI app
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
import asyncio
|
|
65
|
+
from contextlib import asynccontextmanager
|
|
66
|
+
from fastapi import FastAPI
|
|
67
|
+
from orthomni_codeco import get_corrector
|
|
68
|
+
from orthomni_codeco.server import create_router
|
|
69
|
+
|
|
70
|
+
@asynccontextmanager
|
|
71
|
+
async def lifespan(app: FastAPI):
|
|
72
|
+
await asyncio.to_thread(get_corrector) # load the model at startup
|
|
73
|
+
yield
|
|
74
|
+
|
|
75
|
+
app = FastAPI(lifespan=lifespan)
|
|
76
|
+
app.include_router(create_router(), prefix="/spell") # -> POST /spell/predict, GET /spell/health
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## CLI and standalone server
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
orthomni download # download the model to the cache, print its path
|
|
83
|
+
orthomni correct "toi di hoc" "xin chao" # quick test
|
|
84
|
+
orthomni serve --port 8006 # workers/threads derived from the vCPU count
|
|
85
|
+
orthomni serve --workers 1 --threads 8 # lowest latency
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`python -m orthomni_codeco ...` is equivalent to `orthomni ...`.
|
|
89
|
+
|
|
90
|
+
| Method | Path | Description |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| GET | `/health` | `200 {"status":"ok"}` when the model is ready, `503` otherwise |
|
|
93
|
+
| POST | `/predict` | Body `{"text": str}` **or** `{"texts": list[str]}` |
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
curl -X POST localhost:8006/predict -H 'content-type: application/json' -d '{"text":"hom nay troi dep qua"}'
|
|
97
|
+
# {"result":"Hôm nay trời đẹp quá.","latency_ms":113.7}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Error codes: `422` invalid input (both `text` and `texts`, batch too large, empty text), `413` sentence longer than `MAX_INPUT_TOKENS` (the server does not truncate), `500` inference error.
|
|
101
|
+
|
|
102
|
+
## Configuration (environment variables)
|
|
103
|
+
|
|
104
|
+
| Variable | Default | Meaning |
|
|
105
|
+
|---|---|---|
|
|
106
|
+
| `ORTHOMNI_MODEL_DIR` | (empty) | Local model directory; when set, no download happens |
|
|
107
|
+
| `ORTHOMNI_REPO_ID` / `ORTHOMNI_REVISION` | see `config.py` | Hugging Face Hub repo and revision |
|
|
108
|
+
| `ORTHOMNI_CACHE_DIR` | `~/.cache/orthomni_codeco` | Model cache directory |
|
|
109
|
+
| `ORTHOMNI_INTRA_OP_THREADS` | `0` (= vCPU / workers) | ORT threads within one operator. **Biggest effect on latency** |
|
|
110
|
+
| `ORTHOMNI_INTER_OP_THREADS` | `1` | Parallelism across graph branches |
|
|
111
|
+
| `WEB_CONCURRENCY` | `vCPU / 4` | Number of uvicorn workers; each holds its own model copy (~1.7 GB RAM) |
|
|
112
|
+
| `ORTHOMNI_MAX_INPUT_TOKENS` | `256` | Maximum tokens per sentence |
|
|
113
|
+
| `ORTHOMNI_INFER_BATCH_SIZE` | `8` | Sub-batch size (sorted by length to reduce padding) |
|
|
114
|
+
| `ORTHOMNI_NEW_TOKENS_RATIO` / `_OFFSET` / `_CAP` | `1.3` / `8` / `320` | `max_new_tokens = min(CAP, len_input × RATIO + OFFSET)` |
|
|
115
|
+
| `ORTHOMNI_WARMUP` | `true` | Run one dummy inference on load |
|
|
116
|
+
| `ORTHOMNI_MAX_BATCH_SIZE` | `32` | Maximum number of `texts` per request |
|
|
117
|
+
| `ORTHOMNI_MAX_TEXT_CHARS` | `4000` | Maximum characters per text |
|
|
118
|
+
| `ORTHOMNI_MAX_CONCURRENCY` | `1` | Concurrent generate requests per worker |
|
|
119
|
+
|
|
120
|
+
**Avoid thread thrashing:** keep `workers × ORTHOMNI_INTRA_OP_THREADS ≤ vCPUs`. The `serve` command reads CPU limits from cgroups (`docker --cpus`, K8s limits). Fewer workers with more threads gives lower latency; more workers with fewer threads gives higher throughput.
|
|
121
|
+
|
|
122
|
+
## Docker
|
|
123
|
+
|
|
124
|
+
Requires the INT8 model in `./model` (see below):
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
docker build -t orthomni-codeco:cpu .
|
|
128
|
+
docker run --rm -p 8006:8006 --cpus 8 --memory 4g orthomni-codeco:cpu
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Development
|
|
132
|
+
|
|
133
|
+
### Environment setup
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
python -m venv .venv && . .venv/bin/activate
|
|
137
|
+
pip install -e ".[server,dev]"
|
|
138
|
+
ORTHOMNI_MODEL_DIR=./model pytest -q # model tests are skipped if ./model is missing
|
|
139
|
+
./run.sh # local server using ./model, port 7428
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Exporting a new model
|
|
143
|
+
|
|
144
|
+
The current INT8 model is quantized for **arm64** (see `model_info.json`). It still runs on x86 but not optimally, so re-export on a machine with the same CPU architecture as the serving machine:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
pip install torch --index-url https://download.pytorch.org/whl/cpu # CPU-only torch first
|
|
148
|
+
pip install -e ".[export]"
|
|
149
|
+
orthomni export --model_dir /path/to/checkpoint --output_dir ./model [--target avx512_vnni]
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Quantization is dynamic INT8 **per-channel**, applied only to `MatMul`; embeddings stay FP32. Per-tensor quantization produced incorrect sentences.
|
|
153
|
+
|
|
154
|
+
### Publishing the model to the Hugging Face Hub
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
hf auth login
|
|
158
|
+
python scripts/upload_model.py --model_dir ./model --repo_id <org>/orthomni-codeco
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The script prints a commit hash. Update `DEFAULT_REPO_ID` and `DEFAULT_REVISION` in `src/orthomni_codeco/config.py` before releasing, so each package version always downloads the same set of weights.
|
|
162
|
+
|
|
163
|
+
### Build and publish
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
pip install -U build twine
|
|
167
|
+
rm -rf dist/ && python -m build # -> dist/*.whl + dist/*.tar.gz
|
|
168
|
+
twine check dist/* # check metadata and that the README renders on PyPI
|
|
169
|
+
unzip -l dist/*.whl # confirm no .onnx files are inside
|
|
170
|
+
|
|
171
|
+
# Try the wheel in a clean venv
|
|
172
|
+
python -m venv /tmp/t && /tmp/t/bin/pip install dist/*.whl
|
|
173
|
+
/tmp/t/bin/python -c "from orthomni_codeco import OrthoMNIModel; print(OrthoMNIModel().correct('xin chao'))"
|
|
174
|
+
|
|
175
|
+
twine upload --repository testpypi dist/* # try on TestPyPI first
|
|
176
|
+
twine upload dist/* # real PyPI
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
MIT
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "orthomni-codeco"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Vietnamese spelling correction (BARTpho seq2seq) on CPU with ONNX Runtime INT8 - no PyTorch needed"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Nhat H. Pham", email = "nhatho1999.ai@gmail.com" },
|
|
15
|
+
]
|
|
16
|
+
keywords = ["vietnamese", "spelling-correction", "onnx", "onnxruntime", "bartpho", "nlp"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Natural Language :: Vietnamese",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
24
|
+
"Topic :: Text Processing :: Linguistic",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
# Inference chỉ cần ONNX Runtime + numpy + sentencepiece. Model (~1.1 GB) tải từ Hugging Face Hub lúc chạy.
|
|
28
|
+
dependencies = [
|
|
29
|
+
"onnxruntime>=1.16",
|
|
30
|
+
"numpy>=1.21",
|
|
31
|
+
"sentencepiece>=0.1.99",
|
|
32
|
+
"huggingface-hub>=0.20",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
server = ["fastapi>=0.110", "uvicorn[standard]>=0.29", "pydantic>=2.6"]
|
|
37
|
+
# Chỉ cần khi export checkpoint PyTorch -> ONNX INT8. Nên cài torch CPU-only trước (xem README).
|
|
38
|
+
export = ["torch>=2.1", "optimum-onnx[onnxruntime]>=0.1.0", "transformers>=4.45,<4.58"]
|
|
39
|
+
dev = ["pytest>=7", "httpx>=0.27", "build>=1.0", "twine>=5.0"]
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Homepage = "https://huggingface.co/nhatho1999/orthomni-codeco"
|
|
43
|
+
|
|
44
|
+
[project.scripts]
|
|
45
|
+
orthomni = "orthomni_codeco.__main__:cli"
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.wheel]
|
|
48
|
+
packages = ["src/orthomni_codeco"]
|
|
49
|
+
|
|
50
|
+
[tool.hatch.build.targets.sdist]
|
|
51
|
+
include = ["src/", "tests/", "README.md", "LICENSE", "pyproject.toml"]
|
|
52
|
+
|
|
53
|
+
[tool.pytest.ini_options]
|
|
54
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""orthomni_codeco - Sửa chính tả tiếng Việt (Seq2Seq BARTpho, ONNX Runtime INT8, CPU).
|
|
2
|
+
|
|
3
|
+
from orthomni_codeco import OrthoMNIModel
|
|
4
|
+
model = OrthoMNIModel() # lần đầu tự tải model về ~/.cache/orthomni_codeco
|
|
5
|
+
model.correct("toi di hoc") # -> "Tôi đi học."
|
|
6
|
+
|
|
7
|
+
Import package rất nhẹ: onnxruntime/numpy chỉ được import khi dùng tới OrthoMNIModel.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
from .config import DEFAULT_REPO_ID, DEFAULT_REVISION, CorrectorConfig, ServerConfig, available_cpus
|
|
15
|
+
from .downloader import ensure_model
|
|
16
|
+
from .exceptions import InputTooLongError, ModelNotFoundError, OrthomniError
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from .model import Corrector, OrthoMNIModel, get_corrector
|
|
20
|
+
|
|
21
|
+
__version__ = "0.1.0"
|
|
22
|
+
__all__ = [
|
|
23
|
+
"OrthoMNIModel",
|
|
24
|
+
"Corrector",
|
|
25
|
+
"CorrectorConfig",
|
|
26
|
+
"ServerConfig",
|
|
27
|
+
"get_corrector",
|
|
28
|
+
"ensure_model",
|
|
29
|
+
"available_cpus",
|
|
30
|
+
"DEFAULT_REPO_ID",
|
|
31
|
+
"DEFAULT_REVISION",
|
|
32
|
+
"OrthomniError",
|
|
33
|
+
"ModelNotFoundError",
|
|
34
|
+
"InputTooLongError",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def __getattr__(name: str) -> Any:
|
|
39
|
+
if name in ("OrthoMNIModel", "Corrector", "get_corrector"):
|
|
40
|
+
from . import model
|
|
41
|
+
|
|
42
|
+
return getattr(model, name)
|
|
43
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""CLI:
|
|
2
|
+
python -m orthomni_codeco serve [--port 8006] [--workers N] [--threads T]
|
|
3
|
+
python -m orthomni_codeco export --model_dir /path/to/checkpoint [--target auto]
|
|
4
|
+
python -m orthomni_codeco correct "toi di hoc" "xin chao"
|
|
5
|
+
python -m orthomni_codeco download [--repo_id ...] [--revision ...]
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
from typing import List, Optional
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _thread_env_defaults() -> None:
|
|
18
|
+
# Tính toán nặng nằm trong thread pool của ONNX Runtime; OpenMP/BLAS (numpy, torch khi export)
|
|
19
|
+
# để 1 luồng để tránh thread thrashing. Phải set trước khi import các thư viện này.
|
|
20
|
+
for var in ("OMP_NUM_THREADS", "MKL_NUM_THREADS", "OPENBLAS_NUM_THREADS"):
|
|
21
|
+
os.environ.setdefault(var, "1")
|
|
22
|
+
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def cmd_serve(args: argparse.Namespace) -> int:
|
|
26
|
+
import uvicorn
|
|
27
|
+
|
|
28
|
+
from .config import available_cpus
|
|
29
|
+
|
|
30
|
+
# Quy tắc: workers x intra_op_threads <= số vCPU (đã tính cgroup quota).
|
|
31
|
+
cpus = available_cpus()
|
|
32
|
+
workers = args.workers or int(os.getenv("WEB_CONCURRENCY", "0")) or max(1, cpus // 4)
|
|
33
|
+
threads = args.threads or int(os.getenv("ORT_INTRA_OP_THREADS", os.getenv("ORTHOMNI_INTRA_OP_THREADS", "0"))) \
|
|
34
|
+
or max(1, cpus // workers)
|
|
35
|
+
# Worker là process con -> truyền cấu hình qua env.
|
|
36
|
+
os.environ["WEB_CONCURRENCY"] = str(workers)
|
|
37
|
+
os.environ["ORTHOMNI_INTRA_OP_THREADS"] = str(threads)
|
|
38
|
+
# Tải model 1 lần ở process cha, tránh N worker cùng tải song song.
|
|
39
|
+
_ensure_model_from_env()
|
|
40
|
+
print(f"[orthomni] cpus={cpus} workers={workers} ort_intra_threads={threads}", flush=True)
|
|
41
|
+
|
|
42
|
+
uvicorn.run(
|
|
43
|
+
"orthomni_codeco.server:app_factory",
|
|
44
|
+
factory=True,
|
|
45
|
+
host=args.host,
|
|
46
|
+
port=args.port,
|
|
47
|
+
workers=workers,
|
|
48
|
+
access_log=False,
|
|
49
|
+
timeout_keep_alive=30,
|
|
50
|
+
timeout_graceful_shutdown=30,
|
|
51
|
+
limit_concurrency=args.limit_concurrency,
|
|
52
|
+
backlog=2048,
|
|
53
|
+
)
|
|
54
|
+
return 0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _ensure_model_from_env() -> None:
|
|
58
|
+
from .config import CorrectorConfig
|
|
59
|
+
from .downloader import ensure_model
|
|
60
|
+
|
|
61
|
+
cfg = CorrectorConfig.from_env()
|
|
62
|
+
if not cfg.model_dir:
|
|
63
|
+
ensure_model(cfg.repo_id, cfg.revision, cfg.cache_dir or None)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def cmd_download(args: argparse.Namespace) -> int:
|
|
67
|
+
from .config import CorrectorConfig
|
|
68
|
+
from .downloader import ensure_model
|
|
69
|
+
|
|
70
|
+
cfg = CorrectorConfig.from_env()
|
|
71
|
+
path = ensure_model(
|
|
72
|
+
args.repo_id or cfg.repo_id,
|
|
73
|
+
args.revision or cfg.revision,
|
|
74
|
+
args.cache_dir or cfg.cache_dir or None,
|
|
75
|
+
force=args.force,
|
|
76
|
+
)
|
|
77
|
+
print(path)
|
|
78
|
+
return 0
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def cmd_correct(args: argparse.Namespace) -> int:
|
|
82
|
+
from .model import OrthoMNIModel
|
|
83
|
+
|
|
84
|
+
model = OrthoMNIModel()
|
|
85
|
+
for src, out in zip(args.texts, model.correct(args.texts)):
|
|
86
|
+
print(f"{src}\t->\t{out}")
|
|
87
|
+
return 0
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
91
|
+
_thread_env_defaults()
|
|
92
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
|
93
|
+
parser = argparse.ArgumentParser(prog="python -m orthomni_codeco")
|
|
94
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
95
|
+
|
|
96
|
+
p_serve = sub.add_parser("serve", help="Chạy FastAPI server (cần extra [server]).")
|
|
97
|
+
p_serve.add_argument("--host", default=os.getenv("HOST", "0.0.0.0"))
|
|
98
|
+
p_serve.add_argument("--port", type=int, default=int(os.getenv("PORT", "8006")))
|
|
99
|
+
p_serve.add_argument("--workers", type=int, default=0, help="0 = 1 worker / 4 vCPU.")
|
|
100
|
+
p_serve.add_argument("--threads", type=int, default=0, help="Luồng ORT mỗi worker. 0 = vCPU / workers.")
|
|
101
|
+
p_serve.add_argument("--limit_concurrency", type=int, default=int(os.getenv("LIMIT_CONCURRENCY", "256")))
|
|
102
|
+
|
|
103
|
+
p_download = sub.add_parser("download", help="Tải model từ Hugging Face Hub về cache (in ra đường dẫn).")
|
|
104
|
+
p_download.add_argument("--repo_id", default=None)
|
|
105
|
+
p_download.add_argument("--revision", default=None)
|
|
106
|
+
p_download.add_argument("--cache_dir", default=None)
|
|
107
|
+
p_download.add_argument("--force", action="store_true", help="Tải lại kể cả khi cache đã đủ.")
|
|
108
|
+
|
|
109
|
+
p_export = sub.add_parser("export", help="Export checkpoint PyTorch -> ONNX INT8 (cần extra [export]).")
|
|
110
|
+
from .export import add_arguments
|
|
111
|
+
|
|
112
|
+
add_arguments(p_export)
|
|
113
|
+
|
|
114
|
+
p_correct = sub.add_parser("correct", help="Sửa nhanh vài câu từ dòng lệnh.")
|
|
115
|
+
p_correct.add_argument("texts", nargs="+")
|
|
116
|
+
|
|
117
|
+
args = parser.parse_args(argv)
|
|
118
|
+
if args.command == "serve":
|
|
119
|
+
return cmd_serve(args)
|
|
120
|
+
if args.command == "download":
|
|
121
|
+
return cmd_download(args)
|
|
122
|
+
if args.command == "export":
|
|
123
|
+
from .export import main as export_main
|
|
124
|
+
|
|
125
|
+
return export_main(args)
|
|
126
|
+
return cmd_correct(args)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def cli() -> int:
|
|
130
|
+
"""Entry point: lỗi đã biết (thiếu model, tải thất bại...) in 1 dòng thay vì traceback."""
|
|
131
|
+
from .exceptions import OrthomniError
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
return main()
|
|
135
|
+
except OrthomniError as exc:
|
|
136
|
+
print(f"[orthomni] Lỗi: {exc}", file=sys.stderr)
|
|
137
|
+
return 1
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
if __name__ == "__main__":
|
|
141
|
+
sys.exit(cli())
|