ellf-recipes-gpu 0.1.1__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.
- ellf_recipes_gpu-0.1.1/LICENSE +3 -0
- ellf_recipes_gpu-0.1.1/MANIFEST.in +2 -0
- ellf_recipes_gpu-0.1.1/PKG-INFO +23 -0
- ellf_recipes_gpu-0.1.1/ellf_recipes_gpu/__init__.py +3 -0
- ellf_recipes_gpu-0.1.1/ellf_recipes_gpu/about.json +13 -0
- ellf_recipes_gpu-0.1.1/ellf_recipes_gpu/about.py +23 -0
- ellf_recipes_gpu-0.1.1/ellf_recipes_gpu/recipes/__init__.py +11 -0
- ellf_recipes_gpu-0.1.1/ellf_recipes_gpu/recipes/apply_curated_transformer.py +118 -0
- ellf_recipes_gpu-0.1.1/ellf_recipes_gpu/recipes/train_curated_transformer.py +374 -0
- ellf_recipes_gpu-0.1.1/ellf_recipes_gpu.egg-info/PKG-INFO +23 -0
- ellf_recipes_gpu-0.1.1/ellf_recipes_gpu.egg-info/SOURCES.txt +19 -0
- ellf_recipes_gpu-0.1.1/ellf_recipes_gpu.egg-info/dependency_links.txt +1 -0
- ellf_recipes_gpu-0.1.1/ellf_recipes_gpu.egg-info/entry_points.txt +2 -0
- ellf_recipes_gpu-0.1.1/ellf_recipes_gpu.egg-info/not-zip-safe +1 -0
- ellf_recipes_gpu-0.1.1/ellf_recipes_gpu.egg-info/requires.txt +7 -0
- ellf_recipes_gpu-0.1.1/ellf_recipes_gpu.egg-info/top_level.txt +1 -0
- ellf_recipes_gpu-0.1.1/pyproject.toml +9 -0
- ellf_recipes_gpu-0.1.1/requirements.in +9 -0
- ellf_recipes_gpu-0.1.1/setup.cfg +4 -0
- ellf_recipes_gpu-0.1.1/setup.py +89 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ellf-recipes-gpu
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: GPU recipes for Ellf (spaCy curated transformers training & inference)
|
|
5
|
+
Home-page: https://prodi.gy
|
|
6
|
+
Author: ExplosionAI GmbH
|
|
7
|
+
Author-email: contact@explosion.ai
|
|
8
|
+
License: All rights reserved
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: ellf-recipes
|
|
11
|
+
Requires-Dist: ellf_recipes_sdk
|
|
12
|
+
Requires-Dist: huggingface-hub>=0.20.0
|
|
13
|
+
Requires-Dist: prodigy-company-plugins
|
|
14
|
+
Requires-Dist: prodigy>=1.16.0
|
|
15
|
+
Requires-Dist: spacy-curated-transformers<1.0.0,>=0.3.0
|
|
16
|
+
Requires-Dist: transformers>=4.30.0
|
|
17
|
+
Dynamic: author
|
|
18
|
+
Dynamic: author-email
|
|
19
|
+
Dynamic: home-page
|
|
20
|
+
Dynamic: license
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
Dynamic: requires-dist
|
|
23
|
+
Dynamic: summary
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ellf-recipes-gpu",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"summary": "GPU recipes for Ellf (spaCy curated transformers training & inference)",
|
|
5
|
+
"uri": "https://prodi.gy",
|
|
6
|
+
"prog": "ptr",
|
|
7
|
+
"package_data": [],
|
|
8
|
+
"entry_points": {
|
|
9
|
+
"ellf_recipes": [
|
|
10
|
+
"ellf_recipes_gpu = ellf_recipes_gpu.recipes"
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This file originates in shared/shared_about.py, and
|
|
3
|
+
is copied verbatim into the different projects. It should
|
|
4
|
+
not be edited directly, edit the shared version and replicate
|
|
5
|
+
it everywhere.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
PWD = Path(__file__).parent
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
with (PWD / "about.json").open("r", encoding="utf8") as file_:
|
|
15
|
+
_about_data = json.load(file_)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__name__ = _about_data["name"]
|
|
19
|
+
__title__ = _about_data.get("title", "")
|
|
20
|
+
__description__ = _about_data.get("description", "")
|
|
21
|
+
__summary__ = _about_data.get("summary", "")
|
|
22
|
+
__version__ = _about_data["version"]
|
|
23
|
+
__prog__ = _about_data.get("prog", "")
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Union
|
|
2
|
+
|
|
3
|
+
import spacy
|
|
4
|
+
from prodigy.errors import RecipeError
|
|
5
|
+
from prodigy.util import msg
|
|
6
|
+
|
|
7
|
+
from ellf_recipes_sdk import (
|
|
8
|
+
Input,
|
|
9
|
+
InputDataset,
|
|
10
|
+
IntProps,
|
|
11
|
+
Model,
|
|
12
|
+
TextProps,
|
|
13
|
+
action_recipe,
|
|
14
|
+
save_examples,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _doc_predictions(doc) -> Dict[str, Any]: # type: ignore[no-untyped-def]
|
|
19
|
+
"""Extract a generic, component-agnostic prediction payload from a Doc.
|
|
20
|
+
|
|
21
|
+
Deliberately simple rather than reproducing every task-specific
|
|
22
|
+
Prodigy annotation format (span boundaries, textcat "accept" shape,
|
|
23
|
+
etc.) for every possible component — this is a batch-inference
|
|
24
|
+
action recipe, not an annotation task recipe, so its output is meant
|
|
25
|
+
to be inspected or consumed downstream rather than re-annotated.
|
|
26
|
+
"""
|
|
27
|
+
predictions: Dict[str, Any] = {}
|
|
28
|
+
if doc.ents:
|
|
29
|
+
predictions["ents"] = [
|
|
30
|
+
{"start": ent.start_char, "end": ent.end_char, "label": ent.label_}
|
|
31
|
+
for ent in doc.ents
|
|
32
|
+
]
|
|
33
|
+
if doc.spans:
|
|
34
|
+
predictions["spans"] = {
|
|
35
|
+
key: [
|
|
36
|
+
{"start": span.start_char, "end": span.end_char, "label": span.label_}
|
|
37
|
+
for span in spans
|
|
38
|
+
]
|
|
39
|
+
for key, spans in doc.spans.items()
|
|
40
|
+
}
|
|
41
|
+
if doc.cats:
|
|
42
|
+
predictions["cats"] = dict(doc.cats)
|
|
43
|
+
return predictions
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@action_recipe(
|
|
47
|
+
title="Apply a spacy-curated-transformers model (GPU)",
|
|
48
|
+
description=(
|
|
49
|
+
"Run inference with a trained spacy-curated-transformers model "
|
|
50
|
+
"over an input dataset or asset, writing predictions to an "
|
|
51
|
+
"output dataset."
|
|
52
|
+
),
|
|
53
|
+
field_props={
|
|
54
|
+
# fmt: off
|
|
55
|
+
"model": TextProps(
|
|
56
|
+
title="Trained model",
|
|
57
|
+
description="Model asset produced by train-curated-transformer",
|
|
58
|
+
),
|
|
59
|
+
"gpu_id": IntProps(
|
|
60
|
+
title="GPU device id", description="Use 0 for the first GPU"
|
|
61
|
+
),
|
|
62
|
+
# fmt: on
|
|
63
|
+
},
|
|
64
|
+
resources={"gpu": 1},
|
|
65
|
+
)
|
|
66
|
+
def apply_curated_transformer(
|
|
67
|
+
*,
|
|
68
|
+
model: Model,
|
|
69
|
+
input: Union[Input, InputDataset],
|
|
70
|
+
output_dataset: str,
|
|
71
|
+
gpu_id: int = 0,
|
|
72
|
+
batch_size: int = 32,
|
|
73
|
+
) -> None:
|
|
74
|
+
"""Run batch inference with a trained spacy-curated-transformers model.
|
|
75
|
+
|
|
76
|
+
Writes ``{"text": ..., "predictions": {...}}`` rows to
|
|
77
|
+
``output_dataset`` — see ``_doc_predictions`` for the prediction
|
|
78
|
+
shape. Predictions are model output, not user annotation, so they're
|
|
79
|
+
saved as ``server_ann_keys`` rather than ``user_ann_keys``.
|
|
80
|
+
|
|
81
|
+
Contract:
|
|
82
|
+
Preconditions:
|
|
83
|
+
- Every example loaded from ``input`` must have a ``"text"``
|
|
84
|
+
key; a missing one raises ``RecipeError``.
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
RecipeError: if an input example has no ``"text"`` key.
|
|
88
|
+
Various spaCy/torch errors (not caught) if the requested GPU
|
|
89
|
+
device is unavailable or the model fails to load.
|
|
90
|
+
"""
|
|
91
|
+
if not spacy.prefer_gpu(gpu_id): # pyright: ignore[reportPrivateImportUsage]
|
|
92
|
+
msg.warn(
|
|
93
|
+
f"Could not activate GPU {gpu_id} — falling back to CPU. "
|
|
94
|
+
"Inference will be slower than expected."
|
|
95
|
+
)
|
|
96
|
+
nlp = model.load()
|
|
97
|
+
|
|
98
|
+
texts: List[str] = []
|
|
99
|
+
for eg in input.load():
|
|
100
|
+
text = eg.get("text")
|
|
101
|
+
if not text:
|
|
102
|
+
raise RecipeError("Every input example must have a non-empty 'text' field.")
|
|
103
|
+
texts.append(text)
|
|
104
|
+
|
|
105
|
+
examples = []
|
|
106
|
+
for text, doc in zip(texts, nlp.pipe(texts, batch_size=batch_size)):
|
|
107
|
+
examples.append({"text": text, "predictions": _doc_predictions(doc)})
|
|
108
|
+
|
|
109
|
+
save_examples(
|
|
110
|
+
output_dataset,
|
|
111
|
+
examples,
|
|
112
|
+
input_keys=["text"],
|
|
113
|
+
server_ann_keys=["predictions"],
|
|
114
|
+
)
|
|
115
|
+
msg.good(f"Wrote {len(examples)} predictions to dataset '{output_dataset}'")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
__all__ = ["apply_curated_transformer"]
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import tempfile
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Dict, List, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
import prodigy.recipes.train as prodigy_train
|
|
7
|
+
import spacy
|
|
8
|
+
from prodigy.errors import RecipeError
|
|
9
|
+
from spacy.cli.init_config import init_config as spacy_init_config
|
|
10
|
+
from spacy.cli.init_config import save_config
|
|
11
|
+
from spacy.util import load_config
|
|
12
|
+
from thinc.api import Config
|
|
13
|
+
|
|
14
|
+
from ellf_recipes.recipes.train import (
|
|
15
|
+
COMPONENTS,
|
|
16
|
+
Data,
|
|
17
|
+
OutputSettings,
|
|
18
|
+
_cleanup_temp_datasets,
|
|
19
|
+
_get_pam_context,
|
|
20
|
+
_load_meta_json,
|
|
21
|
+
_parse_config_overrides,
|
|
22
|
+
_preflight_outputs,
|
|
23
|
+
_save_outputs,
|
|
24
|
+
)
|
|
25
|
+
from ellf_recipes_sdk import (
|
|
26
|
+
BoolProps,
|
|
27
|
+
FloatProps,
|
|
28
|
+
IntProps,
|
|
29
|
+
ListProps,
|
|
30
|
+
TextProps,
|
|
31
|
+
action_recipe,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class HfModelType(Enum):
|
|
36
|
+
"""Hugging Face model families spacy-curated-transformers can load.
|
|
37
|
+
|
|
38
|
+
Deliberately narrower than the 5 architectures curated-transformers
|
|
39
|
+
registers (it also has ``AlbertTransformer.v1``): its HF-tokenizer
|
|
40
|
+
loader (``tokenization/hf_loader.py``) only converts Bert/Roberta/
|
|
41
|
+
XLMRoberta/Camembert fast tokenizers, so a real Albert checkpoint
|
|
42
|
+
would register cleanly but fail at model-init time. Leaving it out
|
|
43
|
+
here avoids shipping a family that looks supported but isn't.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
bert = "bert"
|
|
47
|
+
roberta = "roberta"
|
|
48
|
+
camembert = "camembert"
|
|
49
|
+
xlm_roberta = "xlm-roberta"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# (curated-transformer architecture, piece-encoder architecture) per HF
|
|
53
|
+
# model family — from spacy-curated-transformers' own setup.cfg entry
|
|
54
|
+
# points, cross-checked against tokenization/hf_loader.py's tokenizer
|
|
55
|
+
# dispatch so the pairing is actually loadable, not just registered.
|
|
56
|
+
_ARCHITECTURE_BY_MODEL_TYPE: Dict[HfModelType, Tuple[str, str]] = {
|
|
57
|
+
HfModelType.bert: (
|
|
58
|
+
"spacy-curated-transformers.BertTransformer.v1",
|
|
59
|
+
"spacy-curated-transformers.BertWordpieceEncoder.v1",
|
|
60
|
+
),
|
|
61
|
+
HfModelType.roberta: (
|
|
62
|
+
"spacy-curated-transformers.RobertaTransformer.v1",
|
|
63
|
+
"spacy-curated-transformers.ByteBpeEncoder.v1",
|
|
64
|
+
),
|
|
65
|
+
HfModelType.camembert: (
|
|
66
|
+
"spacy-curated-transformers.CamembertTransformer.v1",
|
|
67
|
+
"spacy-curated-transformers.CamembertSentencepieceEncoder.v1",
|
|
68
|
+
),
|
|
69
|
+
HfModelType.xlm_roberta: (
|
|
70
|
+
"spacy-curated-transformers.XlmrTransformer.v1",
|
|
71
|
+
"spacy-curated-transformers.XlmrSentencepieceEncoder.v1",
|
|
72
|
+
),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
TRANSFORMER_PIPE_NAME = "transformer"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _default_curated_transformer_base_config(
|
|
79
|
+
pipes: Dict[str, Tuple[List[str], List[str]]], lang: str
|
|
80
|
+
) -> Config:
|
|
81
|
+
"""Build a tok2vec-listener-based default config for ``pipes``, always
|
|
82
|
+
using spaCy's "accuracy" preset rather than
|
|
83
|
+
``prodigy.recipes.train.generate_default_config``'s default choice of
|
|
84
|
+
"efficiency" for blank models.
|
|
85
|
+
|
|
86
|
+
Efficiency-mode uses architectures that skip the tok2vec listener
|
|
87
|
+
entirely for some components (e.g. textcat's bag-of-words
|
|
88
|
+
``TextCatBOW.v3``) — there would be nothing for
|
|
89
|
+
``_install_curated_transformer`` to redirect at the transformer.
|
|
90
|
+
Accuracy-mode consistently wires every supported component through a
|
|
91
|
+
``model.tok2vec`` listener (verified against the installed spaCy:
|
|
92
|
+
ner/spancat/tagger/parser/senter/textcat/textcat_multilabel all use
|
|
93
|
+
it under "accuracy"), which is what makes the swap possible. This
|
|
94
|
+
also matches practice: pairing an efficiency-oriented head with an
|
|
95
|
+
already-expensive transformer forward pass isn't a combination
|
|
96
|
+
anyone actually wants.
|
|
97
|
+
"""
|
|
98
|
+
config = spacy_init_config(
|
|
99
|
+
lang=lang, pipeline=list(pipes), optimize="accuracy", gpu=False
|
|
100
|
+
)
|
|
101
|
+
if "ner" in config["components"]:
|
|
102
|
+
config["components"]["ner"]["incorrect_spans_key"] = (
|
|
103
|
+
prodigy_train.NER_DEFAULT_INCORRECT_KEY
|
|
104
|
+
)
|
|
105
|
+
if "spancat" in config["components"]:
|
|
106
|
+
config["components"]["spancat"]["spans_key"] = prodigy_train.SPANCAT_DEFAULT_KEY
|
|
107
|
+
prefix = f"spans_{prodigy_train.SPANCAT_DEFAULT_KEY}"
|
|
108
|
+
config["training"]["score_weights"][f"{prefix}_f"] = 1.0
|
|
109
|
+
config["training"]["score_weights"][f"{prefix}_p"] = 0.0
|
|
110
|
+
config["training"]["score_weights"][f"{prefix}_r"] = 0.0
|
|
111
|
+
train_sets, dev_sets = pipes["spancat"]
|
|
112
|
+
db = prodigy_train.connect()
|
|
113
|
+
examples = prodigy_train.load_examples(db, [*train_sets, *dev_sets])
|
|
114
|
+
suggester = prodigy_train.infer_spancat_suggester(examples, spacy.blank(lang))
|
|
115
|
+
config["components"]["spancat"]["suggester"] = suggester
|
|
116
|
+
return config
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _install_curated_transformer(
|
|
120
|
+
config: Config, *, pipes: List[str], hf_model_type: HfModelType
|
|
121
|
+
) -> None:
|
|
122
|
+
"""Replace ``config``'s tok2vec pipe with a curated-transformer
|
|
123
|
+
component in place, redirecting every downstream component's
|
|
124
|
+
listener at it.
|
|
125
|
+
|
|
126
|
+
Raises ``RecipeError`` for any requested component whose default
|
|
127
|
+
"accuracy" architecture doesn't use a ``model.tok2vec`` listener —
|
|
128
|
+
there would be nothing correct to redirect.
|
|
129
|
+
"""
|
|
130
|
+
transformer_arch, piece_encoder_arch = _ARCHITECTURE_BY_MODEL_TYPE[hf_model_type]
|
|
131
|
+
pipeline = list(config["nlp"]["pipeline"])
|
|
132
|
+
if "tok2vec" in pipeline:
|
|
133
|
+
pipeline.remove("tok2vec")
|
|
134
|
+
config["components"].pop("tok2vec", None)
|
|
135
|
+
if TRANSFORMER_PIPE_NAME not in pipeline:
|
|
136
|
+
pipeline.insert(0, TRANSFORMER_PIPE_NAME)
|
|
137
|
+
config["nlp"]["pipeline"] = pipeline
|
|
138
|
+
config["components"][TRANSFORMER_PIPE_NAME] = {
|
|
139
|
+
"factory": "curated_transformer",
|
|
140
|
+
"model": {
|
|
141
|
+
"@architectures": transformer_arch,
|
|
142
|
+
# Placeholder — overwritten by init_fill_config_curated_transformer
|
|
143
|
+
# (via _fill_curated_transformer_from_hf below) with the real HF
|
|
144
|
+
# model config's vocab_size, hidden_width, num_hidden_layers, etc.
|
|
145
|
+
"vocab_size": 1,
|
|
146
|
+
"piece_encoder": {"@architectures": piece_encoder_arch},
|
|
147
|
+
"with_spans": {
|
|
148
|
+
"@architectures": "spacy-curated-transformers.WithStridedSpans.v1",
|
|
149
|
+
"stride": 96,
|
|
150
|
+
"window": 128,
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
for component in pipes:
|
|
155
|
+
model = config["components"][component]["model"]
|
|
156
|
+
if "tok2vec" not in model:
|
|
157
|
+
raise RecipeError(
|
|
158
|
+
f"Component {component!r} has no tok2vec listener to redirect "
|
|
159
|
+
f"to the curated transformer (its default architecture is "
|
|
160
|
+
f"{model.get('@architectures')!r}). GPU training via "
|
|
161
|
+
f"spacy-curated-transformers currently supports components "
|
|
162
|
+
f"whose default config wires a tok2vec listener."
|
|
163
|
+
)
|
|
164
|
+
model["tok2vec"] = {
|
|
165
|
+
"@architectures": "spacy-curated-transformers.LastTransformerLayerListener.v1",
|
|
166
|
+
# Placeholder — corrected below to match the transformer's real
|
|
167
|
+
# hidden width, once known.
|
|
168
|
+
"width": 768,
|
|
169
|
+
"pooling": {"@layers": "reduce_mean.v1"},
|
|
170
|
+
"upstream": "*",
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _fill_curated_transformer_from_hf(
|
|
175
|
+
config: Config, *, hf_model_name: str, hf_model_revision: str
|
|
176
|
+
) -> Config:
|
|
177
|
+
"""Fill the transformer component's model hyperparameters (vocab
|
|
178
|
+
size, hidden width, layer count, ...) from the real Hugging Face
|
|
179
|
+
model config, and set its encoder/piece-encoder loaders to pull
|
|
180
|
+
weights from ``hf_model_name`` at load time.
|
|
181
|
+
|
|
182
|
+
Delegates to spacy-curated-transformers' own ``init fill-curated-
|
|
183
|
+
transformer`` implementation rather than re-deriving the HF-config
|
|
184
|
+
field mapping here — that mapping is version-specific to the
|
|
185
|
+
installed spacy-curated-transformers and re-implementing it would
|
|
186
|
+
drift out of sync silently. It also validates the HF model's actual
|
|
187
|
+
type against the architecture chosen in ``_install_curated_transformer``
|
|
188
|
+
and fails with a clear message on mismatch (e.g. picking "roberta"
|
|
189
|
+
but naming a BERT checkpoint).
|
|
190
|
+
|
|
191
|
+
Only fills the transformer's own ``model`` block — listener
|
|
192
|
+
``width`` on every downstream component must still be propagated
|
|
193
|
+
from the resolved ``hidden_width`` by the caller, since this step
|
|
194
|
+
doesn't know about them.
|
|
195
|
+
"""
|
|
196
|
+
from spacy_curated_transformers.cli.fill_config_transformer import (
|
|
197
|
+
init_fill_config_curated_transformer,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
with tempfile.NamedTemporaryFile(suffix=".cfg", mode="w", delete=False) as tmp:
|
|
201
|
+
tmp_path = Path(tmp.name)
|
|
202
|
+
try:
|
|
203
|
+
save_config(config, tmp_path, is_stdout=False, silent=True)
|
|
204
|
+
init_fill_config_curated_transformer(
|
|
205
|
+
tmp_path,
|
|
206
|
+
tmp_path,
|
|
207
|
+
cli_model_name=hf_model_name,
|
|
208
|
+
cli_model_revision=hf_model_revision,
|
|
209
|
+
cli_transformer_name=TRANSFORMER_PIPE_NAME,
|
|
210
|
+
config_overrides={},
|
|
211
|
+
)
|
|
212
|
+
filled = load_config(tmp_path)
|
|
213
|
+
finally:
|
|
214
|
+
tmp_path.unlink(missing_ok=True)
|
|
215
|
+
|
|
216
|
+
hidden_width = filled["components"][TRANSFORMER_PIPE_NAME]["model"]["hidden_width"]
|
|
217
|
+
for component in filled["components"].values():
|
|
218
|
+
model = component.get("model", {})
|
|
219
|
+
tok2vec = model.get("tok2vec")
|
|
220
|
+
if (
|
|
221
|
+
isinstance(tok2vec, dict)
|
|
222
|
+
and tok2vec.get("@architectures")
|
|
223
|
+
== "spacy-curated-transformers.LastTransformerLayerListener.v1"
|
|
224
|
+
):
|
|
225
|
+
tok2vec["width"] = hidden_width
|
|
226
|
+
return filled
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@action_recipe(
|
|
230
|
+
title="Train a spaCy pipeline with a curated transformer (GPU)",
|
|
231
|
+
description=(
|
|
232
|
+
"Train a spaCy pipeline using spacy-curated-transformers for GPU-"
|
|
233
|
+
"accelerated training and inference, loading weights from a "
|
|
234
|
+
"Hugging Face checkpoint."
|
|
235
|
+
),
|
|
236
|
+
field_props={
|
|
237
|
+
# fmt: off
|
|
238
|
+
"hf_model_name": TextProps(
|
|
239
|
+
title="Hugging Face model name",
|
|
240
|
+
description="e.g. 'roberta-base', 'bert-base-cased', 'xlm-roberta-base'",
|
|
241
|
+
),
|
|
242
|
+
"hf_model_revision": TextProps(
|
|
243
|
+
title="Hugging Face model revision",
|
|
244
|
+
description="Branch or commit to load. Defaults to 'main'.",
|
|
245
|
+
),
|
|
246
|
+
"lang": TextProps(title="Language code", description="e.g. 'en'"),
|
|
247
|
+
"gpu_id": IntProps(
|
|
248
|
+
title="GPU device id", description="Use 0 for the first GPU"
|
|
249
|
+
),
|
|
250
|
+
"eval_split": FloatProps(
|
|
251
|
+
title="Portion of examples to split off for evaluation",
|
|
252
|
+
description="Applied if no dedicated evaluation datasets are provided for a component.",
|
|
253
|
+
min=0.0,
|
|
254
|
+
max=1.0,
|
|
255
|
+
step=0.05,
|
|
256
|
+
),
|
|
257
|
+
"label_stats": BoolProps(title="Show per-label scores"),
|
|
258
|
+
"verbose": BoolProps(title="Enable verbose logging"),
|
|
259
|
+
"config_overrides": ListProps(
|
|
260
|
+
title="Config overrides",
|
|
261
|
+
description="Override config values for training",
|
|
262
|
+
exists=False,
|
|
263
|
+
min=0,
|
|
264
|
+
),
|
|
265
|
+
# fmt: on
|
|
266
|
+
},
|
|
267
|
+
cli_names={
|
|
268
|
+
**{f"data.{c}.training-input-dataset": f"{c}.train" for c in COMPONENTS},
|
|
269
|
+
**{f"data.{c}.training-input": f"{c}.train-asset" for c in COMPONENTS},
|
|
270
|
+
**{f"data.{c}.evaluation-input-dataset": f"{c}.eval" for c in COMPONENTS},
|
|
271
|
+
**{f"data.{c}.evaluation-input": f"{c}.eval-asset" for c in COMPONENTS},
|
|
272
|
+
},
|
|
273
|
+
resources={"gpu": 1},
|
|
274
|
+
)
|
|
275
|
+
def train_curated_transformer(
|
|
276
|
+
*,
|
|
277
|
+
data: Data,
|
|
278
|
+
output: OutputSettings,
|
|
279
|
+
hf_model_type: HfModelType,
|
|
280
|
+
hf_model_name: str,
|
|
281
|
+
hf_model_revision: str = "main",
|
|
282
|
+
lang: str = "en",
|
|
283
|
+
gpu_id: int = 0,
|
|
284
|
+
eval_split: float = 0.2,
|
|
285
|
+
label_stats: bool = False,
|
|
286
|
+
verbose: bool = False,
|
|
287
|
+
config_overrides: Optional[List[str]] = None,
|
|
288
|
+
) -> None:
|
|
289
|
+
"""Train a spaCy pipeline with a spacy-curated-transformers component
|
|
290
|
+
on GPU, loading weights from a Hugging Face checkpoint.
|
|
291
|
+
|
|
292
|
+
Mirrors the base ``ellf_recipes`` ``train`` recipe's data handling,
|
|
293
|
+
validation, and output persistence, but builds a curated-transformer
|
|
294
|
+
config instead of a tok2vec one and trains with a real ``gpu_id``
|
|
295
|
+
instead of the CPU recipe's hardcoded ``-1``.
|
|
296
|
+
|
|
297
|
+
Contract:
|
|
298
|
+
Preconditions:
|
|
299
|
+
- Same as ``ellf_recipes.recipes.train.train``: ``data`` must
|
|
300
|
+
contain at least one non-None component; ``output.name``
|
|
301
|
+
required unless ``output.skip``; each ``config_overrides``
|
|
302
|
+
entry must contain ``"="``.
|
|
303
|
+
- ``hf_model_type`` must match the actual type of the model at
|
|
304
|
+
``hf_model_name`` (validated against the real HF config
|
|
305
|
+
during the fill-in step; raises a clear error on mismatch).
|
|
306
|
+
- Every requested component's default "accuracy" architecture
|
|
307
|
+
must use a ``model.tok2vec`` listener (raises ``RecipeError``
|
|
308
|
+
otherwise — see ``_install_curated_transformer``).
|
|
309
|
+
|
|
310
|
+
Raises:
|
|
311
|
+
RecipeError: on the precondition failures above, or from
|
|
312
|
+
spacy-curated-transformers' own HF-loading errors
|
|
313
|
+
(unreachable model name/revision, unsupported HF
|
|
314
|
+
tokenizer type).
|
|
315
|
+
"""
|
|
316
|
+
output.validate()
|
|
317
|
+
config_overrides_dict = _parse_config_overrides(config_overrides or [])
|
|
318
|
+
prodigy_train.set_log_level(verbose=verbose)
|
|
319
|
+
pam_client = None
|
|
320
|
+
cluster_id = None
|
|
321
|
+
if not output.skip:
|
|
322
|
+
pam_client, cluster_id = _get_pam_context()
|
|
323
|
+
_preflight_outputs(output, pam_client, cluster_id)
|
|
324
|
+
pipes, temp_datasets = data.materialize()
|
|
325
|
+
try:
|
|
326
|
+
if not pipes:
|
|
327
|
+
raise RecipeError("No components to train")
|
|
328
|
+
base_config = _default_curated_transformer_base_config(pipes, lang)
|
|
329
|
+
_install_curated_transformer(
|
|
330
|
+
base_config, pipes=list(pipes), hf_model_type=hf_model_type
|
|
331
|
+
)
|
|
332
|
+
transformer_config = _fill_curated_transformer_from_hf(
|
|
333
|
+
base_config,
|
|
334
|
+
hf_model_name=hf_model_name,
|
|
335
|
+
hf_model_revision=hf_model_revision,
|
|
336
|
+
)
|
|
337
|
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
338
|
+
tmp_dir = Path(tmp_dir)
|
|
339
|
+
train_config = prodigy_train._prodigy_config(
|
|
340
|
+
pipes,
|
|
341
|
+
None,
|
|
342
|
+
lang=lang,
|
|
343
|
+
eval_split=eval_split,
|
|
344
|
+
config=transformer_config,
|
|
345
|
+
verbose=verbose,
|
|
346
|
+
)
|
|
347
|
+
prodigy_train._train(
|
|
348
|
+
train_config,
|
|
349
|
+
output_dir=tmp_dir,
|
|
350
|
+
overrides=config_overrides_dict,
|
|
351
|
+
gpu_id=gpu_id,
|
|
352
|
+
show_label_stats=label_stats,
|
|
353
|
+
)
|
|
354
|
+
if output.skip:
|
|
355
|
+
prodigy_train.msg.warn(
|
|
356
|
+
"Skipping output registration. Model and results were NOT "
|
|
357
|
+
"saved. Re-run with 'Skip saving outputs' unticked to "
|
|
358
|
+
"persist them."
|
|
359
|
+
)
|
|
360
|
+
else:
|
|
361
|
+
assert pam_client is not None and cluster_id is not None
|
|
362
|
+
model_best = tmp_dir / "model-best"
|
|
363
|
+
_save_outputs(
|
|
364
|
+
output,
|
|
365
|
+
pam_client,
|
|
366
|
+
cluster_id,
|
|
367
|
+
model_dir=model_best,
|
|
368
|
+
results=_load_meta_json(model_best),
|
|
369
|
+
)
|
|
370
|
+
finally:
|
|
371
|
+
_cleanup_temp_datasets(temp_datasets)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
__all__ = ["train_curated_transformer", "HfModelType"]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ellf-recipes-gpu
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: GPU recipes for Ellf (spaCy curated transformers training & inference)
|
|
5
|
+
Home-page: https://prodi.gy
|
|
6
|
+
Author: ExplosionAI GmbH
|
|
7
|
+
Author-email: contact@explosion.ai
|
|
8
|
+
License: All rights reserved
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: ellf-recipes
|
|
11
|
+
Requires-Dist: ellf_recipes_sdk
|
|
12
|
+
Requires-Dist: huggingface-hub>=0.20.0
|
|
13
|
+
Requires-Dist: prodigy-company-plugins
|
|
14
|
+
Requires-Dist: prodigy>=1.16.0
|
|
15
|
+
Requires-Dist: spacy-curated-transformers<1.0.0,>=0.3.0
|
|
16
|
+
Requires-Dist: transformers>=4.30.0
|
|
17
|
+
Dynamic: author
|
|
18
|
+
Dynamic: author-email
|
|
19
|
+
Dynamic: home-page
|
|
20
|
+
Dynamic: license
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
Dynamic: requires-dist
|
|
23
|
+
Dynamic: summary
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
pyproject.toml
|
|
4
|
+
requirements.in
|
|
5
|
+
setup.cfg
|
|
6
|
+
setup.py
|
|
7
|
+
ellf_recipes_gpu/__init__.py
|
|
8
|
+
ellf_recipes_gpu/about.json
|
|
9
|
+
ellf_recipes_gpu/about.py
|
|
10
|
+
ellf_recipes_gpu.egg-info/PKG-INFO
|
|
11
|
+
ellf_recipes_gpu.egg-info/SOURCES.txt
|
|
12
|
+
ellf_recipes_gpu.egg-info/dependency_links.txt
|
|
13
|
+
ellf_recipes_gpu.egg-info/entry_points.txt
|
|
14
|
+
ellf_recipes_gpu.egg-info/not-zip-safe
|
|
15
|
+
ellf_recipes_gpu.egg-info/requires.txt
|
|
16
|
+
ellf_recipes_gpu.egg-info/top_level.txt
|
|
17
|
+
ellf_recipes_gpu/recipes/__init__.py
|
|
18
|
+
ellf_recipes_gpu/recipes/apply_curated_transformer.py
|
|
19
|
+
ellf_recipes_gpu/recipes/train_curated_transformer.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ellf_recipes_gpu
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# This is a generated file! Do not edit directly.
|
|
2
|
+
# Instead run: pdcli repo write-requirements
|
|
3
|
+
ellf-recipes
|
|
4
|
+
ellf_recipes_sdk
|
|
5
|
+
huggingface-hub>=0.20.0
|
|
6
|
+
prodigy-company-plugins
|
|
7
|
+
prodigy>=1.16.0
|
|
8
|
+
spacy-curated-transformers<1.0.0,>=0.3.0
|
|
9
|
+
transformers>=4.30.0
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
This file originates in shared/shared_setup.py, and
|
|
4
|
+
is copied verbatim into the different projects. It should
|
|
5
|
+
not be edited directly, edit the shared version and replicate
|
|
6
|
+
it everywhere.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, List
|
|
12
|
+
|
|
13
|
+
from setuptools import find_packages, setup
|
|
14
|
+
|
|
15
|
+
PWD = Path(__file__).parent
|
|
16
|
+
EXCLUDED_PACKAGE_DATA_PARTS = {".git", "__pycache__", "tests", "backups", "evals"}
|
|
17
|
+
EXCLUDED_PACKAGE_DATA_FILENAMES = {"CLAUDE.md", "README.md"}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def read_about(path: Path) -> Dict[str, Any]:
|
|
21
|
+
with (path / "about.json").open("r", encoding="utf8") as file_:
|
|
22
|
+
data = json.loads(file_.read())
|
|
23
|
+
return data
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def read_requirements(path: Path) -> List[str]:
|
|
27
|
+
# Read in requirements and split into packages and URLs
|
|
28
|
+
requirements_path = path / "requirements.in"
|
|
29
|
+
with requirements_path.open("r", encoding="utf8") as f:
|
|
30
|
+
requirements = [line.strip() for line in f]
|
|
31
|
+
return requirements
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def should_include_package_data(path: Path, package_path: Path) -> bool:
|
|
35
|
+
rel_path = path.relative_to(package_path)
|
|
36
|
+
if not path.is_file():
|
|
37
|
+
return False
|
|
38
|
+
if any(part in EXCLUDED_PACKAGE_DATA_PARTS for part in rel_path.parts):
|
|
39
|
+
return False
|
|
40
|
+
if path.name in EXCLUDED_PACKAGE_DATA_FILENAMES:
|
|
41
|
+
return False
|
|
42
|
+
if path.suffix == ".pyc":
|
|
43
|
+
return False
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def setup_package():
|
|
48
|
+
root = PWD.resolve()
|
|
49
|
+
packages = [
|
|
50
|
+
p for p in find_packages() if not p.startswith("tests") and p != "migrations"
|
|
51
|
+
]
|
|
52
|
+
package_name = packages[0]
|
|
53
|
+
package_path = root / package_name
|
|
54
|
+
about = read_about(root / package_name)
|
|
55
|
+
requirements = read_requirements(root)
|
|
56
|
+
version = about["version"]
|
|
57
|
+
package_data = about.get("package_data", [])
|
|
58
|
+
package_data_dirs = about.get("package_data_dirs", [])
|
|
59
|
+
for directory in package_data_dirs:
|
|
60
|
+
# Package data doesn't properly support recursive globs, so we fix them.
|
|
61
|
+
package_data.extend(
|
|
62
|
+
[
|
|
63
|
+
str(path.relative_to(package_path))
|
|
64
|
+
for path in (package_path / directory).glob("**/*")
|
|
65
|
+
if should_include_package_data(path, package_path)
|
|
66
|
+
]
|
|
67
|
+
)
|
|
68
|
+
# Ensure the about.json is packaged
|
|
69
|
+
if "about.json" not in package_data:
|
|
70
|
+
package_data.append("about.json")
|
|
71
|
+
setup(
|
|
72
|
+
author=about.get("author", "ExplosionAI GmbH"),
|
|
73
|
+
author_email=about.get("email", "contact@explosion.ai"),
|
|
74
|
+
url=about.get("uri", "https://github.com/explosion/ellf"),
|
|
75
|
+
license=about.get("license", "All rights reserved"),
|
|
76
|
+
name=about["name"],
|
|
77
|
+
description=about["summary"],
|
|
78
|
+
version=version,
|
|
79
|
+
packages=packages,
|
|
80
|
+
package_data={package_name: package_data},
|
|
81
|
+
entry_points=about.get("entry_points", {}),
|
|
82
|
+
install_requires=requirements,
|
|
83
|
+
scripts=[],
|
|
84
|
+
zip_safe=False,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
if __name__ == "__main__":
|
|
89
|
+
setup_package()
|