Skip to content

Commit 967ac8e

Browse files
committed
Add support for pushing audio models to HF hub, and cleanup tiktoken so it can have hf-hub/local-dir based bpe files for Tik
1 parent db4d491 commit 967ac8e

3 files changed

Lines changed: 148 additions & 15 deletions

File tree

src/open_clip/factory.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -931,10 +931,25 @@ def get_tokenizer(
931931
if text_config.get('tokenizer_type', '') == 'tiktoken':
932932
# tiktoken-based tokenizer for generative (GenLIP) models.
933933
encoding_name = text_config.get('tiktoken_name', 'cl100k_base')
934+
bpe_path = text_config.get('tiktoken_bpe_path')
935+
encoding_config_path = text_config.get('tiktoken_config_path')
936+
if bpe_path:
937+
if schema == 'local-dir':
938+
bpe_path = str(local_dir_path / bpe_path)
939+
if encoding_config_path:
940+
encoding_config_path = str(local_dir_path / encoding_config_path)
941+
elif schema == 'hf-hub':
942+
bpe_path = download_pretrained_from_hf(identifier, filename=bpe_path, cache_dir=cache_dir)
943+
if encoding_config_path:
944+
encoding_config_path = download_pretrained_from_hf(
945+
identifier, filename=encoding_config_path, cache_dir=cache_dir,
946+
)
934947
_logger.info(f"Using TikTokenTokenizer with encoding: '{encoding_name}'")
935948
tokenizer = TikTokenTokenizer(
936949
encoding_name=encoding_name,
937950
context_length=context_length,
951+
bpe_path=bpe_path,
952+
encoding_config_path=encoding_config_path,
938953
**{k: v for k, v in tokenizer_kwargs.items() if k in ('add_bos', 'add_eos', 'clean')},
939954
)
940955

src/open_clip/push_to_hf_hub.py

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import argparse
22
import json
3+
from copy import deepcopy
34
from pathlib import Path
45
from tempfile import TemporaryDirectory
56
from typing import Optional, Tuple, Union
@@ -29,27 +30,65 @@
2930

3031
from .constants import HF_WEIGHTS_NAME, HF_SAFE_WEIGHTS_NAME, HF_CONFIG_NAME
3132
from .factory import create_model_from_pretrained, get_model_config, get_tokenizer
32-
from .tokenizer import HFTokenizer, SigLipTokenizer
33+
from .tokenizer import HFTokenizer, SigLipTokenizer, TikTokenTokenizer
34+
35+
36+
_HF_TOKENIZER_FILE_PATTERNS = (
37+
'tokenizer.json',
38+
'tokenizer_config.json',
39+
'special_tokens_map.json',
40+
'added_tokens.json',
41+
'vocab.json',
42+
'merges.txt',
43+
'spiece.model',
44+
'sentencepiece.bpe.model',
45+
)
46+
47+
48+
def _get_text_config(model_config: Optional[dict]) -> dict:
49+
if not model_config:
50+
return {}
51+
return model_config.get('text_cfg') or model_config.get('multimodal_cfg') or {}
52+
53+
54+
def _uses_tiktoken(tokenizer, model_config: Optional[dict]) -> bool:
55+
text_cfg = _get_text_config(model_config)
56+
return isinstance(tokenizer, TikTokenTokenizer) or text_cfg.get('tokenizer_type') == 'tiktoken'
57+
58+
59+
def _with_tiktoken_assets(model_config: Optional[dict], tokenizer_assets: Optional[dict]) -> Optional[dict]:
60+
if not model_config or not isinstance(tokenizer_assets, dict):
61+
return model_config
62+
model_config = deepcopy(model_config)
63+
text_cfg = model_config.get('text_cfg') or model_config.get('multimodal_cfg')
64+
if text_cfg is not None:
65+
text_cfg.update({
66+
k: v for k, v in tokenizer_assets.items()
67+
if k in ('tiktoken_bpe_path', 'tiktoken_config_path') and v
68+
})
69+
return model_config
3370

3471

3572
def save_config_for_hf(
3673
model,
3774
config_path: str,
3875
model_config: Optional[dict],
3976
):
40-
preprocess_cfg = {
41-
'mean': model.visual.image_mean,
42-
'std': model.visual.image_std,
43-
}
44-
other_pp = getattr(model.visual, 'preprocess_cfg', {})
45-
if 'interpolation' in other_pp:
46-
preprocess_cfg['interpolation'] = other_pp['interpolation']
47-
if 'resize_mode' in other_pp:
48-
preprocess_cfg['resize_mode'] = other_pp['resize_mode']
4977
hf_config = {
5078
'model_cfg': model_config,
51-
'preprocess_cfg': preprocess_cfg,
5279
}
80+
visual = getattr(model, 'visual', None)
81+
if visual is not None:
82+
preprocess_cfg = {
83+
'mean': visual.image_mean,
84+
'std': visual.image_std,
85+
}
86+
other_pp = getattr(visual, 'preprocess_cfg', {})
87+
if 'interpolation' in other_pp:
88+
preprocess_cfg['interpolation'] = other_pp['interpolation']
89+
if 'resize_mode' in other_pp:
90+
preprocess_cfg['resize_mode'] = other_pp['resize_mode']
91+
hf_config['preprocess_cfg'] = preprocess_cfg
5392

5493
with config_path.open('w') as f:
5594
json.dump(hf_config, f, indent=2)
@@ -76,10 +115,17 @@ def save_for_hf(
76115
if safe_serialization is False or safe_serialization == "both":
77116
torch.save(tensors, save_directory / HF_WEIGHTS_NAME)
78117

79-
tokenizer.save_pretrained(save_directory)
118+
tokenizer_assets = None
119+
save_pretrained = getattr(tokenizer, 'save_pretrained', None)
120+
if callable(save_pretrained):
121+
tokenizer_assets = save_pretrained(save_directory)
80122

81123
config_path = save_directory / config_filename
82-
save_config_for_hf(model, config_path, model_config=model_config)
124+
save_config_for_hf(
125+
model,
126+
config_path,
127+
model_config=_with_tiktoken_assets(model_config, tokenizer_assets),
128+
)
83129

84130

85131
def push_to_hf_hub(
@@ -95,7 +141,9 @@ def push_to_hf_hub(
95141
model_card: Optional[dict] = None,
96142
safe_serialization: Union[bool, str] = 'both',
97143
):
98-
if not isinstance(tokenizer, (HFTokenizer, SigLipTokenizer)):
144+
uses_tiktoken = _uses_tiktoken(tokenizer, model_config)
145+
delete_patterns = _HF_TOKENIZER_FILE_PATTERNS if uses_tiktoken else None
146+
if not isinstance(tokenizer, (HFTokenizer, SigLipTokenizer)) and not uses_tiktoken:
99147
# FIXME this makes it awkward to push models with new tokenizers, come up with better soln.
100148
# default CLIP tokenizers use https://huggingface.co/openai/clip-vit-large-patch14
101149
tokenizer = HFTokenizer('openai/clip-vit-large-patch14')
@@ -150,6 +198,7 @@ def push_to_hf_hub(
150198
revision=revision,
151199
create_pr=create_pr,
152200
commit_message=commit_message,
201+
delete_patterns=delete_patterns,
153202
)
154203

155204

src/open_clip/tokenizer.py

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44
"""
55
import gzip
66
import html
7+
import base64
8+
import json
79
import os
810
import random
911
import string
1012
from functools import lru_cache, partial
13+
from pathlib import Path
1114
from typing import Callable, Dict, List, Optional, Tuple, Union
1215
import warnings
1316

@@ -760,14 +763,27 @@ def __init__(
760763
add_bos: bool = True,
761764
add_eos: bool = True,
762765
clean: Optional[str] = None,
766+
bpe_path: Optional[Union[str, os.PathLike]] = None,
767+
encoding_config_path: Optional[Union[str, os.PathLike]] = None,
763768
):
764769
try:
765770
import tiktoken
766771
except ImportError as e:
767772
raise ImportError("Please install tiktoken to use TikTokenTokenizer (`pip install tiktoken`).") from e
768773

769774
self.encoding_name = encoding_name
770-
self.enc = tiktoken.get_encoding(encoding_name)
775+
if bpe_path is not None:
776+
cfg = self._load_encoding_config(encoding_name, encoding_config_path)
777+
self.enc = tiktoken.Encoding(
778+
cfg.get("name", encoding_name),
779+
pat_str=cfg["pat_str"],
780+
mergeable_ranks=self._load_tiktoken_bpe(bpe_path),
781+
special_tokens={str(k): int(v) for k, v in cfg.get("special_tokens", {}).items()},
782+
explicit_n_vocab=cfg.get("explicit_n_vocab"),
783+
)
784+
else:
785+
self.enc = tiktoken.get_encoding(encoding_name)
786+
self.encoding_name = self.enc.name
771787
self.context_length = context_length
772788
self.add_bos = add_bos
773789
self.add_eos = add_eos
@@ -787,6 +803,59 @@ def __init__(
787803
self.all_special_ids = [self.eot_token_id, self.pad_token_id, self.bos_token_id]
788804
self.vocab_size = base + 3
789805

806+
@staticmethod
807+
def _load_tiktoken_bpe(path: Union[str, os.PathLike]) -> Dict[bytes, int]:
808+
ranks = {}
809+
with open(path, "rb") as f:
810+
for line in f:
811+
if not line.strip():
812+
continue
813+
token, rank = line.split()
814+
ranks[base64.b64decode(token)] = int(rank)
815+
return ranks
816+
817+
@staticmethod
818+
def _dump_tiktoken_bpe(ranks: Dict[bytes, int], path: Union[str, os.PathLike]) -> None:
819+
with open(path, "wb") as f:
820+
for token, rank in sorted(ranks.items(), key=lambda x: x[1]):
821+
f.write(base64.b64encode(token) + b" " + str(rank).encode() + b"\n")
822+
823+
@staticmethod
824+
def _load_encoding_config(encoding_name: str, path: Optional[Union[str, os.PathLike]]) -> Dict[str, object]:
825+
if path is None:
826+
raise ValueError(
827+
f"encoding_config_path is required when loading tiktoken encoding {encoding_name!r} from bpe_path."
828+
)
829+
with open(path, "r", encoding="utf-8") as f:
830+
return json.load(f)
831+
832+
def save_pretrained(self, dest: Union[str, os.PathLike]) -> Dict[str, str]:
833+
dest = Path(dest)
834+
asset_dir = dest / "open_clip_tiktoken"
835+
asset_dir.mkdir(parents=True, exist_ok=True)
836+
837+
stem = self.encoding_name.replace("/", "_")
838+
bpe_path = asset_dir / f"{stem}.tiktoken"
839+
config_path = asset_dir / f"{stem}.json"
840+
841+
self._dump_tiktoken_bpe(self.enc._mergeable_ranks, bpe_path)
842+
with config_path.open("w", encoding="utf-8") as f:
843+
json.dump(
844+
{
845+
"name": self.enc.name,
846+
"pat_str": self.enc._pat_str,
847+
"special_tokens": self.enc._special_tokens,
848+
"explicit_n_vocab": self.enc.n_vocab,
849+
},
850+
f,
851+
indent=2,
852+
)
853+
854+
return {
855+
"tiktoken_bpe_path": str(bpe_path.relative_to(dest)),
856+
"tiktoken_config_path": str(config_path.relative_to(dest)),
857+
}
858+
790859
def encode(self, text: str) -> List[int]:
791860
# encode_ordinary ignores any special-token markup in the text, treating it as plain bytes.
792861
if self.clean_fn is not None:

0 commit comments

Comments
 (0)