meepsquared 0.1.0__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.
- meepsquared/__init__.py +39 -0
- meepsquared/__main__.py +10 -0
- meepsquared/_allocator.py +91 -0
- meepsquared/_banner.py +51 -0
- meepsquared/_batches.py +364 -0
- meepsquared/_checkpointing.py +605 -0
- meepsquared/_compile.py +165 -0
- meepsquared/_data_checks.py +211 -0
- meepsquared/_fp8.py +289 -0
- meepsquared/_fused.py +243 -0
- meepsquared/_geglu_tanh.py +93 -0
- meepsquared/_grouped.py +444 -0
- meepsquared/_liger.py +61 -0
- meepsquared/_linear_attention.py +66 -0
- meepsquared/_moe.py +228 -0
- meepsquared/_save.py +278 -0
- meepsquared/_selective.py +238 -0
- meepsquared/_sessions.py +453 -0
- meepsquared/_split.py +83 -0
- meepsquared/_trl.py +305 -0
- meepsquared/_unified_memory.py +112 -0
- meepsquared/_varlen.py +241 -0
- meepsquared/_vendor/__init__.py +0 -0
- meepsquared/_vendor/axolotl/LICENSE +202 -0
- meepsquared/_vendor/axolotl/README.md +19 -0
- meepsquared/_vendor/axolotl/__init__.py +0 -0
- meepsquared/_vendor/axolotl/geglu.py +176 -0
- meepsquared/_vendor/axolotl/lora.py +2450 -0
- meepsquared/_vendor/axolotl/op_registry.py +66 -0
- meepsquared/_vendor/axolotl/quantize.py +263 -0
- meepsquared/_vendor/axolotl/swiglu.py +179 -0
- meepsquared/_vendor/axolotl/utils.py +12 -0
- meepsquared/_vllm.py +152 -0
- meepsquared/chat_templates.py +224 -0
- meepsquared/cli.py +93 -0
- meepsquared/job.py +1933 -0
- meepsquared/models.py +510 -0
- meepsquared/multi_gpu.py +35 -0
- meepsquared-0.1.0.dist-info/METADATA +273 -0
- meepsquared-0.1.0.dist-info/RECORD +44 -0
- meepsquared-0.1.0.dist-info/WHEEL +4 -0
- meepsquared-0.1.0.dist-info/entry_points.txt +2 -0
- meepsquared-0.1.0.dist-info/licenses/LICENSE +201 -0
- meepsquared-0.1.0.dist-info/licenses/NOTICE +20 -0
meepsquared/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Copyright Alejandro Martínez Corriá and the Thinkube contributors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Fast LoRA and QLoRA fine-tuning on Transformers, PEFT and TRL.
|
|
5
|
+
|
|
6
|
+
import meepsquared as m2
|
|
7
|
+
job = m2.TrainingJob(model="org/model", data="org/dataset", method="lora", steps=1000)
|
|
8
|
+
print(job.plan())
|
|
9
|
+
job.train().save("out")
|
|
10
|
+
|
|
11
|
+
The calls code coming from Unsloth uses stay (see meepsquared.models):
|
|
12
|
+
|
|
13
|
+
model, tokenizer = m2.MeepLanguageModel.from_pretrained("org/model", load_in_4bit=True)
|
|
14
|
+
model = m2.MeepLanguageModel.get_peft_model(model, r=16)
|
|
15
|
+
|
|
16
|
+
Importing meepsquared makes TRL's SFTTrainer and GRPOTrainer take their settings
|
|
17
|
+
from a meepsquared model, unless their config sets them (see meepsquared._trl), and
|
|
18
|
+
makes GRPO's weight updates reach vLLM under their checkpoint names (see meepsquared._vllm).
|
|
19
|
+
It also sets PyTorch's CUDA allocator to expandable segments, unless the
|
|
20
|
+
allocator is already set (see meepsquared._allocator), and registers the attention
|
|
21
|
+
implementation "meepsquared_varlen" with Transformers (see meepsquared._varlen).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from meepsquared import _allocator
|
|
25
|
+
|
|
26
|
+
_allocator.apply() # before anything below can start CUDA
|
|
27
|
+
|
|
28
|
+
from meepsquared import _trl, _varlen, _vllm # noqa: E402
|
|
29
|
+
from meepsquared.job import Overrides, Plan, Recipe, Result, TrainingJob # noqa: E402
|
|
30
|
+
from meepsquared.models import MeepLanguageModel, MeepModel # noqa: E402
|
|
31
|
+
from meepsquared.multi_gpu import fsdp_config # noqa: E402
|
|
32
|
+
|
|
33
|
+
__version__ = "0.1.0"
|
|
34
|
+
__all__ = ["TrainingJob", "Recipe", "Overrides", "Plan", "Result", "MeepLanguageModel", "MeepModel",
|
|
35
|
+
"fsdp_config", "__version__"]
|
|
36
|
+
|
|
37
|
+
_trl.patch_trainers()
|
|
38
|
+
_varlen.register()
|
|
39
|
+
_vllm.patch_vllm_generation()
|
meepsquared/__main__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Copyright Alejandro Martínez Corriá and the Thinkube contributors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""python -m meepsquared: the meepsquared command (see meepsquared.cli)."""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from meepsquared.cli import main
|
|
9
|
+
|
|
10
|
+
sys.exit(main())
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Copyright Alejandro Martínez Corriá and the Thinkube contributors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""PyTorch's CUDA allocator with expandable segments.
|
|
5
|
+
|
|
6
|
+
PyTorch keeps freed GPU memory in fixed-size segments for reuse. A training
|
|
7
|
+
whose tensors change shape from step to step (padding-free batches of
|
|
8
|
+
changing length, the routed rows of a mixture of experts) leaves those
|
|
9
|
+
segments in pieces that the next request does not fit, so memory that is free
|
|
10
|
+
cannot be used. With expandable segments a segment grows in place instead,
|
|
11
|
+
which keeps the reserved memory close to what is in use; auto checkpointing
|
|
12
|
+
counts reserved but unused memory as free, so it also reads truer.
|
|
13
|
+
|
|
14
|
+
meepsquared turns it on when it is imported, unless something else decided:
|
|
15
|
+
- before CUDA starts, through PyTorch's own setting, PYTORCH_ALLOC_CONF;
|
|
16
|
+
- once CUDA has started (importing TRL or bitsandbytes can start it), through
|
|
17
|
+
torch.cuda.memory._set_allocator_settings, which changes the setting for the
|
|
18
|
+
segments the allocator creates from then on (vLLM switches expandable
|
|
19
|
+
segments with it);
|
|
20
|
+
- not when PYTORCH_ALLOC_CONF or PYTORCH_CUDA_ALLOC_CONF is already set: the
|
|
21
|
+
user's setting stands;
|
|
22
|
+
- not on Windows or under WSL, where PyTorch does not support it, nor on an
|
|
23
|
+
NVIDIA Jetson board, whose driver refuses the virtual-memory calls it is
|
|
24
|
+
built on. The DGX Spark's GB10 supports them.
|
|
25
|
+
The decision and its reason are kept in DECISION and printed in the banner.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import os
|
|
29
|
+
import sys
|
|
30
|
+
|
|
31
|
+
VARIABLES = ("PYTORCH_ALLOC_CONF", "PYTORCH_CUDA_ALLOC_CONF")
|
|
32
|
+
SETTING = "expandable_segments:True"
|
|
33
|
+
JETSON_RELEASE_FILE = "/etc/nv_tegra_release"
|
|
34
|
+
|
|
35
|
+
DECISION = "not decided"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _cuda_started() -> bool:
|
|
39
|
+
torch = sys.modules.get("torch")
|
|
40
|
+
if torch is None:
|
|
41
|
+
return False
|
|
42
|
+
try:
|
|
43
|
+
return bool(torch.cuda.is_initialized())
|
|
44
|
+
except AttributeError:
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _unsupported_platform() -> str:
|
|
49
|
+
"""Why this platform cannot use expandable segments, or "" when it can."""
|
|
50
|
+
if os.name == "nt":
|
|
51
|
+
return "Windows"
|
|
52
|
+
try:
|
|
53
|
+
if "microsoft" in os.uname().release.lower():
|
|
54
|
+
return "WSL"
|
|
55
|
+
except AttributeError:
|
|
56
|
+
pass
|
|
57
|
+
if os.path.exists(JETSON_RELEASE_FILE):
|
|
58
|
+
return "a Jetson board"
|
|
59
|
+
return ""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _set_running_allocator(setting: str) -> None:
|
|
63
|
+
import torch
|
|
64
|
+
|
|
65
|
+
torch.cuda.memory._set_allocator_settings(setting)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def decide(environ=None, cuda_started=None, unsupported=None, set_running=None) -> str:
|
|
69
|
+
"""Turn expandable segments on where that is right; returns what was decided, and why.
|
|
70
|
+
|
|
71
|
+
The keyword arguments are for tests; by default the process's environment,
|
|
72
|
+
CUDA state and platform are read, and a running allocator is changed in place.
|
|
73
|
+
"""
|
|
74
|
+
environ = os.environ if environ is None else environ
|
|
75
|
+
cuda_started = _cuda_started() if cuda_started is None else cuda_started
|
|
76
|
+
unsupported = _unsupported_platform() if unsupported is None else unsupported
|
|
77
|
+
set_running = _set_running_allocator if set_running is None else set_running
|
|
78
|
+
given = [name for name in VARIABLES if name in environ]
|
|
79
|
+
if given:
|
|
80
|
+
return f"as set in {given[0]}"
|
|
81
|
+
if unsupported:
|
|
82
|
+
return f"off (not supported on {unsupported})"
|
|
83
|
+
environ["PYTORCH_ALLOC_CONF"] = SETTING
|
|
84
|
+
if cuda_started:
|
|
85
|
+
set_running(SETTING)
|
|
86
|
+
return "expandable segments"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def apply() -> None:
|
|
90
|
+
global DECISION
|
|
91
|
+
DECISION = decide()
|
meepsquared/_banner.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Copyright Alejandro Martínez Corriá and the Thinkube contributors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""The banner from_pretrained prints once a model is loaded: what meepsquared will
|
|
5
|
+
train it with. MEEPSQUARED_QUIET=1 in the environment turns it off."""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
MARK = ["╭────────────╮ ", "│ meep meep! │ ", "╰──┬─────────╯ ", " ╵ ∘ ○ · · ·"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _gpu() -> str:
|
|
13
|
+
import torch
|
|
14
|
+
|
|
15
|
+
if not torch.cuda.is_available():
|
|
16
|
+
return "none"
|
|
17
|
+
major, minor = torch.cuda.get_device_capability(0)
|
|
18
|
+
return f"{torch.cuda.get_device_name(0)} (sm_{major}{minor})"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _allocator() -> str:
|
|
22
|
+
from meepsquared import _allocator as allocator
|
|
23
|
+
|
|
24
|
+
return allocator.DECISION
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _versions() -> str:
|
|
28
|
+
import torch
|
|
29
|
+
import transformers
|
|
30
|
+
import trl
|
|
31
|
+
|
|
32
|
+
return f"PyTorch {torch.__version__.split('+')[0]}. Transformers {transformers.__version__}. TRL {trl.__version__}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def text(version: str, model_type: str, weights: str, liger: bool, padding_free: bool, padding_free_reason: str) -> str:
|
|
36
|
+
padding = "on" if padding_free else f"off ({padding_free_reason})"
|
|
37
|
+
lines = [
|
|
38
|
+
f"Meep² {version}: fast LoRA and QLoRA on Transformers, PEFT and TRL",
|
|
39
|
+
f"GPU: {_gpu()}, allocator: {_allocator()}. {_versions()}",
|
|
40
|
+
f"Model: {model_type}, {weights}. Liger: {'on' if liger else 'off'}. Padding-free: {padding}",
|
|
41
|
+
"Apache-2.0: https://github.com/thinkube/meepsquared",
|
|
42
|
+
]
|
|
43
|
+
return "\n".join(f"{mark} {line}" for mark, line in zip(MARK, lines))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def show(**kwargs) -> None:
|
|
47
|
+
if os.environ.get("MEEPSQUARED_QUIET") == "1":
|
|
48
|
+
return
|
|
49
|
+
from meepsquared import __version__
|
|
50
|
+
|
|
51
|
+
print(text(__version__, **kwargs), flush=True)
|
meepsquared/_batches.py
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
# Copyright Alejandro Martínez Corriá and the Thinkube contributors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""The order of SFT's training data, laid out ahead as the micro-batches the training runs.
|
|
5
|
+
|
|
6
|
+
meepsquared owns the order of the examples, so that the memory is measured on a
|
|
7
|
+
batch the training runs, and no batch it runs is larger (see
|
|
8
|
+
meepsquared._checkpointing.largest_batch). Each epoch shuffles the examples with
|
|
9
|
+
a generator seeded from the trainer's seed plus the epoch (args.data_seed
|
|
10
|
+
when set, else args.seed, as Transformers' TrainingArguments define them),
|
|
11
|
+
then cuts them into micro-batches in that order, one of two ways:
|
|
12
|
+
|
|
13
|
+
- a number of examples per micro-batch: consecutive examples `micro` at a
|
|
14
|
+
time, the last micro-batch shorter, or dropped with dataloader_drop_last,
|
|
15
|
+
as PyTorch's BatchSampler cuts them;
|
|
16
|
+
- a token budget per micro-batch: consecutive examples grouped until the
|
|
17
|
+
next would take the group over the budget (see groups). A conversation is
|
|
18
|
+
never split, so a group holds at least one, even one longer than the
|
|
19
|
+
budget.
|
|
20
|
+
|
|
21
|
+
The cost of a group is the tokens the GPU computes:
|
|
22
|
+
|
|
23
|
+
- packed (padding-free): the sum of the lengths, as the collator lays the
|
|
24
|
+
conversations end to end in one sequence;
|
|
25
|
+
- padded: the count times the longest, rounded up to the padding multiple,
|
|
26
|
+
as every conversation is padded to the longest one.
|
|
27
|
+
|
|
28
|
+
Under several processes, Accelerate's BatchSamplerShard gives each process
|
|
29
|
+
its share of the micro-batches: one in turn to each process, and with
|
|
30
|
+
even_batches, the end evened out with micro-batches from the start of the
|
|
31
|
+
epoch. meepsquared counts each process's share with that same class.
|
|
32
|
+
|
|
33
|
+
The Trainer counts the micro-batches of an epoch once, from len() before the
|
|
34
|
+
first epoch: ceil(count / accumulation) updates an epoch, `accumulation`
|
|
35
|
+
micro-batches each and the last the rest, until max_steps updates; with
|
|
36
|
+
epochs, max_steps is ceil(epochs x updates an epoch). trained_by() follows
|
|
37
|
+
that count. With a token budget, an epoch's group count can differ from the
|
|
38
|
+
first epoch's by a few: a shorter epoch ends early, a longer one leaves its
|
|
39
|
+
last groups unseen, and trained_by() leaves them out too.
|
|
40
|
+
|
|
41
|
+
install() gives the trainer the layout: its get_train_dataloader is replaced
|
|
42
|
+
by one that passes the layout as the DataLoader's batch sampler. TRL's
|
|
43
|
+
SFTTrainer, on TRL 0.29 and 1.x alike, builds its DataLoader with the
|
|
44
|
+
Trainer's get_train_dataloader.
|
|
45
|
+
|
|
46
|
+
A training resumed from a checkpoint continues at the next micro-batch laid
|
|
47
|
+
out: resume_at() starts the batch sampler in the epoch the training stopped
|
|
48
|
+
in, and the Trainer skips the micro-batches of the steps trained in it, on
|
|
49
|
+
each process its own share.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
import dataclasses
|
|
53
|
+
import functools
|
|
54
|
+
import itertools
|
|
55
|
+
import math
|
|
56
|
+
import types
|
|
57
|
+
from typing import Optional
|
|
58
|
+
|
|
59
|
+
from torch.utils.data import DataLoader, Sampler
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def lengths(dataset, max_length: int) -> list:
|
|
63
|
+
"""The tokens of each example of a tokenized datasets.Dataset, cut to max_length as the collator cuts them."""
|
|
64
|
+
import datasets
|
|
65
|
+
import pyarrow.compute as pc
|
|
66
|
+
|
|
67
|
+
if not isinstance(dataset, datasets.Dataset):
|
|
68
|
+
raise TypeError(f"meepsquared lays out the batches of a datasets.Dataset with tokenized examples, not "
|
|
69
|
+
f"{type(dataset).__name__}")
|
|
70
|
+
if "input_ids" not in dataset.column_names:
|
|
71
|
+
raise ValueError(f"meepsquared lays out the batches from the tokenized examples ('input_ids'); the dataset has "
|
|
72
|
+
f"{dataset.column_names}")
|
|
73
|
+
counted = pc.list_value_length(dataset.with_format("arrow")["input_ids"]).to_pylist()
|
|
74
|
+
return [min(n, max_length) for n in counted]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def groups(lengths: list, order, budget: int, padded: bool, multiple: int = 1) -> list:
|
|
78
|
+
"""Consecutive examples of `order`, grouped while their cost stays within `budget`; one at least per group."""
|
|
79
|
+
out, group, longest, total = [], [], 0, 0
|
|
80
|
+
for index in order:
|
|
81
|
+
length = lengths[index]
|
|
82
|
+
if group:
|
|
83
|
+
if padded:
|
|
84
|
+
cost = (len(group) + 1) * math.ceil(max(longest, length) / multiple) * multiple
|
|
85
|
+
else:
|
|
86
|
+
cost = total + length
|
|
87
|
+
if cost > budget:
|
|
88
|
+
out.append(group)
|
|
89
|
+
group, longest, total = [], 0, 0
|
|
90
|
+
group.append(index)
|
|
91
|
+
longest, total = max(longest, length), total + length
|
|
92
|
+
if group:
|
|
93
|
+
out.append(group)
|
|
94
|
+
return out
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def shuffled(count: int, seed: int, epoch: int) -> list:
|
|
98
|
+
"""The examples 0 to count - 1 in the order of `epoch`: a permutation drawn by a generator seeded seed + epoch."""
|
|
99
|
+
import torch
|
|
100
|
+
|
|
101
|
+
return torch.randperm(count, generator=torch.Generator().manual_seed(seed + epoch)).tolist()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def held_out(count: int, share: float, seed: int) -> tuple:
|
|
105
|
+
"""(trained, held out): the examples 0 to count - 1 split for evaluation, each part in the data's order.
|
|
106
|
+
|
|
107
|
+
The held out are the first round(share x count) of a permutation drawn by
|
|
108
|
+
a generator seeded with `seed`, at least one; at least one is trained.
|
|
109
|
+
"""
|
|
110
|
+
import torch
|
|
111
|
+
|
|
112
|
+
held = max(1, round(share * count))
|
|
113
|
+
if held >= count:
|
|
114
|
+
raise ValueError(f"eval_share {share:g} of {count} examples holds out {held}, and leaves none to train on")
|
|
115
|
+
chosen = set(torch.randperm(count, generator=torch.Generator().manual_seed(seed))[:held].tolist())
|
|
116
|
+
return [i for i in range(count) if i not in chosen], sorted(chosen)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def cut(order: list, micro: int, drop_last: bool) -> list:
|
|
120
|
+
"""Consecutive examples of `order`, `micro` at a time; a shorter last one kept unless drop_last."""
|
|
121
|
+
batches = [order[start:start + micro] for start in range(0, len(order), micro)]
|
|
122
|
+
if drop_last and batches and len(batches[-1]) < micro:
|
|
123
|
+
batches.pop()
|
|
124
|
+
return batches
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def padding(args) -> tuple:
|
|
128
|
+
"""(padded, multiple) from TRL's SFTConfig: whether batches are padded, and to what multiple of tokens (1: none)."""
|
|
129
|
+
padded = not args.padding_free
|
|
130
|
+
return padded, args.pad_to_multiple_of if padded and args.pad_to_multiple_of is not None else 1
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclasses.dataclass(frozen=True)
|
|
134
|
+
class Loading:
|
|
135
|
+
"""What decides the order of a trainer's micro-batches besides the batch: its seed, how it pads and drops, and how
|
|
136
|
+
Accelerate shares the micro-batches among its processes."""
|
|
137
|
+
|
|
138
|
+
seed: int
|
|
139
|
+
drop_last: bool
|
|
140
|
+
padded: bool
|
|
141
|
+
multiple: int
|
|
142
|
+
processes: int
|
|
143
|
+
split_batches: bool
|
|
144
|
+
even_batches: bool
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def loading(trainer) -> Loading:
|
|
148
|
+
"""The Loading of a built SFT trainer."""
|
|
149
|
+
args, accelerator = trainer.args, trainer.accelerator
|
|
150
|
+
if accelerator.dispatch_batches:
|
|
151
|
+
raise ValueError("accelerator_config dispatch_batches=True: the main process would load every batch and "
|
|
152
|
+
"split it among the processes; meepsquared lays out each process's micro-batches, leave it unset")
|
|
153
|
+
if accelerator.num_processes > 1 and not accelerator.even_batches:
|
|
154
|
+
raise ValueError("accelerator_config even_batches=False: the processes would train different numbers of "
|
|
155
|
+
"micro-batches; meepsquared lays out even shares, leave it unset")
|
|
156
|
+
padded, multiple = padding(args)
|
|
157
|
+
# TrainingArguments: data_seed, "if not set, random generators for data sampling will use the same seed as seed"
|
|
158
|
+
seed = args.data_seed if args.data_seed is not None else args.seed
|
|
159
|
+
return Loading(seed, bool(args.dataloader_drop_last), padded, multiple, accelerator.num_processes,
|
|
160
|
+
bool(accelerator.split_batches), bool(accelerator.even_batches))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class _Epoch:
|
|
164
|
+
"""One epoch's micro-batches, as a batch sampler for Accelerate's BatchSamplerShard."""
|
|
165
|
+
|
|
166
|
+
def __init__(self, batches: list, batch_size: Optional[int], drop_last: bool):
|
|
167
|
+
self.batches, self.batch_size, self.drop_last = batches, batch_size, drop_last
|
|
168
|
+
|
|
169
|
+
def __len__(self) -> int:
|
|
170
|
+
return len(self.batches)
|
|
171
|
+
|
|
172
|
+
def __iter__(self):
|
|
173
|
+
return iter(self.batches)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def share(batches: list, batch_size: Optional[int], drop_last: bool, loading: Loading, process: int) -> tuple:
|
|
177
|
+
"""(count, micro-batches): what one process trains of an epoch's micro-batches, as Accelerate's
|
|
178
|
+
BatchSamplerShard gives them; the count is its len(), which the Trainer counts by."""
|
|
179
|
+
epoch = _Epoch(batches, batch_size, drop_last)
|
|
180
|
+
if loading.processes == 1:
|
|
181
|
+
return len(epoch), list(epoch)
|
|
182
|
+
from accelerate.data_loader import BatchSamplerShard
|
|
183
|
+
|
|
184
|
+
shard = BatchSamplerShard(epoch, num_processes=loading.processes, process_index=process,
|
|
185
|
+
split_batches=loading.split_batches, even_batches=loading.even_batches)
|
|
186
|
+
return len(shard), list(shard)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@dataclasses.dataclass(frozen=True)
|
|
190
|
+
class Layout:
|
|
191
|
+
"""The micro-batches of every epoch a training runs, and what it trains of them.
|
|
192
|
+
|
|
193
|
+
epochs: each epoch's micro-batches before Accelerate shares them out, lists
|
|
194
|
+
of example indices. batch_size: the examples of a micro-batch, None with a
|
|
195
|
+
token budget. per_epoch: the micro-batches of an epoch on each process, as
|
|
196
|
+
the Trainer counts them. steps: the updates the training makes. trained:
|
|
197
|
+
the micro-batches the training runs, on every process.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
epochs: tuple
|
|
201
|
+
batch_size: Optional[int]
|
|
202
|
+
loading: Loading
|
|
203
|
+
examples: int
|
|
204
|
+
per_epoch: int
|
|
205
|
+
steps: int
|
|
206
|
+
trained: tuple
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def lay_out(lengths: list, loading: Loading, accumulation: int, steps: Optional[int], epochs: Optional[float],
|
|
210
|
+
micro: Optional[int] = None, budget: Optional[int] = None, kinds: Optional[list] = None) -> Layout:
|
|
211
|
+
"""The micro-batches of a training of `steps` updates, or `epochs` (one when neither), `accumulation`
|
|
212
|
+
micro-batches an update; each micro-batch `micro` examples, or as many as fit `budget` tokens.
|
|
213
|
+
|
|
214
|
+
With `kinds` (each example's), each micro-batch holds examples of one
|
|
215
|
+
kind: the epoch's order is cut kind by kind, and its micro-batches are
|
|
216
|
+
then shuffled by the same generator, so that the kinds mix in the order.
|
|
217
|
+
"""
|
|
218
|
+
if (micro is None) == (budget is None):
|
|
219
|
+
raise ValueError("a layout takes micro (examples per micro-batch) or budget (tokens per micro-batch), "
|
|
220
|
+
"one of them")
|
|
221
|
+
if not lengths:
|
|
222
|
+
raise ValueError("the training data has no examples")
|
|
223
|
+
|
|
224
|
+
def batches_of(order: list) -> list:
|
|
225
|
+
if micro is not None:
|
|
226
|
+
return cut(order, micro, loading.drop_last)
|
|
227
|
+
return groups(lengths, order, budget, loading.padded, loading.multiple)
|
|
228
|
+
|
|
229
|
+
def epoch(index: int) -> list:
|
|
230
|
+
if kinds is None:
|
|
231
|
+
return batches_of(shuffled(len(lengths), loading.seed, index))
|
|
232
|
+
import torch
|
|
233
|
+
|
|
234
|
+
generator = torch.Generator().manual_seed(loading.seed + index)
|
|
235
|
+
order = torch.randperm(len(lengths), generator=generator).tolist()
|
|
236
|
+
made = []
|
|
237
|
+
for kind in sorted(set(kinds)):
|
|
238
|
+
made += batches_of([i for i in order if kinds[i] == kind])
|
|
239
|
+
return [made[i] for i in torch.randperm(len(made), generator=generator).tolist()]
|
|
240
|
+
|
|
241
|
+
drop_last = loading.drop_last if micro is not None else False
|
|
242
|
+
first = epoch(0)
|
|
243
|
+
per_epoch = share(first, micro, drop_last, loading, 0)[0]
|
|
244
|
+
if per_epoch == 0:
|
|
245
|
+
raise ValueError(f"the training data's {len(lengths)} examples make no micro-batch on each of "
|
|
246
|
+
f"{loading.processes} processes")
|
|
247
|
+
updates = max(math.ceil(per_epoch / accumulation), 1)
|
|
248
|
+
max_steps = steps if steps is not None else math.ceil((epochs if epochs is not None else 1) * updates)
|
|
249
|
+
count = math.ceil(max_steps / updates) if steps is not None else math.ceil(epochs if epochs is not None else 1)
|
|
250
|
+
laid = [first] + [epoch(index) for index in range(1, count)]
|
|
251
|
+
trained, made = [], None
|
|
252
|
+
for process in range(loading.processes):
|
|
253
|
+
taken, made = trained_by([share(batches, micro, drop_last, loading, process)[1] for batches in laid],
|
|
254
|
+
per_epoch, accumulation, max_steps)
|
|
255
|
+
trained += taken
|
|
256
|
+
return Layout(tuple(laid), micro, loading, len(lengths), per_epoch, made, tuple(trained))
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def trained_by(shares: list, per_epoch: int, accumulation: int, max_steps: int) -> tuple:
|
|
260
|
+
"""(micro-batches, updates): what one process trains of its share of each epoch, as the Trainer takes them.
|
|
261
|
+
|
|
262
|
+
An epoch makes ceil(per_epoch / accumulation) updates, of `accumulation`
|
|
263
|
+
micro-batches each and the last of the rest; an epoch whose share runs
|
|
264
|
+
out first ends there, and the training ends after max_steps updates.
|
|
265
|
+
"""
|
|
266
|
+
updates = max(math.ceil(per_epoch / accumulation), 1)
|
|
267
|
+
last = per_epoch % accumulation or accumulation
|
|
268
|
+
out, step = [], 0
|
|
269
|
+
for batches in shares:
|
|
270
|
+
taken = iter(batches)
|
|
271
|
+
for update in range(updates):
|
|
272
|
+
pulled = list(itertools.islice(taken, accumulation if update != updates - 1 else last))
|
|
273
|
+
if not pulled:
|
|
274
|
+
break
|
|
275
|
+
out += pulled
|
|
276
|
+
step += 1
|
|
277
|
+
if step >= max_steps:
|
|
278
|
+
return out, step
|
|
279
|
+
return out, step
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class LaidOutBatchSampler(Sampler):
|
|
283
|
+
"""The micro-batches of a Layout, one epoch per pass: the epoch set_epoch names, else the one after the last
|
|
284
|
+
pass, starting at `epoch`.
|
|
285
|
+
|
|
286
|
+
len() is the first epoch's count, which the Trainer counts every epoch by.
|
|
287
|
+
batch_size None (a token budget): Accelerate's BatchSamplerShard then
|
|
288
|
+
takes the micro-batches as of varying size.
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
def __init__(self, layout: Layout, epoch: int = 0):
|
|
292
|
+
self.layout = layout
|
|
293
|
+
self.batch_size = layout.batch_size
|
|
294
|
+
self.drop_last = layout.loading.drop_last if layout.batch_size is not None else False
|
|
295
|
+
self.epoch = epoch
|
|
296
|
+
|
|
297
|
+
def set_epoch(self, epoch: int) -> None:
|
|
298
|
+
self.epoch = epoch
|
|
299
|
+
|
|
300
|
+
def __len__(self) -> int:
|
|
301
|
+
return len(self.layout.epochs[0])
|
|
302
|
+
|
|
303
|
+
def __iter__(self):
|
|
304
|
+
epoch = self.epoch
|
|
305
|
+
if epoch >= len(self.layout.epochs):
|
|
306
|
+
raise RuntimeError(f"the trainer started epoch {epoch + 1}, and meepsquared laid out the micro-batches of "
|
|
307
|
+
f"{len(self.layout.epochs)} epochs for {self.layout.steps} updates; the trainer's "
|
|
308
|
+
"max_steps or num_train_epochs changed after the plan")
|
|
309
|
+
self.epoch = epoch + 1
|
|
310
|
+
return iter(self.layout.epochs[epoch])
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _get_train_dataloader(trainer, layout: Layout) -> DataLoader:
|
|
314
|
+
"""The Trainer's training DataLoader, iterating the micro-batches of `layout`."""
|
|
315
|
+
from transformers.trainer_utils import seed_worker
|
|
316
|
+
|
|
317
|
+
from meepsquared import _data_checks
|
|
318
|
+
|
|
319
|
+
args = trainer.args
|
|
320
|
+
dataset = trainer._remove_unused_columns(trainer.train_dataset, description="Training")
|
|
321
|
+
if len(dataset) != layout.examples:
|
|
322
|
+
raise ValueError(f"the trainer's dataset has {len(dataset)} examples, and the micro-batches were laid out "
|
|
323
|
+
f"for {layout.examples}")
|
|
324
|
+
dataset, collator = _data_checks.weighted_loader(trainer, dataset)
|
|
325
|
+
loader = DataLoader(dataset, batch_sampler=LaidOutBatchSampler(layout, trainer._meepsquared_first_epoch),
|
|
326
|
+
collate_fn=collator,
|
|
327
|
+
num_workers=args.dataloader_num_workers, pin_memory=args.dataloader_pin_memory,
|
|
328
|
+
persistent_workers=args.dataloader_persistent_workers,
|
|
329
|
+
prefetch_factor=args.dataloader_prefetch_factor,
|
|
330
|
+
worker_init_fn=functools.partial(seed_worker, num_workers=args.dataloader_num_workers,
|
|
331
|
+
rank=args.process_index))
|
|
332
|
+
return trainer.accelerator.prepare(loader)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def install(trainer, layout: Layout) -> None:
|
|
336
|
+
"""Make the trainer train the micro-batches of `layout`, kept as its _meepsquared_layout; refused when the trainer
|
|
337
|
+
would load them otherwise."""
|
|
338
|
+
if loading(trainer) != layout.loading:
|
|
339
|
+
raise ValueError(f"the micro-batches were laid out for {layout.loading}, and the trainer loads them as "
|
|
340
|
+
f"{loading(trainer)}")
|
|
341
|
+
trainer.get_train_dataloader = types.MethodType(functools.partial(_get_train_dataloader, layout=layout), trainer)
|
|
342
|
+
trainer._meepsquared_layout = layout
|
|
343
|
+
trainer._meepsquared_first_epoch = 0
|
|
344
|
+
trainer._meepsquared_weights = None
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def resume_at(trainer, step: int) -> int:
|
|
348
|
+
"""Make the trainer's micro-batches start in the epoch a training resumed after `step` updates continues;
|
|
349
|
+
returns that epoch.
|
|
350
|
+
|
|
351
|
+
The Trainer counts the epochs trained as step // ceil(micro-batches an
|
|
352
|
+
epoch / accumulation), and skips the micro-batches of the steps trained
|
|
353
|
+
in that epoch. The batch sampler starts in that epoch itself, as
|
|
354
|
+
Accelerate's set_epoch does not reach it through the skipping sampler
|
|
355
|
+
under several processes.
|
|
356
|
+
"""
|
|
357
|
+
layout = trainer._meepsquared_layout
|
|
358
|
+
updates = max(math.ceil(layout.per_epoch / trainer.args.gradient_accumulation_steps), 1)
|
|
359
|
+
epoch = step // updates
|
|
360
|
+
if epoch >= len(layout.epochs):
|
|
361
|
+
raise ValueError(f"resume after {step} updates: epoch {epoch + 1}, and meepsquared laid out the micro-batches of "
|
|
362
|
+
f"{len(layout.epochs)} epochs for {layout.steps} updates")
|
|
363
|
+
trainer._meepsquared_first_epoch = epoch
|
|
364
|
+
return epoch
|