trimbed 0.0.1__py3-none-any.whl
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.
- trimbed/__init__.py +75 -0
- trimbed/_logging.py +45 -0
- trimbed/backends/__init__.py +84 -0
- trimbed/backends/base.py +85 -0
- trimbed/backends/bpe.py +63 -0
- trimbed/backends/unigram.py +35 -0
- trimbed/backends/wordlevel.py +17 -0
- trimbed/backends/wordpiece.py +18 -0
- trimbed/bytelevel.py +71 -0
- trimbed/cli/__init__.py +7 -0
- trimbed/cli/__main__.py +51 -0
- trimbed/cli/count_tokens.py +85 -0
- trimbed/cli/inspect_tokenizer.py +65 -0
- trimbed/cli/list_presets.py +48 -0
- trimbed/cli/trim_vocab.py +167 -0
- trimbed/config.py +389 -0
- trimbed/counting.py +260 -0
- trimbed/exceptions.py +27 -0
- trimbed/loading.py +164 -0
- trimbed/model_trim.py +176 -0
- trimbed/pipeline.py +290 -0
- trimbed/presets.py +373 -0
- trimbed/remap.py +133 -0
- trimbed/report.py +267 -0
- trimbed/selection.py +394 -0
- trimbed/sidecar.py +112 -0
- trimbed/spec.py +367 -0
- trimbed/tokenizer_trim.py +150 -0
- trimbed/verify.py +220 -0
- trimbed-0.0.1.dist-info/METADATA +141 -0
- trimbed-0.0.1.dist-info/RECORD +34 -0
- trimbed-0.0.1.dist-info/WHEEL +4 -0
- trimbed-0.0.1.dist-info/entry_points.txt +2 -0
- trimbed-0.0.1.dist-info/licenses/LICENSE +201 -0
trimbed/__init__.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Trim a tokenizer's vocabulary, and optionally its model, down to what you need.
|
|
2
|
+
|
|
3
|
+
Typical use:
|
|
4
|
+
|
|
5
|
+
>>> from trimbed import TrimConfig, TrimPipeline
|
|
6
|
+
>>> config = TrimConfig.from_yaml("config.yaml")
|
|
7
|
+
>>> report = TrimPipeline(config).run()
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from importlib.metadata import version
|
|
13
|
+
|
|
14
|
+
from trimbed.backends import VocabBackend, get_backend, register_backend, supported_model_types
|
|
15
|
+
from trimbed.config import (
|
|
16
|
+
CorpusConfig,
|
|
17
|
+
DatasetSpec,
|
|
18
|
+
EmbeddingTrimConfig,
|
|
19
|
+
SelectionConfig,
|
|
20
|
+
TrimConfig,
|
|
21
|
+
load_config,
|
|
22
|
+
parse_overrides,
|
|
23
|
+
)
|
|
24
|
+
from trimbed.counting import CorpusCounter, CorpusCounts
|
|
25
|
+
from trimbed.exceptions import MissingDependencyError
|
|
26
|
+
from trimbed.model_trim import trim_model
|
|
27
|
+
from trimbed.pipeline import TrimPipeline
|
|
28
|
+
from trimbed.presets import available_presets, register_preset, resolve_preset
|
|
29
|
+
from trimbed.remap import IdRemap
|
|
30
|
+
from trimbed.report import ModelReport, ModelVerificationReport, TrimReport, VerificationReport
|
|
31
|
+
from trimbed.selection import Selection, select_tokens
|
|
32
|
+
from trimbed.sidecar import DEFAULT_SIDECAR_PATTERNS, copy_sidecar_files
|
|
33
|
+
from trimbed.spec import TokenizerSpec
|
|
34
|
+
from trimbed.tokenizer_trim import TrimmedTokenizer, trim_tokenizer
|
|
35
|
+
from trimbed.verify import verify_model, verify_tokenizer
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
__version__ = version("trimbed")
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"DEFAULT_SIDECAR_PATTERNS",
|
|
42
|
+
"CorpusConfig",
|
|
43
|
+
"CorpusCounter",
|
|
44
|
+
"CorpusCounts",
|
|
45
|
+
"DatasetSpec",
|
|
46
|
+
"EmbeddingTrimConfig",
|
|
47
|
+
"IdRemap",
|
|
48
|
+
"MissingDependencyError",
|
|
49
|
+
"ModelReport",
|
|
50
|
+
"ModelVerificationReport",
|
|
51
|
+
"Selection",
|
|
52
|
+
"SelectionConfig",
|
|
53
|
+
"TokenizerSpec",
|
|
54
|
+
"TrimConfig",
|
|
55
|
+
"TrimPipeline",
|
|
56
|
+
"TrimReport",
|
|
57
|
+
"TrimmedTokenizer",
|
|
58
|
+
"VerificationReport",
|
|
59
|
+
"VocabBackend",
|
|
60
|
+
"__version__",
|
|
61
|
+
"available_presets",
|
|
62
|
+
"copy_sidecar_files",
|
|
63
|
+
"get_backend",
|
|
64
|
+
"load_config",
|
|
65
|
+
"parse_overrides",
|
|
66
|
+
"register_backend",
|
|
67
|
+
"register_preset",
|
|
68
|
+
"resolve_preset",
|
|
69
|
+
"select_tokens",
|
|
70
|
+
"supported_model_types",
|
|
71
|
+
"trim_model",
|
|
72
|
+
"trim_tokenizer",
|
|
73
|
+
"verify_model",
|
|
74
|
+
"verify_tokenizer",
|
|
75
|
+
]
|
trimbed/_logging.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Logging helpers shared by the library and the scripts.
|
|
2
|
+
|
|
3
|
+
`configure_logging()` should be called once at the start of a script to set up the package logger,
|
|
4
|
+
e.g. to set it to quiet mode.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
LOGGER_NAME = "trimbed"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_logger(name: str | None = None) -> logging.Logger:
|
|
16
|
+
"""Return the package logger, or a child of it.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
name: Optional dotted suffix, typically `__name__`.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
The `trimbed` logger or the named child logger.
|
|
23
|
+
"""
|
|
24
|
+
if name is None or name == LOGGER_NAME:
|
|
25
|
+
return logging.getLogger(LOGGER_NAME)
|
|
26
|
+
suffix = name.removeprefix(f"{LOGGER_NAME}.")
|
|
27
|
+
return logging.getLogger(f"{LOGGER_NAME}.{suffix}")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def configure_logging(verbose: bool = False, quiet: bool = False) -> None:
|
|
31
|
+
"""Attach a single stderr handler to the package logger.
|
|
32
|
+
|
|
33
|
+
Repeated calls replace the previous handler rather than stacking.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
verbose: Emit DEBUG-level records.
|
|
37
|
+
quiet: Emit only warnings and above. Ignored when `verbose` is set.
|
|
38
|
+
"""
|
|
39
|
+
logger = logging.getLogger(LOGGER_NAME)
|
|
40
|
+
logger.handlers.clear()
|
|
41
|
+
handler = logging.StreamHandler()
|
|
42
|
+
handler.setFormatter(logging.Formatter("%(levelname)-8s %(name)s: %(message)s"))
|
|
43
|
+
logger.addHandler(handler)
|
|
44
|
+
logger.setLevel(logging.DEBUG if verbose else logging.WARNING if quiet else logging.INFO)
|
|
45
|
+
logger.propagate = False
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Registry of per-tokenizer-family trimming adapters.
|
|
2
|
+
|
|
3
|
+
Adding support for a new tokenizer family means adding one module here and decorating
|
|
4
|
+
its class with `register_backend`, following one of the existing backends. Nothing else
|
|
5
|
+
in the package needs to change.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from trimbed.backends.base import VocabBackend
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_REGISTRY: dict[str, VocabBackend] = {}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def register_backend[BackendT: type[VocabBackend]](cls: BackendT) -> BackendT:
|
|
17
|
+
"""Register a backend adapter under its declared `model_type`.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
cls: A concrete [`VocabBackend`][trimbed.backends.base.VocabBackend] subclass.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
The class unchanged, so this can be used as a decorator.
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
ValueError: If the class has no `model_type` or the type is already taken.
|
|
27
|
+
"""
|
|
28
|
+
model_type = getattr(cls, "model_type", None)
|
|
29
|
+
if not model_type:
|
|
30
|
+
raise ValueError(f"{cls.__name__} must declare a `model_type` class attribute")
|
|
31
|
+
if model_type in _REGISTRY:
|
|
32
|
+
raise ValueError(f"a backend for model type {model_type!r} is already registered")
|
|
33
|
+
_REGISTRY[model_type] = cls()
|
|
34
|
+
return cls
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_backend(model_type: str) -> VocabBackend:
|
|
38
|
+
"""Look up the adapter for a `tokenizer.json` model type.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
model_type: Value of `model.type`, e.g. `"BPE"` for codefuse-ai/F2LLM-v2-160M
|
|
42
|
+
or `"WordPiece"` for google-bert/bert-base-cased.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
The registered adapter instance, e.g. a [`BpeBackend`][trimbed.backends.bpe.BpeBackend].
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
KeyError: If no adapter handles that model type.
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
return _REGISTRY[model_type]
|
|
52
|
+
except KeyError:
|
|
53
|
+
supported = ", ".join(sorted(_REGISTRY)) or "none"
|
|
54
|
+
raise KeyError(
|
|
55
|
+
f"no trimming backend registered for tokenizer model type {model_type!r} (supported: {supported})"
|
|
56
|
+
) from None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def supported_model_types() -> tuple[str, ...]:
|
|
60
|
+
"""Return the tokenizer model types that can currently be trimmed.
|
|
61
|
+
|
|
62
|
+
E.g. `("BPE", "Unigram", "WordLevel", "WordPiece")`.
|
|
63
|
+
"""
|
|
64
|
+
return tuple(sorted(_REGISTRY))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# Importing the adapters is what populates the registry, since that is when their
|
|
68
|
+
# `register_backend` decorators run.
|
|
69
|
+
from trimbed.backends.bpe import BpeBackend
|
|
70
|
+
from trimbed.backends.unigram import UnigramBackend
|
|
71
|
+
from trimbed.backends.wordlevel import WordLevelBackend
|
|
72
|
+
from trimbed.backends.wordpiece import WordPieceBackend
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
__all__ = [
|
|
76
|
+
"BpeBackend",
|
|
77
|
+
"UnigramBackend",
|
|
78
|
+
"VocabBackend",
|
|
79
|
+
"WordLevelBackend",
|
|
80
|
+
"WordPieceBackend",
|
|
81
|
+
"get_backend",
|
|
82
|
+
"register_backend",
|
|
83
|
+
"supported_model_types",
|
|
84
|
+
]
|
trimbed/backends/base.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""The base interface for different tokenizer families.
|
|
2
|
+
|
|
3
|
+
We luckily can rely on `skeletoken` to do the vocabulary surgery for every tokenizer
|
|
4
|
+
family, so these adapters only carry what a trimmer has to know and a serialiser does
|
|
5
|
+
not: which tokens can never be dropped, and which tokens depend on which.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from abc import ABC
|
|
11
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from trimbed.spec import TokenizerSpec
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class VocabBackend(ABC):
|
|
19
|
+
"""Describes the structural constraints of one `tokenizers` model type.
|
|
20
|
+
|
|
21
|
+
Subclasses answer which tokens can never be removed without breaking encoding
|
|
22
|
+
outright and which tokens can only be produced if some other token survives.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
model_type: ClassVar[str]
|
|
26
|
+
"""Value of `model.type` in tokenizer.json that this adapter handles.
|
|
27
|
+
|
|
28
|
+
E.g. `"BPE"` for codefuse-ai/F2LLM-v2-160M, `"WordPiece"` for
|
|
29
|
+
google-bert/bert-base-cased and `"Unigram"` for google-t5/t5-small.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def structural_tokens(self, spec: TokenizerSpec) -> set[str]:
|
|
33
|
+
"""Return tokens that must survive or the tokenizer stops working.
|
|
34
|
+
|
|
35
|
+
The default covers the unknown token. Byte-level backends extend it with the byte
|
|
36
|
+
alphabet.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
spec: The tokenizer being trimmed.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Token strings that may never be dropped. E.g. `{"[UNK]"}` for
|
|
43
|
+
google-bert/bert-base-cased, and an empty set for codefuse-ai/F2LLM-v2-160M,
|
|
44
|
+
which declares no unknown token at all.
|
|
45
|
+
"""
|
|
46
|
+
unk = spec.unk_token
|
|
47
|
+
return {unk} if unk else set()
|
|
48
|
+
|
|
49
|
+
def dependencies(self, spec: TokenizerSpec) -> dict[int, tuple[int, ...]]:
|
|
50
|
+
"""Return which other tokens each token needs in order to stay reachable.
|
|
51
|
+
|
|
52
|
+
Keeping a token while dropping something it is assembled from leaves it in the
|
|
53
|
+
vocabulary but unreachable, so text silently fragments.
|
|
54
|
+
Backends with no such structure return an empty mapping.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
spec: The tokenizer being trimmed.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
A mapping of token id to the ids it directly depends on. Empty here, since
|
|
61
|
+
BPE is the only family that builds tokens out of other tokens. See
|
|
62
|
+
[`BpeBackend.dependencies`][trimbed.backends.bpe.BpeBackend.dependencies] for
|
|
63
|
+
how it fills this in.
|
|
64
|
+
"""
|
|
65
|
+
return {}
|
|
66
|
+
|
|
67
|
+
def _byte_fallback_tokens(self, spec: TokenizerSpec) -> set[str]:
|
|
68
|
+
"""Return the `<0xNN>` tokens present in the vocabulary, if byte fallback is on.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
spec: The tokenizer being trimmed.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
The byte-fallback tokens, e.g. `{"<0x00>", "<0x01>", ..., "<0xFF>"}`, or an
|
|
75
|
+
empty set when the feature is off, as it is for google-t5/t5-small and
|
|
76
|
+
FacebookAI/xlm-roberta-base. A byte-level BPE never needs it: its alphabet
|
|
77
|
+
already covers all 256 bytes.
|
|
78
|
+
"""
|
|
79
|
+
if not getattr(spec.model, "byte_fallback", False):
|
|
80
|
+
return set()
|
|
81
|
+
return {f"<0x{byte:02X}>" for byte in range(256)} & spec.vocabulary.keys()
|
|
82
|
+
|
|
83
|
+
def __repr__(self) -> str:
|
|
84
|
+
"""Return a short debug representation."""
|
|
85
|
+
return f"<{type(self).__name__} model_type={self.model_type!r}>"
|
trimbed/backends/bpe.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Selection constraints for byte-pair-encoding tokenizers (GPT-2, Qwen, Llama, ...)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from trimbed.backends import register_backend
|
|
8
|
+
from trimbed.backends.base import VocabBackend
|
|
9
|
+
from trimbed.bytelevel import byte_level_alphabet
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from trimbed.spec import TokenizerSpec
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@register_backend
|
|
17
|
+
class BpeBackend(VocabBackend):
|
|
18
|
+
"""Keeps a BPE vocabulary encodable and its merge chains intact."""
|
|
19
|
+
|
|
20
|
+
model_type = "BPE"
|
|
21
|
+
|
|
22
|
+
def structural_tokens(self, spec: TokenizerSpec) -> set[str]:
|
|
23
|
+
"""Return the unk token plus, for byte-level BPE, the whole byte alphabet.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
spec: The tokenizer being trimmed.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Token strings that may never be dropped. For codefuse-ai/F2LLM-v2-160M that is
|
|
30
|
+
the 256 alphabet characters (`"!"`, `"Ġ"` for a space, `"Ċ"` for a
|
|
31
|
+
newline, ...) and nothing else, since it has no unknown token.
|
|
32
|
+
"""
|
|
33
|
+
required = super().structural_tokens(spec)
|
|
34
|
+
if spec.uses_byte_level:
|
|
35
|
+
# add the byte-level alphabet, but only the bytes that are actually in the vocabulary
|
|
36
|
+
required |= set(byte_level_alphabet()) & spec.vocabulary.keys()
|
|
37
|
+
return required | self._byte_fallback_tokens(spec)
|
|
38
|
+
|
|
39
|
+
def dependencies(self, spec: TokenizerSpec) -> dict[int, tuple[int, ...]]:
|
|
40
|
+
"""Map each merged token to the pair it is assembled from.
|
|
41
|
+
|
|
42
|
+
codefuse-ai/F2LLM-v2-160M builds `"Ġthe"` by merging `"Ġth"` with `"e"`, so
|
|
43
|
+
dropping `"Ġth"` leaves `"Ġthe"` in the vocabulary but unreachable and the text
|
|
44
|
+
quietly tokenizes into characters instead. The selector uses this mapping to pull
|
|
45
|
+
in whatever a kept token is built from.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
spec: The tokenizer being trimmed.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
A mapping of merged token id to its two parent ids, e.g. `Ġthe -> (Ġth, e)`
|
|
52
|
+
and `Ġworld -> (Ġw, orld)`. That is 151,387 entries for
|
|
53
|
+
codefuse-ai/F2LLM-v2-160M, one per merge rule whose three tokens all survive.
|
|
54
|
+
"""
|
|
55
|
+
vocabulary = spec.vocabulary
|
|
56
|
+
dependencies: dict[int, tuple[int, ...]] = {}
|
|
57
|
+
for left, right in spec.model.merges.root:
|
|
58
|
+
# concatenate the strings and check if the merged token is actually in vocab
|
|
59
|
+
merged = left + right
|
|
60
|
+
if merged in vocabulary and left in vocabulary and right in vocabulary:
|
|
61
|
+
# The first rule producing a token is the one BPE actually applies.
|
|
62
|
+
dependencies.setdefault(vocabulary[merged], (vocabulary[left], vocabulary[right]))
|
|
63
|
+
return dependencies
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Selection constraints for Unigram tokenizers (XLM-R, mT5, ALBERT, ...)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from trimbed.backends import register_backend
|
|
8
|
+
from trimbed.backends.base import VocabBackend
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from trimbed.spec import TokenizerSpec
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@register_backend
|
|
16
|
+
class UnigramBackend(VocabBackend):
|
|
17
|
+
"""Keeps a Unigram vocabulary usable.
|
|
18
|
+
|
|
19
|
+
Unigram scores whole candidate pieces independently rather than composing them from
|
|
20
|
+
merges.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
model_type = "Unigram"
|
|
24
|
+
|
|
25
|
+
def structural_tokens(self, spec: TokenizerSpec) -> set[str]:
|
|
26
|
+
"""Return the unk token plus any byte-fallback tokens the model relies on.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
spec: The tokenizer being trimmed.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Token strings that may never be dropped, e.g. `{"<unk>"}` for
|
|
33
|
+
google-t5/t5-small and FacebookAI/xlm-roberta-base.
|
|
34
|
+
"""
|
|
35
|
+
return super().structural_tokens(spec) | self._byte_fallback_tokens(spec)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Selection constraints for WordLevel tokenizers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from trimbed.backends import register_backend
|
|
6
|
+
from trimbed.backends.base import VocabBackend
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@register_backend
|
|
10
|
+
class WordLevelBackend(VocabBackend):
|
|
11
|
+
"""Keeps a flat word-level vocabulary usable.
|
|
12
|
+
|
|
13
|
+
The simplest case: no merges, no subwords, so no dependencies. Anything dropped
|
|
14
|
+
becomes unk, which makes the unk token the only structural requirement.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
model_type = "WordLevel"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Selection constraints for WordPiece tokenizers (BERT and friends)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from trimbed.backends import register_backend
|
|
6
|
+
from trimbed.backends.base import VocabBackend
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@register_backend
|
|
10
|
+
class WordPieceBackend(VocabBackend):
|
|
11
|
+
"""Keeps a WordPiece vocabulary usable.
|
|
12
|
+
|
|
13
|
+
WordPiece greedily matches the longest prefix in the vocabulary and needs no merge
|
|
14
|
+
table, so tokens carry no dependencies: dropping `"##ing"` costs coverage but
|
|
15
|
+
never makes another vocabulary token unreachable.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
model_type = "WordPiece"
|
trimbed/bytelevel.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""The GPT-2-style byte-to-unicode mapping used by ByteLevel tokenizers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from functools import cache
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@cache
|
|
9
|
+
def bytes_to_unicode() -> dict[int, str]:
|
|
10
|
+
"""Return the reversible byte -> printable-character map used by ByteLevel.
|
|
11
|
+
|
|
12
|
+
Borrowed mostly from GPT-2's tokenizer, also see the [transformers implementation](https://github.com/huggingface/transformers/blob/36bc98ef9dd009569366f5e253ec1876ecafd925/src/transformers/convert_slow_tokenizer.py#L1879).
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
A mapping of all 256 byte values to distinct printable characters, e.g.
|
|
16
|
+
`32 -> "Ġ"` (space), `10 -> "Ċ"` (newline), `0 -> "Ā"`, and `65 -> "A"`
|
|
17
|
+
for the bytes that are already printable.
|
|
18
|
+
"""
|
|
19
|
+
printable = (
|
|
20
|
+
list(range(ord("!"), ord("~") + 1)) + list(range(ord("¡"), ord("¬") + 1)) + list(range(ord("®"), ord("ÿ") + 1))
|
|
21
|
+
)
|
|
22
|
+
mapped = list(printable)
|
|
23
|
+
spare = 0
|
|
24
|
+
for byte in range(256):
|
|
25
|
+
if byte not in printable:
|
|
26
|
+
printable.append(byte)
|
|
27
|
+
mapped.append(256 + spare)
|
|
28
|
+
spare += 1
|
|
29
|
+
return {byte: chr(code) for byte, code in zip(printable, mapped, strict=True)}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@cache
|
|
33
|
+
def unicode_to_bytes() -> dict[str, int]:
|
|
34
|
+
"""Return the inverse of [`bytes_to_unicode`][trimbed.bytelevel.bytes_to_unicode].
|
|
35
|
+
|
|
36
|
+
E.g. `"Ġ" -> 32` and `"Ċ" -> 10`.
|
|
37
|
+
"""
|
|
38
|
+
return {char: byte for byte, char in bytes_to_unicode().items()}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@cache
|
|
42
|
+
def byte_level_alphabet() -> frozenset[str]:
|
|
43
|
+
"""Return the 256 characters a ByteLevel pre-tokenizer can produce.
|
|
44
|
+
|
|
45
|
+
Every one of them must stay in the vocabulary of a byte-level tokenizer, otherwise
|
|
46
|
+
some byte sequences become unencodable.
|
|
47
|
+
"""
|
|
48
|
+
return frozenset(bytes_to_unicode().values())
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def decode_byte_level(token: str) -> str | None:
|
|
52
|
+
"""Turn a byte-level token back into the text it represents.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
token: A token as stored in a ByteLevel vocabulary, e.g. `"Ġde"`, which will
|
|
56
|
+
produce `" de"`. Multi-byte characters arrive as several alphabet
|
|
57
|
+
characters, so `"é"` produces `"é"`.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
The decoded text, or `None` if the token is a partial UTF-8 sequence or
|
|
61
|
+
contains characters outside the byte-level alphabet.
|
|
62
|
+
"""
|
|
63
|
+
table = unicode_to_bytes()
|
|
64
|
+
try:
|
|
65
|
+
raw = bytes(table[char] for char in token)
|
|
66
|
+
except KeyError:
|
|
67
|
+
return None
|
|
68
|
+
try:
|
|
69
|
+
return raw.decode("utf-8")
|
|
70
|
+
except UnicodeDecodeError:
|
|
71
|
+
return None
|
trimbed/cli/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""The command surface: one module per `trimbed` subcommand, plus the router.
|
|
2
|
+
|
|
3
|
+
The router is [`trimbed.cli.__main__`][trimbed.cli.__main__].
|
|
4
|
+
Each module holds a `run(...)` with the logic and an `add_arguments(parser)` describing
|
|
5
|
+
its command line, so a command is equally usable from the shell and as a plain Python
|
|
6
|
+
call.
|
|
7
|
+
"""
|
trimbed/cli/__main__.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""The main `trimbed` command.
|
|
2
|
+
|
|
3
|
+
trimbed inspect --model codefuse-ai/F2LLM-v2-160M
|
|
4
|
+
trimbed trim --config my_config.yaml --dry-run selection.top_k=30000
|
|
5
|
+
trimbed presets
|
|
6
|
+
|
|
7
|
+
Each subcommand owns its own arguments, in `add_arguments`, next to the `run` they feed.
|
|
8
|
+
This module holds the table of subcommand names and nothing else, so adding a command is
|
|
9
|
+
a new module plus one line here.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
from types import ModuleType
|
|
16
|
+
|
|
17
|
+
from trimbed.cli import count_tokens, inspect_tokenizer, list_presets, trim_vocab
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# subcommand name -> the module implementing it and the one line `trimbed --help` shows.
|
|
21
|
+
COMMANDS: dict[str, tuple[ModuleType, str]] = {
|
|
22
|
+
"trim": (trim_vocab, "Trim a tokenizer, and optionally its model, and write the result."),
|
|
23
|
+
"count": (count_tokens, "Count the corpus once and cache the frequencies to JSON."),
|
|
24
|
+
"inspect": (inspect_tokenizer, "Describe a tokenizer as JSON, changing nothing."),
|
|
25
|
+
"presets": (list_presets, "List the registered presets that --keep-preset accepts."),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def main() -> None:
|
|
30
|
+
"""Parse the command line and hand the arguments to the chosen subcommand's `run`."""
|
|
31
|
+
parser = argparse.ArgumentParser(
|
|
32
|
+
prog="trimbed",
|
|
33
|
+
description=(
|
|
34
|
+
"Trim a tokenizer's vocabulary, and optionally its model's embedding table, down"
|
|
35
|
+
" to the subset a corpus and your must-keep rules actually need."
|
|
36
|
+
),
|
|
37
|
+
)
|
|
38
|
+
subparsers = parser.add_subparsers(dest="command", required=True, metavar="COMMAND")
|
|
39
|
+
for name, (module, summary) in COMMANDS.items():
|
|
40
|
+
subparser = subparsers.add_parser(name, help=summary, description=module.DESCRIPTION)
|
|
41
|
+
module.add_arguments(subparser)
|
|
42
|
+
subparser.set_defaults(run=module.run)
|
|
43
|
+
|
|
44
|
+
arguments = vars(parser.parse_args())
|
|
45
|
+
command = arguments.pop("run")
|
|
46
|
+
del arguments["command"]
|
|
47
|
+
command(**arguments)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
main()
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Count corpus token frequencies once and cache them to JSON.
|
|
2
|
+
|
|
3
|
+
Counting is the expensive part of a trimming run and it does not depend on the selection
|
|
4
|
+
policy, so do it once here and point `corpus.counts_cache` at the output to reuse it
|
|
5
|
+
across selection experiments.
|
|
6
|
+
|
|
7
|
+
trimbed count --config my_config.yaml -o counts.json
|
|
8
|
+
trimbed count --config my_config.yaml -o counts.json \
|
|
9
|
+
corpus.batch_size=4000 corpus.num_proc=8
|
|
10
|
+
|
|
11
|
+
Only the corpus half of the config matters here. The selection and embedding sections are
|
|
12
|
+
still validated, but they are not used.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
|
|
19
|
+
from trimbed._logging import configure_logging
|
|
20
|
+
from trimbed.config import load_config, parse_overrides
|
|
21
|
+
from trimbed.counting import CorpusCounter
|
|
22
|
+
from trimbed.loading import load_tokenizer
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def run(
|
|
26
|
+
output: str,
|
|
27
|
+
config: str | None = None,
|
|
28
|
+
model: str | None = None,
|
|
29
|
+
overrides: list[str] | None = None,
|
|
30
|
+
verbose: bool = False,
|
|
31
|
+
quiet: bool = False,
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Count the configured corpus and write the frequencies to a cache file.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
output: JSON file to write the counts to.
|
|
37
|
+
config: Path to a YAML configuration file.
|
|
38
|
+
model: Hub model id or local path, overriding the config.
|
|
39
|
+
overrides: `key=value` strings applied on top of the config and flags.
|
|
40
|
+
verbose: Emit debug logging.
|
|
41
|
+
quiet: Only emit warnings and errors.
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
ValueError: If the resolved config names no datasets to count over.
|
|
45
|
+
"""
|
|
46
|
+
configure_logging(verbose=verbose, quiet=quiet)
|
|
47
|
+
|
|
48
|
+
trim_config = load_config(config, model).with_overrides({"model": model, **parse_overrides(overrides or [])})
|
|
49
|
+
if not trim_config.corpus.datasets:
|
|
50
|
+
raise ValueError("no datasets configured under 'corpus.datasets'; nothing to count")
|
|
51
|
+
|
|
52
|
+
tokenizer = load_tokenizer(trim_config.model, trim_config.revision, trim_config.trust_remote_code)
|
|
53
|
+
counter = CorpusCounter(
|
|
54
|
+
tokenizer, trim_config.corpus, seed=trim_config.seed, sample_size=trim_config.verify_samples
|
|
55
|
+
)
|
|
56
|
+
counts = counter.count()
|
|
57
|
+
counts.save(output)
|
|
58
|
+
print(
|
|
59
|
+
f"counted {counts.total_num_tokens:,} tokens ({counts.distinct_tokens:,} distinct) "
|
|
60
|
+
f"over {counts.num_documents:,} documents -> {output}"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
DESCRIPTION = (
|
|
65
|
+
"Tokenize the configured corpus and write the per-token frequencies to JSON, so"
|
|
66
|
+
" later trimming runs can reuse them instead of re-reading the corpus."
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def add_arguments(parser: argparse.ArgumentParser) -> None:
|
|
71
|
+
"""Add the counting arguments to `parser`.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
parser: The `trimbed count` subparser to populate.
|
|
75
|
+
"""
|
|
76
|
+
parser.add_argument("-c", "--config", help="Path to a YAML configuration file.")
|
|
77
|
+
parser.add_argument("-m", "--model", help="Hub model id or local path (overrides the config).")
|
|
78
|
+
parser.add_argument("-o", "--output", required=True, help="JSON file to write the counts to.")
|
|
79
|
+
parser.add_argument("-v", "--verbose", action="store_true", help="Emit debug logging.")
|
|
80
|
+
parser.add_argument("-q", "--quiet", action="store_true", help="Only emit warnings and errors.")
|
|
81
|
+
parser.add_argument(
|
|
82
|
+
"overrides",
|
|
83
|
+
nargs="*",
|
|
84
|
+
help="Optional key=value overrides, e.g. corpus.batch_size=4000",
|
|
85
|
+
)
|