Skip to content

Commit 587692a

Browse files
authored
feat/fp16 onnx export (#12)
1 parent 636c506 commit 587692a

8 files changed

Lines changed: 76 additions & 16 deletions

File tree

.github/workflows/tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ jobs:
3939
uses: actions/cache@v4
4040
with:
4141
path: tmp/models
42-
key: ${{ runner.os }}-models-${{ matrix.ruby }}-fp32
42+
key: ${{ runner.os }}-models-${{ matrix.ruby }}-variants
4343
restore-keys: |
4444
${{ runner.os }}-models-${{ matrix.ruby }}
4545
@@ -48,5 +48,5 @@ jobs:
4848

4949
- name: Run integration tests
5050
env:
51-
MODEL_FILE: "model.onnx"
51+
MODEL_FILE: "model_fp16.onnx"
5252
run: nix develop . --command bash -lc "bundle exec rake spec:integration"

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@
55
*.gem
66
.rspec_status
77
Gemfile.lock
8-
/onnx/gliner2-multi-v1/
8+
/onnx/onnx/

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ result = Gliner[{ "order" => ["status::[pending|processing|shipped]::str"] }]["S
104104
This implementation expects a directory containing:
105105

106106
- `tokenizer.json`
107-
- `model.onnx` or `model_int8.onnx`
107+
- `model.onnx`, `model_fp16.onnx`, or `model_int8.onnx`
108108
- (optional) `config.json` with `max_width` and `max_seq_len`
109109

110110
One publicly available ONNX export is `cuerbot/gliner2-multi-v1` on Hugging Face.

Rakefile

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,20 @@ task default: :spec
1010

1111
namespace :model do
1212
DEFAULT_REPO_ID = 'cuerbot/gliner2-multi-v1'
13-
DEFAULT_MODEL_FILE = 'model_int8.onnx'
13+
DEFAULT_MODEL_FILE = 'model_fp16.onnx'
14+
DEFAULT_MODEL_SUBDIR = 'onnx'
1415

15-
desc 'Downloads a test model to tmp/ (REPO_ID=... MODEL_FILE=model_int8.onnx)'
16+
desc 'Downloads a test model to tmp/ (REPO_ID=... MODEL_FILE=model_fp16.onnx)'
1617
task :pull do
1718
repo_id = ENV['REPO_ID'] || DEFAULT_REPO_ID
1819
model_file = ENV['MODEL_FILE'] || DEFAULT_MODEL_FILE
20+
model_subdir = ENV['MODEL_SUBDIR'] || DEFAULT_MODEL_SUBDIR
1921

2022
dir = File.expand_path("tmp/models/#{repo_id.tr('/', '__')}", __dir__)
2123
FileUtils.mkdir_p(dir)
2224

2325
base = "https://huggingface.co/#{repo_id}/resolve/main"
26+
base = "#{base}/#{model_subdir}" unless model_subdir.nil? || model_subdir.empty?
2427
files = ['tokenizer.json', 'config.json', model_file]
2528
client = HTTPX.plugin(:follow_redirects).with(max_redirects: 5)
2629

@@ -44,6 +47,7 @@ namespace :spec do
4447
task :integration do
4548
repo_id = ENV['REPO_ID'] || DEFAULT_REPO_ID
4649
model_file = ENV['MODEL_FILE'] || DEFAULT_MODEL_FILE
50+
model_subdir = ENV['MODEL_SUBDIR'] || DEFAULT_MODEL_SUBDIR
4751

4852
Rake::Task['model:pull'].invoke unless ENV['GLINER_MODEL_DIR'] && !ENV['GLINER_MODEL_DIR'].empty?
4953

@@ -53,7 +57,8 @@ namespace :spec do
5357
env = {
5458
'GLINER_INTEGRATION' => '1',
5559
'GLINER_MODEL_DIR' => model_dir,
56-
'GLINER_MODEL_FILE' => model_file
60+
'GLINER_MODEL_FILE' => model_file,
61+
'GLINER_MODEL_SUBDIR' => model_subdir
5762
}
5863
sh env, 'rspec', 'spec/integration_spec.rb'
5964
end

bin/console

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ require "httpx"
1212
require "irb"
1313

1414
DEFAULT_REPO_ID = "cuerbot/gliner2-multi-v1"
15-
DEFAULT_MODEL_FILE = "model_int8.onnx"
15+
DEFAULT_MODEL_FILE = "model_fp16.onnx"
16+
DEFAULT_MODEL_SUBDIR = "onnx"
1617

17-
def ensure_model_dir!(repo_id:, model_file:)
18+
def ensure_model_dir!(repo_id:, model_file:, model_subdir:)
1819
dir = File.expand_path("../tmp/models/#{repo_id.tr('/', '__')}", __dir__)
1920
FileUtils.mkdir_p(dir)
2021

2122
base = "https://huggingface.co/#{repo_id}/resolve/main"
23+
base = "#{base}/#{model_subdir}" unless model_subdir.nil? || model_subdir.empty?
2224
files = ["tokenizer.json", "config.json", model_file]
2325

2426
files.each do |file|
@@ -40,13 +42,14 @@ end
4042
model_dir = ARGV[0] || ENV["GLINER_MODEL_DIR"]
4143
repo_id = ENV["GLINER_REPO_ID"] || DEFAULT_REPO_ID
4244
model_file = ENV["GLINER_MODEL_FILE"] || DEFAULT_MODEL_FILE
45+
model_subdir = ENV["GLINER_MODEL_SUBDIR"] || DEFAULT_MODEL_SUBDIR
4346

4447
if model_dir && !model_dir.empty?
4548
$gliner_model = Gliner.load(model_dir, file: model_file)
4649
else
4750
begin
4851
require "fileutils"
49-
model_dir = ensure_model_dir!(repo_id: repo_id, model_file: model_file)
52+
model_dir = ensure_model_dir!(repo_id: repo_id, model_file: model_file, model_subdir: model_subdir)
5053
$gliner_model = Gliner.load(model_dir, file: model_file)
5154
rescue => e
5255
warn "No model loaded (auto-download failed: #{e.class}: #{e.message})"

onnx/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1+
---
2+
license: mit
3+
base_model:
4+
- fastino/gliner2-multi-v1
5+
---
6+
17
# GLiNER2 ONNX export
8+
## gliner2-multi-v1 ONNX
29

310
- `model.onnx` (FP32 export)
11+
- `model_fp16.onnx` (FP16 weights converted from FP32)
412
- `model_int8.onnx` (dynamic INT8 quantization via onnxruntime)
513
- tokenizer files copied verbatim from the HF model
614
- a small `config.json` describing runtime constraints
@@ -12,6 +20,7 @@ The export script follows the same design:
1220
- Inputs: `input_ids`, `attention_mask` (optionally `token_type_ids`)
1321
- Output: `span_logits`
1422
- Export with `torch.onnx.export` (opset 19) and dynamic batch/sequence axes
23+
- Convert FP32 weights to FP16 with `convert_float_to_float16`
1524
- Quantize with `onnxruntime.quantization.quantize_dynamic(QInt8)`
1625

1726
## Usage
@@ -47,11 +56,13 @@ validation or to load the quantized model, use:
4756
pipenv run python export.py --no-validate
4857
pipenv run python export.py --no-validate-extraction
4958
pipenv run python export.py --validate-quantized
59+
pipenv run python export.py --no-fp16
5060
```
5161

5262
The output directory will include:
5363

5464
- `model.onnx`
65+
- `model_fp16.onnx`
5566
- `model_int8.onnx`
5667
- `tokenizer.json`
5768
- `tokenizer_config.json`

onnx/export.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
#!/usr/bin/env python3
22
import argparse
3+
import inspect
34
import json
45
import shutil
56
from dataclasses import dataclass
67
from pathlib import Path
78
from typing import Dict, List, Sequence, Tuple
89

10+
import onnx
911
import torch
1012
from huggingface_hub import snapshot_download
1113
from onnxruntime.quantization import quantize_dynamic, QuantType
@@ -40,6 +42,7 @@ class ExportConfig:
4042
max_seq_len: int
4143
opset: int
4244
include_token_type_ids: bool
45+
fp16: bool
4346
quantize: bool
4447
validate: bool
4548
validate_quantized: bool
@@ -151,6 +154,33 @@ def export_onnx(
151154
)
152155

153156

157+
def load_fp16_converter():
158+
try:
159+
from onnxruntime.transformers.float16 import convert_float_to_float16
160+
161+
return convert_float_to_float16
162+
except ImportError:
163+
try:
164+
from onnxconverter_common.float16 import convert_float_to_float16
165+
166+
return convert_float_to_float16
167+
except ImportError as exc:
168+
raise RuntimeError(
169+
"FP16 conversion requires onnxruntime.transformers.float16 or onnxconverter-common."
170+
) from exc
171+
172+
173+
def export_fp16(onnx_path: Path, fp16_path: Path) -> None:
174+
converter = load_fp16_converter()
175+
model = onnx.load(onnx_path.as_posix())
176+
params = inspect.signature(converter).parameters
177+
kwargs = {}
178+
if "keep_io_types" in params:
179+
kwargs["keep_io_types"] = True
180+
fp16_model = converter(model, **kwargs)
181+
onnx.save(fp16_model, fp16_path.as_posix())
182+
183+
154184
def export(config: ExportConfig) -> None:
155185
config.output_dir.mkdir(parents=True, exist_ok=True)
156186

@@ -177,6 +207,9 @@ def export(config: ExportConfig) -> None:
177207
opset=config.opset,
178208
)
179209

210+
if config.fp16:
211+
export_fp16(onnx_path, config.output_dir / "model_fp16.onnx")
212+
180213
if config.quantize:
181214
quantize_dynamic(
182215
onnx_path.as_posix(),
@@ -243,6 +276,11 @@ def parse_args() -> ExportConfig:
243276
action="store_true",
244277
help="Include token_type_ids input in the ONNX graph.",
245278
)
279+
parser.add_argument(
280+
"--no-fp16",
281+
action="store_true",
282+
help="Skip FP16 conversion step.",
283+
)
246284
parser.add_argument(
247285
"--no-validate",
248286
action="store_true",
@@ -270,6 +308,7 @@ def parse_args() -> ExportConfig:
270308
max_seq_len=args.max_seq_len,
271309
opset=args.opset,
272310
include_token_type_ids=args.include_token_type_ids,
311+
fp16=not args.no_fp16,
273312
quantize=not args.no_quantize,
274313
validate=not args.no_validate,
275314
validate_quantized=args.validate_quantized,

spec/integration_spec.rb

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66

77
describe 'Gliner Integration', if: ENV.key?('GLINER_INTEGRATION') do
88
REPO_ID = 'cuerbot/gliner2-multi-v1'
9+
REPO_SUBDIR = 'onnx'
10+
MODEL_FILE = 'model_fp16.onnx'
911

1012
describe 'real model inference' do
1113
context 'with entities extraction' do
1214
it 'extracts entities correctly' do
1315
model_dir = ensure_model_dir!
14-
Gliner.load(model_dir)
16+
Gliner.load(model_dir, file: MODEL_FILE)
1517

1618
text = 'Apple CEO Tim Cook announced iPhone 15 in Cupertino yesterday.'
1719
labels = {
@@ -35,7 +37,7 @@
3537
context 'with text classification' do
3638
it 'classifies sentiment correctly' do
3739
model_dir = ensure_model_dir!
38-
Gliner.load(model_dir)
40+
Gliner.load(model_dir, file: MODEL_FILE)
3941

4042
text = 'This laptop has amazing performance but terrible battery life!'
4143
schema = { 'sentiment' => %w[positive negative neutral] }
@@ -49,7 +51,7 @@
4951
context 'with structured extraction' do
5052
it 'extracts JSON structure correctly' do
5153
model_dir = ensure_model_dir!
52-
Gliner.load(model_dir)
54+
Gliner.load(model_dir, file: MODEL_FILE)
5355

5456
text = 'iPhone 15 Pro Max with 256GB storage, A17 Pro chip, priced at $1199.'
5557
complex_schema = {
@@ -72,7 +74,7 @@
7274

7375
it 'supports choices and multiple instances' do
7476
model_dir = ensure_model_dir!
75-
Gliner.load(model_dir)
77+
Gliner.load(model_dir, file: MODEL_FILE)
7678

7779
text = <<~TEXT
7880
Transaction 1
@@ -130,13 +132,13 @@ def ensure_model_dir!
130132

131133
download(hf_resolve_url('tokenizer.json').to_s, File.join(dir, 'tokenizer.json'))
132134
download(hf_resolve_url('config.json').to_s, File.join(dir, 'config.json'))
133-
download(hf_resolve_url('model_int8.onnx').to_s, File.join(dir, 'model_int8.onnx'))
135+
download(hf_resolve_url(MODEL_FILE).to_s, File.join(dir, MODEL_FILE))
134136

135137
dir
136138
end
137139

138140
def hf_resolve_url(filename)
139-
"https://huggingface.co/#{REPO_ID}/resolve/main/#{filename}"
141+
"https://huggingface.co/#{REPO_ID}/resolve/main/#{REPO_SUBDIR}/#{filename}"
140142
end
141143

142144
def download(url, dest)

0 commit comments

Comments
 (0)