tabpackage 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.
- tabpackage/__init__.py +18 -0
- tabpackage/_training.py +3380 -0
- tabpackage/ensemble.py +263 -0
- tabpackage/metrics.py +253 -0
- tabpackage/nn.py +2110 -0
- tabpackage/optim.py +943 -0
- tabpackage/sklearn/__init__.py +18 -0
- tabpackage/sklearn/_config.py +561 -0
- tabpackage/sklearn/_data.py +307 -0
- tabpackage/sklearn/_estimators.py +1210 -0
- tabpackage/utils/__init__.py +1 -0
- tabpackage/utils/torch.py +166 -0
- tabpackage-0.0.1.dist-info/METADATA +398 -0
- tabpackage-0.0.1.dist-info/RECORD +15 -0
- tabpackage-0.0.1.dist-info/WHEEL +4 -0
tabpackage/nn.py
ADDED
|
@@ -0,0 +1,2110 @@
|
|
|
1
|
+
"""Neural network modules, including the "Module Pack" pattern.
|
|
2
|
+
|
|
3
|
+
A module pack is a set of same-family modules with potentially different
|
|
4
|
+
hyperparameters that are stored and applied as one module, which makes it possible to
|
|
5
|
+
train many models in parallel efficiently. The base class for module packs is
|
|
6
|
+
`ModulePack`. The pack dimension of all tensors of a module pack is `PACK_DIM`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import abc
|
|
10
|
+
import contextlib
|
|
11
|
+
import enum
|
|
12
|
+
import inspect
|
|
13
|
+
import itertools
|
|
14
|
+
import math
|
|
15
|
+
import typing
|
|
16
|
+
from collections import OrderedDict
|
|
17
|
+
from typing import Any, Final, NamedTuple, TypeAlias, TypeVar
|
|
18
|
+
|
|
19
|
+
import torch
|
|
20
|
+
import torch.nn as nn # noqa: PLR0402
|
|
21
|
+
import torch.optim.optimizer
|
|
22
|
+
from torch import Tensor
|
|
23
|
+
|
|
24
|
+
_TORCH_IS_OLDER_THAN_2_15 = torch.torch_version.TorchVersion(torch.__version__) < '2.15'
|
|
25
|
+
|
|
26
|
+
# In many places, the implementation _implicitly_ assumes the following values
|
|
27
|
+
# and is _not_ ready for other values. Thus, these constants exist only for additional
|
|
28
|
+
# clarity where possible, and must never be changed.
|
|
29
|
+
PACK_DIM: Final = 0
|
|
30
|
+
BATCH_DIM: Final = 1
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
KWArgs: TypeAlias = dict[str, Any]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# >>> Generic modules
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PackView(nn.Module):
|
|
40
|
+
"""Turn a tensor to a valid input for a module pack.
|
|
41
|
+
|
|
42
|
+
This is a helper module for module packs that is not itself a module pack, but
|
|
43
|
+
follows some of the conventions described in `ModulePack`.
|
|
44
|
+
|
|
45
|
+
.. note::
|
|
46
|
+
The module uses `torch.Tensor.expand` under the hood and thus does not create
|
|
47
|
+
any copies.
|
|
48
|
+
|
|
49
|
+
**Shape**
|
|
50
|
+
|
|
51
|
+
- Input: `(batch_size, d)` or `(pack_size, batch_size, d)`
|
|
52
|
+
- Output: `(pack_size, batch_size, d)`
|
|
53
|
+
|
|
54
|
+
**Examples**
|
|
55
|
+
|
|
56
|
+
Two-dimensional tensors are expanded to three-dimensional tensors holding `k`
|
|
57
|
+
identical views of the original input:
|
|
58
|
+
|
|
59
|
+
>>> m = PackView(pack_size=2)
|
|
60
|
+
>>> x = torch.randn(3, 4)
|
|
61
|
+
>>> m(x).shape
|
|
62
|
+
torch.Size([2, 3, 4])
|
|
63
|
+
>>> torch.all(m(x) == x.unsqueeze(PACK_DIM)).item()
|
|
64
|
+
True
|
|
65
|
+
|
|
66
|
+
Three-dimensional tensors are accepted only in training mode and returned as-is:
|
|
67
|
+
|
|
68
|
+
>>> m = PackView(pack_size=2)
|
|
69
|
+
>>> x = torch.randn(2, 3, 4)
|
|
70
|
+
>>> m(x) is x
|
|
71
|
+
True
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, *, pack_size: int) -> None:
|
|
75
|
+
super().__init__()
|
|
76
|
+
self._pack_size = pack_size
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def pack_size(self) -> int:
|
|
80
|
+
"""The pack size."""
|
|
81
|
+
return self._pack_size
|
|
82
|
+
|
|
83
|
+
def forward(self, x: Tensor, pack_idx: Tensor | None = None) -> Tensor:
|
|
84
|
+
if pack_idx is not None:
|
|
85
|
+
check_pack_idx(pack_idx, self.pack_size)
|
|
86
|
+
|
|
87
|
+
pack_size = self.pack_size if pack_idx is None else len(pack_idx)
|
|
88
|
+
if x.ndim == 2:
|
|
89
|
+
x = x.unsqueeze(PACK_DIM).expand(pack_size, -1, -1)
|
|
90
|
+
else:
|
|
91
|
+
if x.ndim != 3:
|
|
92
|
+
raise ValueError('The input must have two or three dimensions')
|
|
93
|
+
if not self.training:
|
|
94
|
+
raise RuntimeError('Three-dimensional input requires training mode')
|
|
95
|
+
if x.shape[PACK_DIM] != pack_size:
|
|
96
|
+
raise ValueError(
|
|
97
|
+
'The pack dimension of the input must match the selected pack size'
|
|
98
|
+
)
|
|
99
|
+
return x
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# >>> Tensor packs
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _check_idx(idx: Tensor) -> None:
|
|
106
|
+
if not (idx.dtype == torch.int32 or idx.dtype == torch.int64):
|
|
107
|
+
raise TypeError(
|
|
108
|
+
'Indices must have the data type `torch.int32` or `torch.int64`'
|
|
109
|
+
)
|
|
110
|
+
if idx.ndim != 1:
|
|
111
|
+
raise ValueError('Indices must be one-dimensional')
|
|
112
|
+
if len(idx) == 0:
|
|
113
|
+
raise ValueError('Indices must not be empty')
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def check_pack_idx(pack_idx: Tensor, pack_size: int | None) -> None:
|
|
117
|
+
"""Check basic requirements for indexing pack members."""
|
|
118
|
+
# Only cheap checks are performed. In particular, the values of `pack_idx`
|
|
119
|
+
# are not checked, because that would require a device-to-host synchronization.
|
|
120
|
+
_check_idx(pack_idx)
|
|
121
|
+
if pack_size is not None and len(pack_idx) > pack_size:
|
|
122
|
+
raise ValueError('The number of pack indices must not exceed the pack size')
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def make_complementary_pack_idx(pack_idx: Tensor, *, pack_size: int) -> Tensor:
|
|
126
|
+
"""Make the indices of the pack members that are not in `pack_idx`."""
|
|
127
|
+
check_pack_idx(pack_idx, pack_size)
|
|
128
|
+
|
|
129
|
+
mask = torch.ones(pack_size, dtype=torch.bool, device=pack_idx.device)
|
|
130
|
+
mask[pack_idx] = False
|
|
131
|
+
# The function could have a keyword argument like this:
|
|
132
|
+
# `assume_unique_nonnegative_pack_idx: bool = False`; so that when it is `True`,
|
|
133
|
+
# then `torch.nonzero_static` is used to avoid the device-to-host synchronization.
|
|
134
|
+
# However, to keep it simple and less error-prone for users, the argument is not
|
|
135
|
+
# added for now.
|
|
136
|
+
return torch.nonzero(mask)[:, 0]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@typing.overload
|
|
140
|
+
def _get_tensor_pack(x: None, pack_idx: Tensor | None) -> None: ...
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@typing.overload
|
|
144
|
+
def _get_tensor_pack(x: Tensor, pack_idx: Tensor | None) -> Tensor: ...
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _get_tensor_pack(x, pack_idx):
|
|
148
|
+
if x is None or pack_idx is None:
|
|
149
|
+
return x
|
|
150
|
+
|
|
151
|
+
_check_idx(pack_idx)
|
|
152
|
+
# `index_select` seems to be a little bit faster for the expected workloads than the
|
|
153
|
+
# square-brackets-based indexing. However, it requires nonnegative indices.
|
|
154
|
+
return x.index_select(
|
|
155
|
+
PACK_DIM, torch.where(pack_idx < 0, pack_idx + x.shape[PACK_DIM], pack_idx)
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class ParameterPack(nn.Parameter):
|
|
160
|
+
"""Parameter pack.
|
|
161
|
+
|
|
162
|
+
Module packs _must_ use this class to store their trainable parameters.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class _BufferPackMeta(nn.parameter._BufferMeta): # ty: ignore[unresolved-attribute]
|
|
167
|
+
"""
|
|
168
|
+
`BufferPack` relies on this metaclass for `isinstance(..., BufferPack)` to work
|
|
169
|
+
as one would expect. Otherwise, because of `torch.nn.Buffer` implementation details,
|
|
170
|
+
the following (unexpected) behavior would take place:
|
|
171
|
+
|
|
172
|
+
>>> class NaiveBufferPack(nn.Buffer):
|
|
173
|
+
... pass
|
|
174
|
+
...
|
|
175
|
+
>>> naive_buffer_pack = NaiveBufferPack(torch.zeros(1))
|
|
176
|
+
>>> isinstance(naive_buffer_pack, NaiveBufferPack)
|
|
177
|
+
False
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
def __instancecheck__(self, instance):
|
|
181
|
+
return super().__instancecheck__(instance) or (
|
|
182
|
+
self is BufferPack
|
|
183
|
+
and isinstance(instance, torch.Tensor)
|
|
184
|
+
and getattr(instance, '_is_buffer_pack', False)
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@typing.final
|
|
189
|
+
class BufferPack(nn.Buffer, metaclass=_BufferPackMeta):
|
|
190
|
+
"""Buffer pack.
|
|
191
|
+
|
|
192
|
+
Module packs _must_ use this class to store their non-trainable tensors.
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
def __new__(cls, *args, **kwargs):
|
|
196
|
+
t = nn.Buffer.__new__(cls, *args, **kwargs)
|
|
197
|
+
t._is_buffer_pack = True # ty: ignore[unresolved-attribute]
|
|
198
|
+
return t
|
|
199
|
+
|
|
200
|
+
def __init_subclass__(cls, *args, **kwargs):
|
|
201
|
+
raise TypeError('Subclassing is not allowed')
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
_TensorPackT = TypeVar('_TensorPackT', bound='ParameterPack | BufferPack')
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _make_tensor_pack_like(x: _TensorPackT, *, data: Tensor) -> _TensorPackT:
|
|
208
|
+
# `type` works as expected for `ParameterPack` and inherited classes, but not for
|
|
209
|
+
# `BufferPack`. This is why inheriting from `BufferPack` is not allowed.
|
|
210
|
+
cls = type(x) if isinstance(x, ParameterPack) else BufferPack
|
|
211
|
+
return cls(data).requires_grad_(x.requires_grad) # ty: ignore[invalid-return-type]
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _load_tensor_packs_from_state_dict_at(
|
|
215
|
+
module: nn.Module,
|
|
216
|
+
state_dict: dict[str, Tensor],
|
|
217
|
+
prefix: str,
|
|
218
|
+
*,
|
|
219
|
+
pack_idx: Tensor,
|
|
220
|
+
state_dict_idx: Tensor | None,
|
|
221
|
+
) -> None:
|
|
222
|
+
with torch.no_grad():
|
|
223
|
+
for name, x in itertools.chain(
|
|
224
|
+
module.named_parameters(recurse=False, remove_duplicate=False),
|
|
225
|
+
(
|
|
226
|
+
(name, value)
|
|
227
|
+
for name, value in module.named_buffers(
|
|
228
|
+
recurse=False, remove_duplicate=False
|
|
229
|
+
)
|
|
230
|
+
if name not in module._non_persistent_buffers_set
|
|
231
|
+
),
|
|
232
|
+
):
|
|
233
|
+
if not isinstance(x, ParameterPack | BufferPack):
|
|
234
|
+
raise TypeError(
|
|
235
|
+
'The module contains parameters or buffers that do not support pack'
|
|
236
|
+
' operations'
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
key = prefix + name
|
|
240
|
+
value = state_dict[key]
|
|
241
|
+
|
|
242
|
+
if not isinstance(value, Tensor):
|
|
243
|
+
raise TypeError('State dictionary values must be tensors')
|
|
244
|
+
if value.ndim != x.ndim:
|
|
245
|
+
raise ValueError(
|
|
246
|
+
'The source and destination tensors have incompatible shapes'
|
|
247
|
+
)
|
|
248
|
+
if value.shape[1:] != x.shape[1:]:
|
|
249
|
+
raise ValueError(
|
|
250
|
+
'The source and destination tensors have incompatible shapes'
|
|
251
|
+
)
|
|
252
|
+
if state_dict_idx is None and value.shape[PACK_DIM] != len(pack_idx):
|
|
253
|
+
raise ValueError(
|
|
254
|
+
'The state dictionary contains a different number of pack members'
|
|
255
|
+
' than the destination selection'
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
x[pack_idx] = value if state_dict_idx is None else value[state_dict_idx]
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# >>> Module packs
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class _ModulePackBuffers(OrderedDict):
|
|
265
|
+
"""
|
|
266
|
+
This class ensures that reassigning a module pack's field that is a buffer pack
|
|
267
|
+
will result in a field that is also a buffer pack.
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
def __setitem__(self, key, value) -> None:
|
|
271
|
+
current_value = self.get(key)
|
|
272
|
+
if (
|
|
273
|
+
current_value is not None
|
|
274
|
+
# The following checks are the reason why `_BufferPackMeta` is needed.
|
|
275
|
+
and isinstance(current_value, BufferPack)
|
|
276
|
+
and not isinstance(value, BufferPack)
|
|
277
|
+
):
|
|
278
|
+
value = BufferPack(value)
|
|
279
|
+
return super().__setitem__(key, value)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class ModulePack(nn.Module, abc.ABC):
|
|
283
|
+
"""The base class for module packs.
|
|
284
|
+
|
|
285
|
+
A module pack is a set of modules of the same family that may differ in their
|
|
286
|
+
hyperparameters.
|
|
287
|
+
|
|
288
|
+
**Examples**
|
|
289
|
+
|
|
290
|
+
- `DropoutPack`: a pack of dropout layers with different dropout ratios.
|
|
291
|
+
- `LinearPack`: a pack of linear layers with different input and output shapes.
|
|
292
|
+
|
|
293
|
+
**Conventions**
|
|
294
|
+
|
|
295
|
+
- Module packs _must_ use `ParameterPack` and `BufferPack` classes to store
|
|
296
|
+
parameters and buffers rather than `torch.nn.Parameter` and `torch.nn.Buffer`.
|
|
297
|
+
Otherwise, the behavior will be incorrect.
|
|
298
|
+
- Module pack constructors _must_ take a required `pack_size: int` argument denoting
|
|
299
|
+
the number of modules in the pack.
|
|
300
|
+
- Module pack constructors _must_ take per-pack-member hyperparameters as lists of
|
|
301
|
+
per-pack-member hyperparameter values.
|
|
302
|
+
- Module packs _may_ implement the `replace` method for replacing some of the
|
|
303
|
+
current members with new ones, with potentially different hyperparameters and
|
|
304
|
+
reinitialized parameters. When doing this during training, also call the optimizer
|
|
305
|
+
pack's `replace` method for the same members to reset their optimizer state.
|
|
306
|
+
- Module pack forward passes _may_ take an additional `pack_idx: Tensor | None`
|
|
307
|
+
argument to perform a forward pass with a selected set of pack members. Inputs
|
|
308
|
+
with a pack dimension must already follow the order and repetitions in `pack_idx`;
|
|
309
|
+
the forward pass selects module state, not input rows. Note that selecting pack
|
|
310
|
+
members with `pack_idx` copies their selected entries. Therefore, this mechanism
|
|
311
|
+
should be used only when this overhead is acceptable, or when there are no
|
|
312
|
+
alternatives (e.g., for performing a training step for a subset of pack members).
|
|
313
|
+
As an example of the second use case, `MLPBackbonePack` currently relies on this
|
|
314
|
+
mechanism for depth-varying MLPs. Examples of use cases where `pack_idx` should
|
|
315
|
+
_not_ be used: (1) permanently disabling some of the pack members; instead, use
|
|
316
|
+
`ModulePack.remove`; (2) evaluating some of the pack members; instead, use
|
|
317
|
+
`ModulePack.temporarily_select`.
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
def __init__(self, *args, pack_size: int, **kwargs) -> None:
|
|
321
|
+
del pack_size
|
|
322
|
+
super().__init__(*args, **kwargs)
|
|
323
|
+
self._buffers = _ModulePackBuffers()
|
|
324
|
+
|
|
325
|
+
@property
|
|
326
|
+
@abc.abstractmethod
|
|
327
|
+
def pack_size(self) -> int:
|
|
328
|
+
"""The pack size."""
|
|
329
|
+
|
|
330
|
+
def try_get_pack_size(self) -> int | None:
|
|
331
|
+
"""Get the pack size, or `None` if it cannot be inferred."""
|
|
332
|
+
try:
|
|
333
|
+
return self.pack_size
|
|
334
|
+
except RuntimeError:
|
|
335
|
+
return None
|
|
336
|
+
|
|
337
|
+
def load_state_dict_at(
|
|
338
|
+
self,
|
|
339
|
+
state_dict: dict[str, Tensor],
|
|
340
|
+
*,
|
|
341
|
+
pack_idx: Tensor,
|
|
342
|
+
state_dict_idx: Tensor | None = None,
|
|
343
|
+
) -> None:
|
|
344
|
+
"""Load state dictionary for specific pack members.
|
|
345
|
+
|
|
346
|
+
Source rows are matched positionally to destination rows selected by `pack_idx`.
|
|
347
|
+
If `state_dict_idx` is omitted, source tensors must contain exactly
|
|
348
|
+
`len(pack_idx)` members; otherwise, `state_dict_idx` selects the source rows.
|
|
349
|
+
|
|
350
|
+
Warning:
|
|
351
|
+
This method does _not_ call the hooks registered for `load_state_dict`.
|
|
352
|
+
"""
|
|
353
|
+
_check_module_pack(self)
|
|
354
|
+
check_pack_idx(pack_idx, self.pack_size)
|
|
355
|
+
if state_dict_idx is not None:
|
|
356
|
+
_check_idx(state_dict_idx)
|
|
357
|
+
if pack_idx.shape != state_dict_idx.shape:
|
|
358
|
+
raise ValueError(
|
|
359
|
+
'The source and destination selections contain different numbers of'
|
|
360
|
+
' pack members'
|
|
361
|
+
)
|
|
362
|
+
if self.state_dict().keys() != state_dict.keys():
|
|
363
|
+
raise ValueError(
|
|
364
|
+
'The state dictionary has missing or unexpected keys for this module'
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
def load(
|
|
368
|
+
module: nn.Module, local_state_dict: dict[str, Tensor], prefix: str
|
|
369
|
+
) -> None:
|
|
370
|
+
(
|
|
371
|
+
type(module)._load_from_state_dict_at
|
|
372
|
+
if isinstance(module, ModulePack)
|
|
373
|
+
else _load_tensor_packs_from_state_dict_at
|
|
374
|
+
)(
|
|
375
|
+
module, # ty: ignore[invalid-argument-type]
|
|
376
|
+
local_state_dict,
|
|
377
|
+
prefix,
|
|
378
|
+
pack_idx=pack_idx,
|
|
379
|
+
state_dict_idx=state_dict_idx,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
# `.named_children()` is not used, because it removes duplicates.
|
|
383
|
+
for name, child in module._modules.items():
|
|
384
|
+
if child is not None:
|
|
385
|
+
child_prefix = prefix + name + '.'
|
|
386
|
+
child_state_dict = {
|
|
387
|
+
k: v
|
|
388
|
+
for k, v in local_state_dict.items()
|
|
389
|
+
if k.startswith(child_prefix)
|
|
390
|
+
}
|
|
391
|
+
load(child, child_state_dict, child_prefix)
|
|
392
|
+
|
|
393
|
+
load(self, state_dict, '')
|
|
394
|
+
|
|
395
|
+
def _load_from_state_dict_at(
|
|
396
|
+
self,
|
|
397
|
+
state_dict: dict[str, Tensor],
|
|
398
|
+
prefix: str,
|
|
399
|
+
*,
|
|
400
|
+
pack_idx: Tensor,
|
|
401
|
+
state_dict_idx: Tensor | None,
|
|
402
|
+
) -> None:
|
|
403
|
+
_load_tensor_packs_from_state_dict_at(
|
|
404
|
+
self,
|
|
405
|
+
state_dict,
|
|
406
|
+
prefix,
|
|
407
|
+
pack_idx=pack_idx,
|
|
408
|
+
state_dict_idx=state_dict_idx,
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
def remove(self, *, pack_idx: Tensor) -> dict[ParameterPack, ParameterPack]:
|
|
412
|
+
"""Remove some members from the module pack.
|
|
413
|
+
|
|
414
|
+
The method replaces the current parameter packs and buffer packs with smaller
|
|
415
|
+
ones. To continue training, pass the returned parameter mapping and the same
|
|
416
|
+
`pack_idx` to `tabpackage.optim.optimizer_pack_remove` to update the optimizer.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
pack_idx: the pack member indices to remove.
|
|
420
|
+
Returns:
|
|
421
|
+
The mapping from the replaced parameter packs to the new ones.
|
|
422
|
+
"""
|
|
423
|
+
_check_module_pack(self)
|
|
424
|
+
check_pack_idx(pack_idx, self.pack_size)
|
|
425
|
+
# Duplicate indices would make the `PackView` size updates incorrect, so the
|
|
426
|
+
# cost of device-to-host synchronization is paid here to avoid the issue.
|
|
427
|
+
if len(pack_idx) != len(torch.unique(pack_idx)):
|
|
428
|
+
raise ValueError('Pack indices to remove must be unique')
|
|
429
|
+
|
|
430
|
+
keep_pack_idx = None
|
|
431
|
+
old_to_new_parameters = {}
|
|
432
|
+
for name, x in itertools.chain(self.named_parameters(), self.named_buffers()):
|
|
433
|
+
if isinstance(x, ParameterPack | BufferPack):
|
|
434
|
+
if keep_pack_idx is None:
|
|
435
|
+
keep_pack_idx = make_complementary_pack_idx(
|
|
436
|
+
pack_idx, pack_size=x.shape[PACK_DIM]
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
if '.' in name:
|
|
440
|
+
submodule_name, attr = name.rsplit('.', 1)
|
|
441
|
+
submodule = self.get_submodule(submodule_name)
|
|
442
|
+
else:
|
|
443
|
+
submodule = self
|
|
444
|
+
attr = name
|
|
445
|
+
|
|
446
|
+
new_x = _make_tensor_pack_like(x, data=x.data[keep_pack_idx])
|
|
447
|
+
if isinstance(x, BufferPack):
|
|
448
|
+
submodule.register_buffer(
|
|
449
|
+
attr,
|
|
450
|
+
new_x,
|
|
451
|
+
persistent=attr not in submodule._non_persistent_buffers_set,
|
|
452
|
+
)
|
|
453
|
+
else:
|
|
454
|
+
setattr(submodule, attr, new_x)
|
|
455
|
+
|
|
456
|
+
if isinstance(x, ParameterPack):
|
|
457
|
+
assert isinstance(new_x, ParameterPack)
|
|
458
|
+
old_to_new_parameters[x] = new_x
|
|
459
|
+
|
|
460
|
+
for submodule in self.modules():
|
|
461
|
+
if isinstance(submodule, PackView):
|
|
462
|
+
submodule._pack_size -= len(pack_idx)
|
|
463
|
+
|
|
464
|
+
return old_to_new_parameters
|
|
465
|
+
|
|
466
|
+
# A potential alternative to the following method could be `ModulePack.index`
|
|
467
|
+
# that would produce a copy of the original module with selected pack members.
|
|
468
|
+
# Contrary to that hypothetical method, this method performs the minimal possible
|
|
469
|
+
# number of copy operations (one per tensor pack).
|
|
470
|
+
@contextlib.contextmanager
|
|
471
|
+
def temporarily_select(self, *, pack_idx: Tensor):
|
|
472
|
+
"""Temporarily use specified pack members in evaluation mode.
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
pack_idx: the indices of the pack members to use.
|
|
476
|
+
|
|
477
|
+
The selection is performed literally based on the provided `pack_idx`. That is,
|
|
478
|
+
the order of the new pack members will follow that of `pack_idx`, and repeating
|
|
479
|
+
values in `pack_idx` means repeating pack members, which in particular can
|
|
480
|
+
result in a module pack of a larger size than the original.
|
|
481
|
+
|
|
482
|
+
Technically, the method temporarily replaces all parameter packs and buffer
|
|
483
|
+
packs with new ones. This in particular means that all in-place modifications of
|
|
484
|
+
the tensor packs within the context are discarded on exit.
|
|
485
|
+
"""
|
|
486
|
+
_check_module_pack(self)
|
|
487
|
+
check_pack_idx(pack_idx, None)
|
|
488
|
+
|
|
489
|
+
for submodule in self.modules():
|
|
490
|
+
# Since the method replaces the original tensor objects with new ones,
|
|
491
|
+
# which in particular means disconnecting the module from its optimizer,
|
|
492
|
+
# training the module in its temporary state is rather an unusual use case.
|
|
493
|
+
if submodule.training:
|
|
494
|
+
raise RuntimeError(
|
|
495
|
+
'The module and all its submodules must be in evaluation mode to'
|
|
496
|
+
' use this method'
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
original_modules_and_tensors = []
|
|
500
|
+
original_pack_views = []
|
|
501
|
+
|
|
502
|
+
try:
|
|
503
|
+
for name, x in itertools.chain(
|
|
504
|
+
self.named_parameters(), self.named_buffers()
|
|
505
|
+
):
|
|
506
|
+
if isinstance(x, ParameterPack | BufferPack):
|
|
507
|
+
if '.' in name:
|
|
508
|
+
submodule_name, attr = name.rsplit('.', 1)
|
|
509
|
+
submodule = self.get_submodule(submodule_name)
|
|
510
|
+
else:
|
|
511
|
+
submodule = self
|
|
512
|
+
attr = name
|
|
513
|
+
setattr(
|
|
514
|
+
submodule,
|
|
515
|
+
attr,
|
|
516
|
+
_make_tensor_pack_like(x, data=x.data[pack_idx]),
|
|
517
|
+
)
|
|
518
|
+
original_modules_and_tensors.append((submodule, attr, x))
|
|
519
|
+
|
|
520
|
+
for submodule in self.modules():
|
|
521
|
+
if isinstance(submodule, PackView):
|
|
522
|
+
original_pack_views.append((submodule, submodule.pack_size))
|
|
523
|
+
submodule._pack_size = len(pack_idx)
|
|
524
|
+
|
|
525
|
+
yield
|
|
526
|
+
|
|
527
|
+
finally:
|
|
528
|
+
for submodule, attr, parameter in original_modules_and_tensors:
|
|
529
|
+
setattr(submodule, attr, parameter)
|
|
530
|
+
for submodule, pack_size in original_pack_views:
|
|
531
|
+
submodule._pack_size = pack_size
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _check_module_pack(module: ModulePack) -> None:
|
|
535
|
+
for word, cls, iter_named_tensors in (
|
|
536
|
+
('parameter', ParameterPack, module.named_parameters),
|
|
537
|
+
('buffer', BufferPack, module.named_buffers),
|
|
538
|
+
):
|
|
539
|
+
values_to_names = {}
|
|
540
|
+
for name, value in iter_named_tensors(remove_duplicate=False):
|
|
541
|
+
if value in values_to_names:
|
|
542
|
+
raise RuntimeError(
|
|
543
|
+
f'The module pack contains aliased {word}s, which is not allowed:'
|
|
544
|
+
f' "{values_to_names[value]}" and "{name}"' # ty: ignore[invalid-argument-type]
|
|
545
|
+
)
|
|
546
|
+
if not isinstance(value, cls):
|
|
547
|
+
raise RuntimeError( # noqa: TRY004
|
|
548
|
+
f'In module packs, {cls} must be used to store {word}s.'
|
|
549
|
+
f' However, the {word} "{name}" is stored as `{type(value)}`.'
|
|
550
|
+
)
|
|
551
|
+
values_to_names[value] = name
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _prepare_positive_integer_pack(
|
|
555
|
+
value: int | list[int], max_value: int | None, *, pack_size: int, **factory_kwargs
|
|
556
|
+
) -> tuple[BufferPack | None, int]:
|
|
557
|
+
if isinstance(value, list):
|
|
558
|
+
# The values are specified for all pack members separately.
|
|
559
|
+
if len(value) != pack_size:
|
|
560
|
+
raise ValueError(
|
|
561
|
+
'The number of per-member hyperparameter values does not match the pack'
|
|
562
|
+
' size'
|
|
563
|
+
)
|
|
564
|
+
if not all(x > 0 for x in value):
|
|
565
|
+
raise ValueError('All values must be positive')
|
|
566
|
+
|
|
567
|
+
if max_value is None:
|
|
568
|
+
max_value = max(value)
|
|
569
|
+
elif not all(x <= max_value for x in value):
|
|
570
|
+
raise ValueError('Values must not exceed `max_value`')
|
|
571
|
+
|
|
572
|
+
value_buffer = BufferPack(torch.tensor(value, **factory_kwargs))
|
|
573
|
+
else:
|
|
574
|
+
# The same value for all pack members.
|
|
575
|
+
if value <= 0:
|
|
576
|
+
raise ValueError('The value must be positive')
|
|
577
|
+
|
|
578
|
+
if max_value is None:
|
|
579
|
+
max_value = value
|
|
580
|
+
elif max_value != value:
|
|
581
|
+
raise ValueError('The configured maximum must equal the shared value')
|
|
582
|
+
|
|
583
|
+
value_buffer = None
|
|
584
|
+
|
|
585
|
+
return value_buffer, max_value
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _check_replace_arguments(
|
|
589
|
+
pack_idx: Tensor, *hyperparameter_lists: list | None
|
|
590
|
+
) -> None:
|
|
591
|
+
if not all(x is None or len(x) == len(pack_idx) for x in hyperparameter_lists):
|
|
592
|
+
raise ValueError(
|
|
593
|
+
'Each provided hyperparameter list must contain one value for each selected'
|
|
594
|
+
' pack member'
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
class LinearPack(ModulePack):
|
|
599
|
+
"""A pack of linear layers.
|
|
600
|
+
|
|
601
|
+
**Shape**
|
|
602
|
+
|
|
603
|
+
- Input: `(pack_size, batch_size, self.max_in_features)`
|
|
604
|
+
- Output: `(pack_size, batch_size, self.max_out_features)`
|
|
605
|
+
|
|
606
|
+
**Implementation details**
|
|
607
|
+
|
|
608
|
+
* The module assumes that the input is already properly masked according to
|
|
609
|
+
`in_features` passed to the constructor. Therefore, `in_features` is only used for
|
|
610
|
+
the parameter initialization in `reset_parameters`, but not in `forward`.
|
|
611
|
+
* The module properly masks its output in `forward` according to `out_features`
|
|
612
|
+
passed to the constructor.
|
|
613
|
+
|
|
614
|
+
**Examples**
|
|
615
|
+
|
|
616
|
+
The same layer shapes for all pack members:
|
|
617
|
+
|
|
618
|
+
>>> m = LinearPack(4, 5, pack_size=2)
|
|
619
|
+
>>> m.pack_size
|
|
620
|
+
2
|
|
621
|
+
>>> x = torch.randn(2, 3, 4)
|
|
622
|
+
>>> m(x).shape # Input masking is assumed
|
|
623
|
+
torch.Size([2, 3, 5])
|
|
624
|
+
|
|
625
|
+
Different layer shapes:
|
|
626
|
+
|
|
627
|
+
>>> m = LinearPack([4, 5], [6, 7], pack_size=2)
|
|
628
|
+
>>> m.max_in_features
|
|
629
|
+
5
|
|
630
|
+
>>> m.max_out_features
|
|
631
|
+
7
|
|
632
|
+
>>> x = torch.randn(2, 3, 5)
|
|
633
|
+
>>> m(x).shape # Input masking is assumed
|
|
634
|
+
torch.Size([2, 3, 7])
|
|
635
|
+
|
|
636
|
+
Different layer shapes, with spare capacity for replacing layers with larger ones
|
|
637
|
+
later:
|
|
638
|
+
|
|
639
|
+
>>> m = LinearPack(
|
|
640
|
+
... [4, 5], [6, 7], max_in_features=8, max_out_features=9, pack_size=2
|
|
641
|
+
... )
|
|
642
|
+
>>> m.max_in_features
|
|
643
|
+
8
|
|
644
|
+
>>> m.max_out_features
|
|
645
|
+
9
|
|
646
|
+
>>> x = torch.randn(2, 3, 8)
|
|
647
|
+
>>> m(x).shape # Input masking is assumed
|
|
648
|
+
torch.Size([2, 3, 9])
|
|
649
|
+
"""
|
|
650
|
+
|
|
651
|
+
def __init__(
|
|
652
|
+
self,
|
|
653
|
+
in_features: int | list[int],
|
|
654
|
+
out_features: int | list[int],
|
|
655
|
+
bias: bool = True,
|
|
656
|
+
*,
|
|
657
|
+
max_in_features: int | None = None,
|
|
658
|
+
max_out_features: int | None = None,
|
|
659
|
+
pack_size: int,
|
|
660
|
+
dtype: torch.dtype | None = None,
|
|
661
|
+
device: str | torch.device | None = None,
|
|
662
|
+
) -> None:
|
|
663
|
+
if pack_size <= 0:
|
|
664
|
+
raise ValueError('The pack size must be positive')
|
|
665
|
+
|
|
666
|
+
factory_kwargs: KWArgs = {'dtype': dtype, 'device': device}
|
|
667
|
+
in_features_buffer, max_in_features = _prepare_positive_integer_pack(
|
|
668
|
+
in_features, max_in_features, pack_size=pack_size, **factory_kwargs
|
|
669
|
+
)
|
|
670
|
+
out_features_buffer, max_out_features = _prepare_positive_integer_pack(
|
|
671
|
+
out_features, max_out_features, pack_size=pack_size, **factory_kwargs
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
super().__init__(pack_size=pack_size)
|
|
675
|
+
self.in_features = in_features_buffer
|
|
676
|
+
self.out_features = out_features_buffer
|
|
677
|
+
self.weight = ParameterPack(
|
|
678
|
+
torch.empty(pack_size, max_out_features, max_in_features, **factory_kwargs)
|
|
679
|
+
)
|
|
680
|
+
self.bias = (
|
|
681
|
+
ParameterPack(torch.empty(pack_size, max_out_features, **factory_kwargs))
|
|
682
|
+
if bias
|
|
683
|
+
else None
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
self.reset_parameters()
|
|
687
|
+
|
|
688
|
+
@property
|
|
689
|
+
def max_in_features(self) -> int:
|
|
690
|
+
"""The maximum supported number of input features."""
|
|
691
|
+
return self.weight.shape[-1]
|
|
692
|
+
|
|
693
|
+
@property
|
|
694
|
+
def max_out_features(self) -> int:
|
|
695
|
+
"""The maximum supported number of output features."""
|
|
696
|
+
return self.weight.shape[-2]
|
|
697
|
+
|
|
698
|
+
@property
|
|
699
|
+
def pack_size(self) -> int:
|
|
700
|
+
return self.weight.shape[PACK_DIM]
|
|
701
|
+
|
|
702
|
+
def reset_parameters(self, *, pack_idx: Tensor | None = None) -> None:
|
|
703
|
+
"""Initialize the parameters."""
|
|
704
|
+
if pack_idx is not None:
|
|
705
|
+
check_pack_idx(pack_idx, self.pack_size)
|
|
706
|
+
|
|
707
|
+
if self.in_features is None:
|
|
708
|
+
d_in_rsqrt = self.max_in_features**-0.5
|
|
709
|
+
init_args = (-d_in_rsqrt, d_in_rsqrt)
|
|
710
|
+
|
|
711
|
+
for p in (self.weight, self.bias):
|
|
712
|
+
if p is not None:
|
|
713
|
+
if pack_idx is None:
|
|
714
|
+
nn.init.uniform_(p, *init_args)
|
|
715
|
+
else:
|
|
716
|
+
with torch.no_grad():
|
|
717
|
+
p[pack_idx] = torch.empty(
|
|
718
|
+
len(pack_idx),
|
|
719
|
+
*p.shape[1:],
|
|
720
|
+
dtype=p.dtype,
|
|
721
|
+
device=p.device,
|
|
722
|
+
).uniform_(*init_args)
|
|
723
|
+
|
|
724
|
+
else:
|
|
725
|
+
in_features = _get_tensor_pack(self.in_features, pack_idx)
|
|
726
|
+
d_in_rsqrt = in_features.float().rsqrt()
|
|
727
|
+
d_in_rsqrt_neg = -d_in_rsqrt
|
|
728
|
+
|
|
729
|
+
for p in (self.weight, self.bias):
|
|
730
|
+
if p is not None:
|
|
731
|
+
single_shape = p.shape[1:]
|
|
732
|
+
broadcast_idx = (slice(None), *((None,) * (p.ndim - 1)))
|
|
733
|
+
p_init = torch.lerp(
|
|
734
|
+
d_in_rsqrt_neg[broadcast_idx].expand(-1, *single_shape),
|
|
735
|
+
d_in_rsqrt[broadcast_idx].expand(-1, *single_shape),
|
|
736
|
+
torch.rand(
|
|
737
|
+
self.pack_size if pack_idx is None else len(pack_idx),
|
|
738
|
+
*single_shape,
|
|
739
|
+
dtype=p.dtype,
|
|
740
|
+
device=p.device,
|
|
741
|
+
),
|
|
742
|
+
)
|
|
743
|
+
with torch.no_grad():
|
|
744
|
+
if pack_idx is None:
|
|
745
|
+
p.copy_(p_init)
|
|
746
|
+
else:
|
|
747
|
+
p[pack_idx] = p_init
|
|
748
|
+
|
|
749
|
+
def replace(
|
|
750
|
+
self,
|
|
751
|
+
*,
|
|
752
|
+
pack_idx: Tensor,
|
|
753
|
+
in_features: list[int] | None = None,
|
|
754
|
+
out_features: list[int] | None = None,
|
|
755
|
+
) -> None:
|
|
756
|
+
check_pack_idx(pack_idx, self.pack_size)
|
|
757
|
+
_check_replace_arguments(pack_idx, in_features, out_features)
|
|
758
|
+
|
|
759
|
+
updates = []
|
|
760
|
+
for buffer, max_n_features, n_features in [
|
|
761
|
+
(self.in_features, self.max_in_features, in_features),
|
|
762
|
+
(self.out_features, self.max_out_features, out_features),
|
|
763
|
+
]:
|
|
764
|
+
if n_features is not None:
|
|
765
|
+
if buffer is None:
|
|
766
|
+
raise RuntimeError(
|
|
767
|
+
'Cannot replace feature counts that are shared by all pack'
|
|
768
|
+
' members'
|
|
769
|
+
)
|
|
770
|
+
if not all(0 < x <= max_n_features for x in n_features):
|
|
771
|
+
raise ValueError(
|
|
772
|
+
'Feature counts must be positive and must not exceed the'
|
|
773
|
+
' allocated maximum'
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
# Validate both feature counts before updating either buffer.
|
|
777
|
+
updates.append(
|
|
778
|
+
(
|
|
779
|
+
buffer,
|
|
780
|
+
torch.tensor(
|
|
781
|
+
n_features, dtype=buffer.dtype, device=buffer.device
|
|
782
|
+
),
|
|
783
|
+
)
|
|
784
|
+
)
|
|
785
|
+
for buffer, values in updates:
|
|
786
|
+
buffer[pack_idx] = values
|
|
787
|
+
|
|
788
|
+
# The parameters are reset _after_ updating the hyperparameters, because the
|
|
789
|
+
# initialization depends on the hyperparameter values.
|
|
790
|
+
self.reset_parameters(pack_idx=pack_idx)
|
|
791
|
+
|
|
792
|
+
def forward(self, x: Tensor, pack_idx: Tensor | None = None) -> Tensor:
|
|
793
|
+
if x.ndim != 3:
|
|
794
|
+
raise ValueError('The input must have three dimensions')
|
|
795
|
+
if x.shape[-1] != self.max_in_features:
|
|
796
|
+
raise ValueError(
|
|
797
|
+
'The number of input features must be equal to the maximum supported'
|
|
798
|
+
' number of features'
|
|
799
|
+
)
|
|
800
|
+
|
|
801
|
+
weight = _get_tensor_pack(self.weight, pack_idx)
|
|
802
|
+
bias = _get_tensor_pack(self.bias, pack_idx)
|
|
803
|
+
out_features = _get_tensor_pack(self.out_features, pack_idx)
|
|
804
|
+
|
|
805
|
+
x = (
|
|
806
|
+
torch.bmm(x, weight.transpose(-2, -1))
|
|
807
|
+
if bias is None
|
|
808
|
+
else torch.baddbmm(bias.unsqueeze(BATCH_DIM), x, weight.transpose(-2, -1))
|
|
809
|
+
)
|
|
810
|
+
if out_features is not None:
|
|
811
|
+
output_mask = (
|
|
812
|
+
torch.arange(self.max_out_features, device=x.device)[None]
|
|
813
|
+
< out_features[:, None]
|
|
814
|
+
)
|
|
815
|
+
x = x * output_mask.to(x.dtype).unsqueeze(BATCH_DIM)
|
|
816
|
+
return x
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
class DropoutPack(ModulePack):
|
|
820
|
+
"""A pack of dropout layers.
|
|
821
|
+
|
|
822
|
+
Dropout probabilities must be in `[0, 1)`.
|
|
823
|
+
|
|
824
|
+
**Shapes**
|
|
825
|
+
|
|
826
|
+
- Input: `(pack_size, *)`
|
|
827
|
+
- Output: `(pack_size, *)`
|
|
828
|
+
|
|
829
|
+
**Examples**
|
|
830
|
+
|
|
831
|
+
The same dropout ratio for all pack members:
|
|
832
|
+
|
|
833
|
+
>>> m = DropoutPack(0.5, pack_size=2)
|
|
834
|
+
>>> x = torch.randn(2, 3, 4)
|
|
835
|
+
>>> m(x).shape
|
|
836
|
+
torch.Size([2, 3, 4])
|
|
837
|
+
|
|
838
|
+
Different dropout ratios:
|
|
839
|
+
|
|
840
|
+
>>> m = DropoutPack([0.1, 0.5], pack_size=2)
|
|
841
|
+
>>> x = torch.randn(2, 3, 4)
|
|
842
|
+
>>> m(x).shape
|
|
843
|
+
torch.Size([2, 3, 4])
|
|
844
|
+
"""
|
|
845
|
+
|
|
846
|
+
def __init__(self, p: float | list[float], *, pack_size: int) -> None:
|
|
847
|
+
if isinstance(p, int | float):
|
|
848
|
+
if not math.isfinite(p) or p < 0.0 or p >= 1.0:
|
|
849
|
+
raise ValueError('The dropout probability must be finite and in [0, 1)')
|
|
850
|
+
else:
|
|
851
|
+
if len(p) != pack_size:
|
|
852
|
+
raise ValueError('Provide one dropout probability per pack member')
|
|
853
|
+
if any(not math.isfinite(x) or x < 0.0 or x >= 1.0 for x in p):
|
|
854
|
+
raise ValueError('Dropout probabilities must be finite and in [0, 1)')
|
|
855
|
+
|
|
856
|
+
super().__init__(pack_size=pack_size)
|
|
857
|
+
self.p = (
|
|
858
|
+
float(p)
|
|
859
|
+
if isinstance(p, int | float)
|
|
860
|
+
else BufferPack(torch.tensor([float(x) for x in p]))
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
@property
|
|
864
|
+
def pack_size(self) -> int:
|
|
865
|
+
if isinstance(self.p, int | float):
|
|
866
|
+
raise RuntimeError('Cannot infer the pack size') # noqa: TRY004
|
|
867
|
+
return self.p.shape[PACK_DIM]
|
|
868
|
+
|
|
869
|
+
def replace(self, *, pack_idx: Tensor, p: list[float] | None = None) -> None:
|
|
870
|
+
if p is None:
|
|
871
|
+
return
|
|
872
|
+
|
|
873
|
+
if not isinstance(self.p, BufferPack):
|
|
874
|
+
raise RuntimeError( # noqa: TRY004
|
|
875
|
+
'Cannot replace dropout probabilities when one probability is shared by'
|
|
876
|
+
' all pack members'
|
|
877
|
+
)
|
|
878
|
+
|
|
879
|
+
check_pack_idx(pack_idx, self.pack_size)
|
|
880
|
+
_check_replace_arguments(pack_idx, p)
|
|
881
|
+
if any(not math.isfinite(x) or x < 0.0 or x >= 1.0 for x in p):
|
|
882
|
+
raise ValueError('Dropout probabilities must be finite and in [0, 1)')
|
|
883
|
+
|
|
884
|
+
self.p[pack_idx] = torch.tensor(p, dtype=self.p.dtype, device=self.p.device)
|
|
885
|
+
|
|
886
|
+
def forward(self, x: Tensor, pack_idx: Tensor | None = None) -> Tensor:
|
|
887
|
+
if pack_idx is not None:
|
|
888
|
+
check_pack_idx(pack_idx, self.try_get_pack_size())
|
|
889
|
+
|
|
890
|
+
# Passing the tuple instead of `int | float` keeps code traceable by
|
|
891
|
+
# PyTorch 2.7-2.9 strict export.
|
|
892
|
+
if isinstance(self.p, (int, float)):
|
|
893
|
+
return nn.functional.dropout(x, p=self.p, training=self.training)
|
|
894
|
+
|
|
895
|
+
p = _get_tensor_pack(self.p, pack_idx)
|
|
896
|
+
|
|
897
|
+
if self.training:
|
|
898
|
+
p_keep = 1.0 - p
|
|
899
|
+
p_keep = p_keep[(slice(None), *((None,) * (x.ndim - 1)))]
|
|
900
|
+
mask = torch.empty_like(x).bernoulli_(p_keep.expand_as(x))
|
|
901
|
+
return mask.div_(p_keep) * x
|
|
902
|
+
else:
|
|
903
|
+
return x
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
class LeakyReLUPack(ModulePack):
|
|
907
|
+
"""A pack of leaky ReLU activations.
|
|
908
|
+
|
|
909
|
+
This class is not used in the original paper experiments and exists as an additional
|
|
910
|
+
illustration of how the "Module Pack" pattern can be used.
|
|
911
|
+
|
|
912
|
+
**Shapes**
|
|
913
|
+
|
|
914
|
+
- Input: `(pack_size, *)`
|
|
915
|
+
- Output: `(pack_size, *)`
|
|
916
|
+
|
|
917
|
+
**Examples**
|
|
918
|
+
|
|
919
|
+
>>> m = LeakyReLUPack(negative_slope=[0.01, 0.02], pack_size=2)
|
|
920
|
+
>>> x = torch.randn(2, 3)
|
|
921
|
+
>>> m(x).shape
|
|
922
|
+
torch.Size([2, 3])
|
|
923
|
+
"""
|
|
924
|
+
|
|
925
|
+
def __init__(self, *, negative_slope: float | list[float], pack_size: int) -> None:
|
|
926
|
+
if isinstance(negative_slope, int | float):
|
|
927
|
+
if not math.isfinite(negative_slope) or negative_slope <= 0:
|
|
928
|
+
raise ValueError('The slope must be finite and positive')
|
|
929
|
+
else:
|
|
930
|
+
if len(negative_slope) != pack_size:
|
|
931
|
+
raise ValueError(
|
|
932
|
+
'The size of the list of per-member slope values must match the'
|
|
933
|
+
' pack size'
|
|
934
|
+
)
|
|
935
|
+
if any(not math.isfinite(x) or x <= 0 for x in negative_slope):
|
|
936
|
+
raise ValueError('Slopes must be finite and positive')
|
|
937
|
+
|
|
938
|
+
super().__init__(pack_size=pack_size)
|
|
939
|
+
self.negative_slope = (
|
|
940
|
+
BufferPack(torch.tensor([float(x) for x in negative_slope]))
|
|
941
|
+
if isinstance(negative_slope, list)
|
|
942
|
+
else float(negative_slope)
|
|
943
|
+
)
|
|
944
|
+
|
|
945
|
+
@property
|
|
946
|
+
def pack_size(self) -> int:
|
|
947
|
+
if isinstance(self.negative_slope, int | float):
|
|
948
|
+
raise RuntimeError('Cannot infer the pack size') # noqa: TRY004
|
|
949
|
+
return self.negative_slope.shape[PACK_DIM]
|
|
950
|
+
|
|
951
|
+
def replace(
|
|
952
|
+
self, *, pack_idx: Tensor, negative_slope: list[float] | None = None
|
|
953
|
+
) -> None:
|
|
954
|
+
if negative_slope is None:
|
|
955
|
+
return
|
|
956
|
+
|
|
957
|
+
if not isinstance(self.negative_slope, BufferPack):
|
|
958
|
+
raise RuntimeError( # noqa: TRY004
|
|
959
|
+
'Cannot replace slopes that are shared by all pack members'
|
|
960
|
+
)
|
|
961
|
+
|
|
962
|
+
check_pack_idx(pack_idx, self.pack_size)
|
|
963
|
+
_check_replace_arguments(pack_idx, negative_slope)
|
|
964
|
+
if any(not math.isfinite(x) or x <= 0 for x in negative_slope):
|
|
965
|
+
raise ValueError('Slopes must be finite and positive')
|
|
966
|
+
|
|
967
|
+
self.negative_slope[pack_idx] = torch.tensor(
|
|
968
|
+
negative_slope,
|
|
969
|
+
dtype=self.negative_slope.dtype,
|
|
970
|
+
device=self.negative_slope.device,
|
|
971
|
+
)
|
|
972
|
+
|
|
973
|
+
def forward(self, x: Tensor, pack_idx: Tensor | None = None) -> Tensor:
|
|
974
|
+
if pack_idx is not None:
|
|
975
|
+
check_pack_idx(pack_idx, self.try_get_pack_size())
|
|
976
|
+
|
|
977
|
+
# Passing the tuple instead of `int | float` keeps code traceable by
|
|
978
|
+
# PyTorch 2.7-2.9 strict export.
|
|
979
|
+
if isinstance(self.negative_slope, (int, float)):
|
|
980
|
+
return nn.functional.leaky_relu(x, self.negative_slope)
|
|
981
|
+
|
|
982
|
+
negative_slope = _get_tensor_pack(self.negative_slope, pack_idx)
|
|
983
|
+
unsqueeze_idx = (slice(None), *((None,) * (x.ndim - 1)))
|
|
984
|
+
return torch.where(x >= 0, x, x * negative_slope[unsqueeze_idx])
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
def _make_activation(type: type[nn.Module], *, pack_size: int, **kwargs) -> nn.Module:
|
|
988
|
+
cls = type
|
|
989
|
+
del type
|
|
990
|
+
|
|
991
|
+
return cls(
|
|
992
|
+
**kwargs, **({'pack_size': pack_size} if issubclass(cls, ModulePack) else {})
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
class MLPBackbonePack(ModulePack):
|
|
997
|
+
"""A pack of multilayer perceptron backbones.
|
|
998
|
+
|
|
999
|
+
**Examples**
|
|
1000
|
+
|
|
1001
|
+
>>> m = MLPBackbonePack(
|
|
1002
|
+
... d_in=[4, 5],
|
|
1003
|
+
... n_blocks=[6, 7],
|
|
1004
|
+
... d_block=[8, 9],
|
|
1005
|
+
... dropout=[0.1, 0.2],
|
|
1006
|
+
... pack_size=2,
|
|
1007
|
+
... )
|
|
1008
|
+
>>> m.pack_size
|
|
1009
|
+
2
|
|
1010
|
+
>>> x = torch.randn(2, 3, 5)
|
|
1011
|
+
>>> m(x).shape # Input masking is assumed
|
|
1012
|
+
torch.Size([2, 3, 9])
|
|
1013
|
+
"""
|
|
1014
|
+
|
|
1015
|
+
class Block(ModulePack):
|
|
1016
|
+
"""A pack of MLP blocks (linear layer + activation + dropout)."""
|
|
1017
|
+
|
|
1018
|
+
def __init__(
|
|
1019
|
+
self,
|
|
1020
|
+
*,
|
|
1021
|
+
activation: KWArgs,
|
|
1022
|
+
dropout: float | list[float],
|
|
1023
|
+
pack_size: int,
|
|
1024
|
+
**linear_kwargs,
|
|
1025
|
+
) -> None:
|
|
1026
|
+
super().__init__(pack_size=pack_size)
|
|
1027
|
+
self.linear = LinearPack(pack_size=pack_size, **linear_kwargs)
|
|
1028
|
+
self.activation = _make_activation(pack_size=pack_size, **activation)
|
|
1029
|
+
self.dropout = DropoutPack(dropout, pack_size=pack_size)
|
|
1030
|
+
|
|
1031
|
+
@property
|
|
1032
|
+
def pack_size(self) -> int:
|
|
1033
|
+
return self.linear.pack_size
|
|
1034
|
+
|
|
1035
|
+
def replace(
|
|
1036
|
+
self,
|
|
1037
|
+
*,
|
|
1038
|
+
pack_idx: Tensor,
|
|
1039
|
+
in_features: list[int] | None = None,
|
|
1040
|
+
out_features: list[int] | None = None,
|
|
1041
|
+
activation: KWArgs | None = None,
|
|
1042
|
+
dropout: list[float] | None = None,
|
|
1043
|
+
) -> None:
|
|
1044
|
+
self.linear.replace(
|
|
1045
|
+
pack_idx=pack_idx, in_features=in_features, out_features=out_features
|
|
1046
|
+
)
|
|
1047
|
+
|
|
1048
|
+
activation_replace = (
|
|
1049
|
+
getattr(self.activation, 'replace', None)
|
|
1050
|
+
if isinstance(self.activation, ModulePack)
|
|
1051
|
+
else None
|
|
1052
|
+
)
|
|
1053
|
+
if activation_replace is not None and not inspect.ismethod(
|
|
1054
|
+
activation_replace
|
|
1055
|
+
):
|
|
1056
|
+
raise RuntimeError(
|
|
1057
|
+
'The activation module does not support member replacement'
|
|
1058
|
+
)
|
|
1059
|
+
if activation is not None and activation_replace is None:
|
|
1060
|
+
raise RuntimeError(
|
|
1061
|
+
'The activation module does not support member replacement'
|
|
1062
|
+
)
|
|
1063
|
+
|
|
1064
|
+
if activation_replace is not None:
|
|
1065
|
+
activation_replace(
|
|
1066
|
+
pack_idx=pack_idx, **({} if activation is None else activation)
|
|
1067
|
+
)
|
|
1068
|
+
|
|
1069
|
+
self.dropout.replace(pack_idx=pack_idx, p=dropout)
|
|
1070
|
+
|
|
1071
|
+
def forward(self, x: Tensor, pack_idx: Tensor | None = None) -> Tensor:
|
|
1072
|
+
x = self.linear(x, pack_idx)
|
|
1073
|
+
x = (
|
|
1074
|
+
self.activation(x, pack_idx)
|
|
1075
|
+
if isinstance(self.activation, ModulePack)
|
|
1076
|
+
else self.activation(x)
|
|
1077
|
+
)
|
|
1078
|
+
x = self.dropout(x, pack_idx)
|
|
1079
|
+
return x
|
|
1080
|
+
|
|
1081
|
+
@enum.unique
|
|
1082
|
+
class ForwardImpl(enum.StrEnum):
|
|
1083
|
+
"""The implementation of the forward pass."""
|
|
1084
|
+
|
|
1085
|
+
DENSE = enum.auto()
|
|
1086
|
+
"""
|
|
1087
|
+
All blocks are applied to all pack members with masking. This implementation is
|
|
1088
|
+
usually faster for light workloads (e.g., small input batches) and makes the
|
|
1089
|
+
module easier to export.
|
|
1090
|
+
"""
|
|
1091
|
+
|
|
1092
|
+
INDEXED = enum.auto()
|
|
1093
|
+
"""
|
|
1094
|
+
In each block, only the pack members that are still active are computed. This
|
|
1095
|
+
implementation is faster for training (large) packs of depth-varying MLPs. For
|
|
1096
|
+
non-trivial export scenarios (e.g., using ONNX or `torch.export`) with this
|
|
1097
|
+
implementation:
|
|
1098
|
+
|
|
1099
|
+
- First, set the module's device and data type as needed.
|
|
1100
|
+
- Then, do a warmup forward pass _without_ passing `pack_idx`.
|
|
1101
|
+
- Then, export the module. The exported module must be used _without_ passing
|
|
1102
|
+
`pack_idx` to the forward pass.
|
|
1103
|
+
|
|
1104
|
+
Note:
|
|
1105
|
+
With this implementation, for depth-varying packs, passing `pack_idx` to the
|
|
1106
|
+
forward pass triggers a host-device synchronization in addition to the usual
|
|
1107
|
+
overhead associated with using `pack_idx`. See the documentation for
|
|
1108
|
+
`ModulePack` for more details on the `pack_idx` mechanism.
|
|
1109
|
+
"""
|
|
1110
|
+
|
|
1111
|
+
class _ActiveLocalPackIdxCache(NamedTuple):
|
|
1112
|
+
n_blocks: BufferPack | None
|
|
1113
|
+
n_blocks_version: int | None
|
|
1114
|
+
value: list[Tensor | None]
|
|
1115
|
+
|
|
1116
|
+
@staticmethod
|
|
1117
|
+
def _default_load_state_dict_post_hook(m: 'MLPBackbonePack', _) -> None:
|
|
1118
|
+
m._reset_active_local_pack_idx_cache()
|
|
1119
|
+
|
|
1120
|
+
def __init__(
|
|
1121
|
+
self,
|
|
1122
|
+
*,
|
|
1123
|
+
d_in: int | list[int],
|
|
1124
|
+
max_d_in: int | None = None,
|
|
1125
|
+
n_blocks: int | list[int],
|
|
1126
|
+
d_block: int | list[int],
|
|
1127
|
+
activation: KWArgs | None = None,
|
|
1128
|
+
dropout: float | list[float],
|
|
1129
|
+
max_n_blocks: int | None = None,
|
|
1130
|
+
max_d_block: int | None = None,
|
|
1131
|
+
forward_impl: ForwardImpl | str = ForwardImpl.DENSE,
|
|
1132
|
+
pack_size: int,
|
|
1133
|
+
) -> None:
|
|
1134
|
+
n_blocks_buffer, max_n_blocks = _prepare_positive_integer_pack(
|
|
1135
|
+
n_blocks, max_n_blocks, pack_size=pack_size
|
|
1136
|
+
)
|
|
1137
|
+
|
|
1138
|
+
super().__init__(pack_size=pack_size)
|
|
1139
|
+
cls = type(self)
|
|
1140
|
+
self.blocks = nn.ModuleList(
|
|
1141
|
+
[
|
|
1142
|
+
cls.Block(
|
|
1143
|
+
in_features=d_in if i == 0 else d_block,
|
|
1144
|
+
out_features=d_block,
|
|
1145
|
+
max_in_features=max_d_in if i == 0 else max_d_block,
|
|
1146
|
+
max_out_features=max_d_block,
|
|
1147
|
+
activation={'type': nn.ReLU} if activation is None else activation,
|
|
1148
|
+
dropout=dropout,
|
|
1149
|
+
pack_size=pack_size,
|
|
1150
|
+
)
|
|
1151
|
+
for i in range(max_n_blocks)
|
|
1152
|
+
]
|
|
1153
|
+
)
|
|
1154
|
+
|
|
1155
|
+
self._max_n_blocks: Final = max_n_blocks
|
|
1156
|
+
self.n_blocks = n_blocks_buffer
|
|
1157
|
+
# For depth-varying packs, deeper blocks may have fewer active members. Their
|
|
1158
|
+
# indices are computed from `self.n_blocks` and cached to avoid repeating the
|
|
1159
|
+
# host-device synchronization required to compute them.
|
|
1160
|
+
#
|
|
1161
|
+
# Keep the cached `self.n_blocks` reference inside the tuple: assigning it
|
|
1162
|
+
# directly as a module attribute would register an aliased buffer, which
|
|
1163
|
+
# is forbidden in module packs.
|
|
1164
|
+
#
|
|
1165
|
+
# Finally, note that modifying `self.n_blocks` in place invalidates the cache.
|
|
1166
|
+
# Therefore, use `self.n_blocks_index_put_` for any in-place modifications.
|
|
1167
|
+
self._active_local_pack_idx_cache: (
|
|
1168
|
+
MLPBackbonePack._ActiveLocalPackIdxCache | None
|
|
1169
|
+
) = None
|
|
1170
|
+
|
|
1171
|
+
self.register_load_state_dict_post_hook(
|
|
1172
|
+
MLPBackbonePack._default_load_state_dict_post_hook
|
|
1173
|
+
)
|
|
1174
|
+
|
|
1175
|
+
self._forward_impl = MLPBackbonePack.ForwardImpl(forward_impl)
|
|
1176
|
+
|
|
1177
|
+
@property
|
|
1178
|
+
def pack_size(self) -> int:
|
|
1179
|
+
return self.blocks[0].pack_size # ty: ignore[invalid-return-type]
|
|
1180
|
+
|
|
1181
|
+
@property
|
|
1182
|
+
def max_n_blocks(self) -> int:
|
|
1183
|
+
"""The maximum supported number of blocks."""
|
|
1184
|
+
return self._max_n_blocks
|
|
1185
|
+
|
|
1186
|
+
@property
|
|
1187
|
+
def forward_impl(self) -> ForwardImpl:
|
|
1188
|
+
"""The implementation of the forward pass."""
|
|
1189
|
+
return self._forward_impl
|
|
1190
|
+
|
|
1191
|
+
def set_forward_impl(self, impl: ForwardImpl | str) -> None:
|
|
1192
|
+
"""Set the implementation of the forward pass."""
|
|
1193
|
+
self._forward_impl = MLPBackbonePack.ForwardImpl(impl)
|
|
1194
|
+
|
|
1195
|
+
def _reset_active_local_pack_idx_cache(self) -> None:
|
|
1196
|
+
self._active_local_pack_idx_cache = None
|
|
1197
|
+
|
|
1198
|
+
def _load_from_state_dict_at(
|
|
1199
|
+
self,
|
|
1200
|
+
state_dict: dict[str, Tensor],
|
|
1201
|
+
prefix: str,
|
|
1202
|
+
*,
|
|
1203
|
+
pack_idx: Tensor,
|
|
1204
|
+
state_dict_idx: Tensor | None,
|
|
1205
|
+
) -> None:
|
|
1206
|
+
self._reset_active_local_pack_idx_cache()
|
|
1207
|
+
super()._load_from_state_dict_at(
|
|
1208
|
+
state_dict, prefix, pack_idx=pack_idx, state_dict_idx=state_dict_idx
|
|
1209
|
+
)
|
|
1210
|
+
|
|
1211
|
+
def n_blocks_index_put_(self, indices: tuple[Tensor, ...], values: Tensor) -> None:
|
|
1212
|
+
"""Modify `n_blocks` in place.
|
|
1213
|
+
|
|
1214
|
+
This method is the only correct way to modify `n_blocks` in place. The method
|
|
1215
|
+
directly calls `self.n_blocks.index_put_` with the provided arguments, and
|
|
1216
|
+
updates the parts of the module's state that depend on `self.n_blocks`.
|
|
1217
|
+
"""
|
|
1218
|
+
if self.n_blocks is None:
|
|
1219
|
+
raise RuntimeError(
|
|
1220
|
+
'Cannot change block counts when all pack members share a fixed depth'
|
|
1221
|
+
)
|
|
1222
|
+
|
|
1223
|
+
self._reset_active_local_pack_idx_cache()
|
|
1224
|
+
self.n_blocks.index_put_(indices, values)
|
|
1225
|
+
|
|
1226
|
+
def replace(
|
|
1227
|
+
self,
|
|
1228
|
+
*,
|
|
1229
|
+
pack_idx: Tensor,
|
|
1230
|
+
d_in: list[int] | None = None,
|
|
1231
|
+
n_blocks: list[int] | None = None,
|
|
1232
|
+
d_block: list[int] | None = None,
|
|
1233
|
+
**kwargs,
|
|
1234
|
+
) -> None:
|
|
1235
|
+
"""
|
|
1236
|
+
All blocks of a given MLP share the replacement width, activation settings,
|
|
1237
|
+
and dropout rate.
|
|
1238
|
+
"""
|
|
1239
|
+
check_pack_idx(pack_idx, self.pack_size)
|
|
1240
|
+
_check_replace_arguments(pack_idx, n_blocks)
|
|
1241
|
+
|
|
1242
|
+
if n_blocks is None:
|
|
1243
|
+
new_n_blocks = None
|
|
1244
|
+
else:
|
|
1245
|
+
if self.n_blocks is None:
|
|
1246
|
+
raise RuntimeError(
|
|
1247
|
+
'Cannot change block counts when all pack members share a fixed'
|
|
1248
|
+
' depth'
|
|
1249
|
+
)
|
|
1250
|
+
if not all(0 < x <= self.max_n_blocks for x in n_blocks):
|
|
1251
|
+
raise ValueError(
|
|
1252
|
+
'Block counts must be positive and must not exceed `max_n_blocks`'
|
|
1253
|
+
)
|
|
1254
|
+
|
|
1255
|
+
# Defer the depth update until all block replacements succeed.
|
|
1256
|
+
new_n_blocks = torch.tensor(
|
|
1257
|
+
n_blocks, dtype=self.n_blocks.dtype, device=self.n_blocks.device
|
|
1258
|
+
)
|
|
1259
|
+
|
|
1260
|
+
# For simplicity, all the blocks are replaced regardless of `n_blocks`.
|
|
1261
|
+
for i_block, block in enumerate(self.blocks):
|
|
1262
|
+
block.replace(
|
|
1263
|
+
pack_idx=pack_idx,
|
|
1264
|
+
in_features=d_in if i_block == 0 else d_block,
|
|
1265
|
+
out_features=d_block,
|
|
1266
|
+
**kwargs,
|
|
1267
|
+
)
|
|
1268
|
+
|
|
1269
|
+
if new_n_blocks is not None:
|
|
1270
|
+
self.n_blocks_index_put_((pack_idx,), new_n_blocks)
|
|
1271
|
+
|
|
1272
|
+
def _forward_dense(self, x: Tensor, pack_idx: Tensor | None = None) -> Tensor:
|
|
1273
|
+
if pack_idx is not None:
|
|
1274
|
+
check_pack_idx(pack_idx, self.pack_size)
|
|
1275
|
+
|
|
1276
|
+
n_blocks = _get_tensor_pack(self.n_blocks, pack_idx)
|
|
1277
|
+
for i_block, block in enumerate(self.blocks):
|
|
1278
|
+
x_new = block(x, pack_idx)
|
|
1279
|
+
# All members are active in the first block.
|
|
1280
|
+
if n_blocks is None or i_block == 0:
|
|
1281
|
+
x = x_new
|
|
1282
|
+
else:
|
|
1283
|
+
assert PACK_DIM == 0
|
|
1284
|
+
active_pack_mask = i_block < n_blocks
|
|
1285
|
+
x = torch.where(
|
|
1286
|
+
active_pack_mask.view(-1, *((1,) * (x.ndim - 1))), x_new, x
|
|
1287
|
+
)
|
|
1288
|
+
return x
|
|
1289
|
+
|
|
1290
|
+
# The method must return non-inference tensors to make the computed indices usable
|
|
1291
|
+
# in all modes regardless of the mode in which the indices are computed.
|
|
1292
|
+
@torch.inference_mode(False)
|
|
1293
|
+
def _compute_active_local_pack_idx_by_block(
|
|
1294
|
+
self, pack_idx: Tensor | None
|
|
1295
|
+
) -> list[Tensor | None]:
|
|
1296
|
+
n_blocks = _get_tensor_pack(self.n_blocks, pack_idx)
|
|
1297
|
+
|
|
1298
|
+
if n_blocks is None:
|
|
1299
|
+
return [None for _ in range(self.max_n_blocks)]
|
|
1300
|
+
|
|
1301
|
+
else:
|
|
1302
|
+
pack_size = self.pack_size if pack_idx is None else len(pack_idx)
|
|
1303
|
+
# Consider the following example (`pack_size = 6`):
|
|
1304
|
+
# n_blocks = [1, 2, 3, 1, 2, 3]
|
|
1305
|
+
#
|
|
1306
|
+
# The i-th row of `mask` indicates which pack members are active
|
|
1307
|
+
# in the i-th block:
|
|
1308
|
+
# mask = [
|
|
1309
|
+
# [True, True, True, True, True, True],
|
|
1310
|
+
# [False, True, True, False, True, True],
|
|
1311
|
+
# [False, False, True, False, False, True],
|
|
1312
|
+
# ]
|
|
1313
|
+
#
|
|
1314
|
+
# `counts` contains the number of pack members active in each block:
|
|
1315
|
+
# counts = [6, 4, 2]
|
|
1316
|
+
#
|
|
1317
|
+
# `counts` enables the use of `nonzero_static` instead of `nonzero`,
|
|
1318
|
+
# reducing the number of host-device synchronizations in this function
|
|
1319
|
+
# to one (triggered by the `.tolist()` call below).
|
|
1320
|
+
mask = (
|
|
1321
|
+
torch.arange(self.max_n_blocks, device=n_blocks.device)[:, None]
|
|
1322
|
+
< n_blocks[None]
|
|
1323
|
+
)
|
|
1324
|
+
counts = mask.long().sum(1).tolist()
|
|
1325
|
+
return [
|
|
1326
|
+
(
|
|
1327
|
+
# `None` means that all selected members have a depth of at least
|
|
1328
|
+
# `i_block + 1`.
|
|
1329
|
+
None
|
|
1330
|
+
if counts[i_block] == pack_size
|
|
1331
|
+
else torch.nonzero_static(mask[i_block], size=counts[i_block])[
|
|
1332
|
+
:, 0
|
|
1333
|
+
].clone()
|
|
1334
|
+
)
|
|
1335
|
+
for i_block in range(self.max_n_blocks)
|
|
1336
|
+
]
|
|
1337
|
+
|
|
1338
|
+
def _get_active_local_pack_idx_by_block(
|
|
1339
|
+
self, pack_idx: Tensor | None
|
|
1340
|
+
) -> list[Tensor | None]:
|
|
1341
|
+
if pack_idx is not None:
|
|
1342
|
+
# The cache is maintained only for the whole pack, since caching the result
|
|
1343
|
+
# for arbitrary `pack_idx` values would require a significantly more complex
|
|
1344
|
+
# caching mechanism. As a result, in depth-varying packs, member indexing
|
|
1345
|
+
# always triggers the host-device synchronization described in the index
|
|
1346
|
+
# computation method.
|
|
1347
|
+
return self._compute_active_local_pack_idx_by_block(pack_idx)
|
|
1348
|
+
|
|
1349
|
+
n_blocks_version = (
|
|
1350
|
+
None
|
|
1351
|
+
if self.n_blocks is None or self.n_blocks.is_inference()
|
|
1352
|
+
else self.n_blocks._version
|
|
1353
|
+
)
|
|
1354
|
+
if (
|
|
1355
|
+
self._active_local_pack_idx_cache is None
|
|
1356
|
+
or self._active_local_pack_idx_cache.n_blocks is not self.n_blocks
|
|
1357
|
+
# Additional protection from in-place modifications of `n_blocks` without
|
|
1358
|
+
# the `n_blocks_index_put_` method. The protection is not absolute, in
|
|
1359
|
+
# particular it does not detect modifications of inference tensors (which
|
|
1360
|
+
# have no version counter) or modifications through `n_blocks.data`.
|
|
1361
|
+
# Any code outside of this method must _not_ rely on this additional
|
|
1362
|
+
# protection and should be written as this protection does not exist.
|
|
1363
|
+
or self._active_local_pack_idx_cache.n_blocks_version != n_blocks_version
|
|
1364
|
+
):
|
|
1365
|
+
self._reset_active_local_pack_idx_cache()
|
|
1366
|
+
self._active_local_pack_idx_cache = (
|
|
1367
|
+
MLPBackbonePack._ActiveLocalPackIdxCache(
|
|
1368
|
+
self.n_blocks,
|
|
1369
|
+
n_blocks_version,
|
|
1370
|
+
self._compute_active_local_pack_idx_by_block(None),
|
|
1371
|
+
)
|
|
1372
|
+
)
|
|
1373
|
+
return self._active_local_pack_idx_cache.value
|
|
1374
|
+
|
|
1375
|
+
def _forward_indexed(self, x: Tensor, pack_idx: Tensor | None = None) -> Tensor:
|
|
1376
|
+
if pack_idx is not None:
|
|
1377
|
+
check_pack_idx(pack_idx, self.pack_size)
|
|
1378
|
+
|
|
1379
|
+
# Without autograd, the in-place version is safe and faster. It never modifies
|
|
1380
|
+
# the input, because all members are active in the first block, which is
|
|
1381
|
+
# explicitly checked by the assertion below.
|
|
1382
|
+
index_copy = (
|
|
1383
|
+
Tensor.index_copy if torch.is_grad_enabled() else Tensor.index_copy_
|
|
1384
|
+
)
|
|
1385
|
+
|
|
1386
|
+
active_local_pack_idx_by_block = self._get_active_local_pack_idx_by_block(
|
|
1387
|
+
pack_idx
|
|
1388
|
+
)
|
|
1389
|
+
assert (
|
|
1390
|
+
active_local_pack_idx_by_block and active_local_pack_idx_by_block[0] is None
|
|
1391
|
+
)
|
|
1392
|
+
|
|
1393
|
+
for block, active_local_pack_idx in zip(
|
|
1394
|
+
self.blocks, active_local_pack_idx_by_block, strict=True
|
|
1395
|
+
):
|
|
1396
|
+
if active_local_pack_idx is None:
|
|
1397
|
+
# All the pack members continue the forward pass.
|
|
1398
|
+
x = block(x, pack_idx)
|
|
1399
|
+
elif active_local_pack_idx.numel() > 0:
|
|
1400
|
+
# Only some of the pack members continue the forward pass. Note that
|
|
1401
|
+
# the computed indices are applied to `x`, i.e. they refer to the
|
|
1402
|
+
# selected members, while the block itself must be indexed with the
|
|
1403
|
+
# corresponding indices in the whole pack.
|
|
1404
|
+
x = index_copy(
|
|
1405
|
+
x,
|
|
1406
|
+
PACK_DIM,
|
|
1407
|
+
active_local_pack_idx,
|
|
1408
|
+
block(
|
|
1409
|
+
x.index_select(PACK_DIM, active_local_pack_idx),
|
|
1410
|
+
(
|
|
1411
|
+
active_local_pack_idx
|
|
1412
|
+
if pack_idx is None
|
|
1413
|
+
else pack_idx[active_local_pack_idx]
|
|
1414
|
+
),
|
|
1415
|
+
),
|
|
1416
|
+
)
|
|
1417
|
+
else:
|
|
1418
|
+
# None of the pack members continue the forward pass.
|
|
1419
|
+
break
|
|
1420
|
+
return x
|
|
1421
|
+
|
|
1422
|
+
def forward(self, x: Tensor, pack_idx: Tensor | None = None) -> Tensor:
|
|
1423
|
+
if self._forward_impl is MLPBackbonePack.ForwardImpl.INDEXED:
|
|
1424
|
+
return self._forward_indexed(x, pack_idx)
|
|
1425
|
+
elif self._forward_impl is MLPBackbonePack.ForwardImpl.DENSE:
|
|
1426
|
+
return self._forward_dense(x, pack_idx)
|
|
1427
|
+
else:
|
|
1428
|
+
raise ValueError(f'Unknown forward implementation: {self._forward_impl}')
|
|
1429
|
+
|
|
1430
|
+
|
|
1431
|
+
class NumericalFeatureEmbeddingsPack(ModulePack, abc.ABC):
|
|
1432
|
+
"""The base class for packs of numerical feature embeddings.
|
|
1433
|
+
|
|
1434
|
+
Each numerical feature is mapped to its own learned vector before entering the main
|
|
1435
|
+
backbone. `d_embedding` controls the vector size per feature.
|
|
1436
|
+
|
|
1437
|
+
In addition to implementing the abstract methods, inherited classes must also
|
|
1438
|
+
accept the following arguments in their constructors:
|
|
1439
|
+
|
|
1440
|
+
- `n_features`
|
|
1441
|
+
- `d_embedding`
|
|
1442
|
+
- `max_d_embedding`
|
|
1443
|
+
- `pack_size`
|
|
1444
|
+
"""
|
|
1445
|
+
|
|
1446
|
+
@abc.abstractmethod
|
|
1447
|
+
def get_output_shape(self) -> torch.Size:
|
|
1448
|
+
"""Get the shape of the embeddings of one object."""
|
|
1449
|
+
...
|
|
1450
|
+
|
|
1451
|
+
|
|
1452
|
+
class LinearEmbeddingsPack(NumericalFeatureEmbeddingsPack):
|
|
1453
|
+
"""A pack of linear feature embeddings."""
|
|
1454
|
+
|
|
1455
|
+
def __init__(
|
|
1456
|
+
self,
|
|
1457
|
+
n_features: int,
|
|
1458
|
+
d_embedding: int | list[int],
|
|
1459
|
+
*,
|
|
1460
|
+
max_d_embedding: int | None = None,
|
|
1461
|
+
pack_size: int,
|
|
1462
|
+
) -> None:
|
|
1463
|
+
d_embedding_buffer, max_d_embedding = _prepare_positive_integer_pack(
|
|
1464
|
+
d_embedding, max_d_embedding, pack_size=pack_size
|
|
1465
|
+
)
|
|
1466
|
+
|
|
1467
|
+
super().__init__(pack_size=pack_size)
|
|
1468
|
+
self.weight = ParameterPack(torch.empty(pack_size, n_features, max_d_embedding))
|
|
1469
|
+
self.bias = ParameterPack(torch.empty(pack_size, n_features, max_d_embedding))
|
|
1470
|
+
self.d_embedding = d_embedding_buffer
|
|
1471
|
+
self.reset_parameters()
|
|
1472
|
+
|
|
1473
|
+
@property
|
|
1474
|
+
def pack_size(self) -> int:
|
|
1475
|
+
return self.weight.shape[PACK_DIM]
|
|
1476
|
+
|
|
1477
|
+
@property
|
|
1478
|
+
def max_d_embedding(self) -> int:
|
|
1479
|
+
"""The maximum supported embedding size."""
|
|
1480
|
+
return self.weight.shape[-1]
|
|
1481
|
+
|
|
1482
|
+
def get_output_shape(self) -> torch.Size:
|
|
1483
|
+
"""Get the shape of the embeddings of one object."""
|
|
1484
|
+
return self.weight.shape[1:] # ty:ignore[invalid-return-type]
|
|
1485
|
+
|
|
1486
|
+
def reset_parameters(self, *, pack_idx: Tensor | None = None) -> None:
|
|
1487
|
+
"""Initialize the parameters."""
|
|
1488
|
+
if pack_idx is not None:
|
|
1489
|
+
check_pack_idx(pack_idx, self.pack_size)
|
|
1490
|
+
|
|
1491
|
+
if self.d_embedding is None:
|
|
1492
|
+
d_rsqrt = self.max_d_embedding**-0.5
|
|
1493
|
+
init_args = (-d_rsqrt, d_rsqrt)
|
|
1494
|
+
|
|
1495
|
+
for p in (self.weight, self.bias):
|
|
1496
|
+
if pack_idx is None:
|
|
1497
|
+
nn.init.uniform_(p, *init_args)
|
|
1498
|
+
else:
|
|
1499
|
+
with torch.no_grad():
|
|
1500
|
+
p[pack_idx] = torch.empty(
|
|
1501
|
+
len(pack_idx), *p.shape[1:], dtype=p.dtype, device=p.device
|
|
1502
|
+
).uniform_(*init_args)
|
|
1503
|
+
|
|
1504
|
+
else:
|
|
1505
|
+
d_embedding = _get_tensor_pack(self.d_embedding, pack_idx)
|
|
1506
|
+
d_rsqrt = d_embedding.float().rsqrt()
|
|
1507
|
+
d_rsqrt_neg = -d_rsqrt
|
|
1508
|
+
|
|
1509
|
+
for p in (self.weight, self.bias):
|
|
1510
|
+
single_shape = p.shape[1:]
|
|
1511
|
+
broadcast_idx = (slice(None), *((None,) * (p.ndim - 1)))
|
|
1512
|
+
p_init = torch.lerp(
|
|
1513
|
+
d_rsqrt_neg[broadcast_idx].expand(-1, *single_shape),
|
|
1514
|
+
d_rsqrt[broadcast_idx].expand(-1, *single_shape),
|
|
1515
|
+
torch.rand(
|
|
1516
|
+
self.pack_size if pack_idx is None else len(pack_idx),
|
|
1517
|
+
*single_shape,
|
|
1518
|
+
dtype=p.dtype,
|
|
1519
|
+
device=p.device,
|
|
1520
|
+
),
|
|
1521
|
+
)
|
|
1522
|
+
with torch.no_grad():
|
|
1523
|
+
if pack_idx is None:
|
|
1524
|
+
p.copy_(p_init)
|
|
1525
|
+
else:
|
|
1526
|
+
p[pack_idx] = p_init
|
|
1527
|
+
|
|
1528
|
+
def replace(
|
|
1529
|
+
self, *, pack_idx: Tensor, d_embedding: list[int] | None = None
|
|
1530
|
+
) -> None:
|
|
1531
|
+
check_pack_idx(pack_idx, self.pack_size)
|
|
1532
|
+
_check_replace_arguments(pack_idx, d_embedding)
|
|
1533
|
+
|
|
1534
|
+
if d_embedding is not None:
|
|
1535
|
+
if not isinstance(self.d_embedding, BufferPack):
|
|
1536
|
+
raise RuntimeError(
|
|
1537
|
+
'Cannot replace embedding sizes that are shared by all pack members'
|
|
1538
|
+
)
|
|
1539
|
+
if not all(0 < x <= self.max_d_embedding for x in d_embedding):
|
|
1540
|
+
raise ValueError(
|
|
1541
|
+
'Embedding sizes must be positive and must not exceed'
|
|
1542
|
+
' `max_d_embedding`'
|
|
1543
|
+
)
|
|
1544
|
+
|
|
1545
|
+
self.d_embedding[pack_idx] = torch.tensor(
|
|
1546
|
+
d_embedding,
|
|
1547
|
+
dtype=self.d_embedding.dtype,
|
|
1548
|
+
device=self.d_embedding.device,
|
|
1549
|
+
)
|
|
1550
|
+
|
|
1551
|
+
# The parameters are reset _after_ updating the hyperparameters, because the
|
|
1552
|
+
# initialization depends on the hyperparameter values.
|
|
1553
|
+
self.reset_parameters(pack_idx=pack_idx)
|
|
1554
|
+
|
|
1555
|
+
def forward(self, x: Tensor, pack_idx: Tensor | None = None) -> Tensor:
|
|
1556
|
+
if x.ndim != 3:
|
|
1557
|
+
raise ValueError('The input must have three dimensions')
|
|
1558
|
+
if x.shape[-1] != self.weight.shape[-2]:
|
|
1559
|
+
raise ValueError(
|
|
1560
|
+
'The number of input features must match the number of features'
|
|
1561
|
+
' configured for the embeddings'
|
|
1562
|
+
)
|
|
1563
|
+
if pack_idx is not None:
|
|
1564
|
+
check_pack_idx(pack_idx, self.pack_size)
|
|
1565
|
+
|
|
1566
|
+
weight = _get_tensor_pack(self.weight, pack_idx)
|
|
1567
|
+
bias = _get_tensor_pack(self.bias, pack_idx)
|
|
1568
|
+
d_embedding = _get_tensor_pack(self.d_embedding, pack_idx)
|
|
1569
|
+
|
|
1570
|
+
if d_embedding is not None:
|
|
1571
|
+
# Masking parameters is significantly faster than masking activations.
|
|
1572
|
+
parameter_mask = (
|
|
1573
|
+
torch.arange(self.max_d_embedding, device=x.device)[None]
|
|
1574
|
+
< d_embedding[:, None]
|
|
1575
|
+
)
|
|
1576
|
+
parameter_mask = parameter_mask.float().unsqueeze(-2)
|
|
1577
|
+
weight = weight * parameter_mask
|
|
1578
|
+
bias = bias * parameter_mask
|
|
1579
|
+
|
|
1580
|
+
return torch.addcmul(
|
|
1581
|
+
bias.unsqueeze(BATCH_DIM), weight.unsqueeze(BATCH_DIM), x[..., None]
|
|
1582
|
+
)
|
|
1583
|
+
|
|
1584
|
+
|
|
1585
|
+
class LinearReLUEmbeddingsPack(NumericalFeatureEmbeddingsPack):
|
|
1586
|
+
"""A pack of linear embeddings followed by the ReLU activation."""
|
|
1587
|
+
|
|
1588
|
+
def __init__(
|
|
1589
|
+
self,
|
|
1590
|
+
n_features: int,
|
|
1591
|
+
d_embedding: int | list[int],
|
|
1592
|
+
*,
|
|
1593
|
+
max_d_embedding: int | None = None,
|
|
1594
|
+
pack_size: int,
|
|
1595
|
+
) -> None:
|
|
1596
|
+
super().__init__(pack_size=pack_size)
|
|
1597
|
+
self.linear = LinearEmbeddingsPack(
|
|
1598
|
+
n_features,
|
|
1599
|
+
d_embedding,
|
|
1600
|
+
max_d_embedding=max_d_embedding,
|
|
1601
|
+
pack_size=pack_size,
|
|
1602
|
+
)
|
|
1603
|
+
self.activation = nn.ReLU()
|
|
1604
|
+
|
|
1605
|
+
@property
|
|
1606
|
+
def pack_size(self) -> int:
|
|
1607
|
+
return self.linear.pack_size
|
|
1608
|
+
|
|
1609
|
+
@property
|
|
1610
|
+
def max_d_embedding(self) -> int:
|
|
1611
|
+
"""The maximum supported embedding size."""
|
|
1612
|
+
return self.linear.max_d_embedding
|
|
1613
|
+
|
|
1614
|
+
def get_output_shape(self) -> torch.Size:
|
|
1615
|
+
"""Get the shape of the embeddings of one object."""
|
|
1616
|
+
return self.linear.get_output_shape()
|
|
1617
|
+
|
|
1618
|
+
def replace(
|
|
1619
|
+
self, *, pack_idx: Tensor, d_embedding: list[int] | None = None
|
|
1620
|
+
) -> None:
|
|
1621
|
+
self.linear.replace(pack_idx=pack_idx, d_embedding=d_embedding)
|
|
1622
|
+
|
|
1623
|
+
def forward(self, x: Tensor, pack_idx: Tensor | None = None) -> Tensor:
|
|
1624
|
+
return self.activation(self.linear(x, pack_idx))
|
|
1625
|
+
|
|
1626
|
+
|
|
1627
|
+
class CosineEmbeddingsPack(NumericalFeatureEmbeddingsPack):
|
|
1628
|
+
"""A pack of cosine feature embeddings.
|
|
1629
|
+
|
|
1630
|
+
These embeddings are proposed in the TabPack paper. They combine learned cosine
|
|
1631
|
+
functions of each numerical feature with the original feature value. `init_scale`
|
|
1632
|
+
controls the initial cosine frequencies.
|
|
1633
|
+
|
|
1634
|
+
**Shape**
|
|
1635
|
+
|
|
1636
|
+
- Input: `(pack_size, batch_size, n_features)`
|
|
1637
|
+
- Output: `(pack_size, batch_size, n_features, self.max_d_embedding)`
|
|
1638
|
+
|
|
1639
|
+
**Examples**
|
|
1640
|
+
|
|
1641
|
+
>>> m = CosineEmbeddingsPack(4, [5, 6], init_scale=[0.01, 0.1], pack_size=2)
|
|
1642
|
+
>>> m.max_d_embedding
|
|
1643
|
+
6
|
|
1644
|
+
>>> x = torch.randn(2, 3, 4)
|
|
1645
|
+
>>> m(x).shape
|
|
1646
|
+
torch.Size([2, 3, 4, 6])
|
|
1647
|
+
"""
|
|
1648
|
+
|
|
1649
|
+
def __init__(
|
|
1650
|
+
self,
|
|
1651
|
+
n_features: int,
|
|
1652
|
+
d_embedding: int | list[int],
|
|
1653
|
+
*,
|
|
1654
|
+
max_d_embedding: int | None = None,
|
|
1655
|
+
init_scale: float | list[float],
|
|
1656
|
+
pack_size: int,
|
|
1657
|
+
) -> None:
|
|
1658
|
+
concat_input = True
|
|
1659
|
+
|
|
1660
|
+
d_embedding_buffer, max_d_embedding = _prepare_positive_integer_pack(
|
|
1661
|
+
d_embedding, max_d_embedding, pack_size=pack_size
|
|
1662
|
+
)
|
|
1663
|
+
|
|
1664
|
+
super().__init__(pack_size=pack_size)
|
|
1665
|
+
self.weight = ParameterPack(
|
|
1666
|
+
torch.empty(pack_size, n_features, max_d_embedding - int(concat_input))
|
|
1667
|
+
)
|
|
1668
|
+
self.bias = ParameterPack(torch.empty(self.weight.shape))
|
|
1669
|
+
self.elementwise_affine_weight = ParameterPack(
|
|
1670
|
+
torch.empty(
|
|
1671
|
+
*self.weight.shape[:-1], self.weight.shape[-1] + int(concat_input)
|
|
1672
|
+
)
|
|
1673
|
+
)
|
|
1674
|
+
self.elementwise_affine_bias = ParameterPack(
|
|
1675
|
+
torch.empty(self.elementwise_affine_weight.shape)
|
|
1676
|
+
)
|
|
1677
|
+
|
|
1678
|
+
self.d_embedding = d_embedding_buffer
|
|
1679
|
+
self.init_scale = (
|
|
1680
|
+
float(init_scale)
|
|
1681
|
+
if isinstance(init_scale, int | float)
|
|
1682
|
+
else BufferPack(torch.tensor([float(x) for x in init_scale]))
|
|
1683
|
+
)
|
|
1684
|
+
|
|
1685
|
+
self._concat_input: Final = concat_input
|
|
1686
|
+
|
|
1687
|
+
self.reset_parameters()
|
|
1688
|
+
|
|
1689
|
+
@property
|
|
1690
|
+
def pack_size(self) -> int:
|
|
1691
|
+
return self.weight.shape[PACK_DIM]
|
|
1692
|
+
|
|
1693
|
+
@property
|
|
1694
|
+
def max_d_embedding(self) -> int:
|
|
1695
|
+
"""The maximum supported embedding size."""
|
|
1696
|
+
return self.weight.shape[-1] + int(self._concat_input)
|
|
1697
|
+
|
|
1698
|
+
def get_output_shape(self) -> torch.Size:
|
|
1699
|
+
"""Get the shape of the embeddings of one object."""
|
|
1700
|
+
return torch.Size(
|
|
1701
|
+
(self.weight.shape[1], self.weight.shape[2] + int(self._concat_input))
|
|
1702
|
+
)
|
|
1703
|
+
|
|
1704
|
+
def reset_parameters(self, *, pack_idx: Tensor | None = None) -> None:
|
|
1705
|
+
"""Initialize the parameters."""
|
|
1706
|
+
if pack_idx is not None:
|
|
1707
|
+
check_pack_idx(pack_idx, self.pack_size)
|
|
1708
|
+
|
|
1709
|
+
bound = 3.0
|
|
1710
|
+
weight = (
|
|
1711
|
+
self.weight
|
|
1712
|
+
if pack_idx is None
|
|
1713
|
+
else torch.empty(
|
|
1714
|
+
len(pack_idx),
|
|
1715
|
+
*self.weight.shape[1:],
|
|
1716
|
+
dtype=self.weight.dtype,
|
|
1717
|
+
device=self.weight.device,
|
|
1718
|
+
)
|
|
1719
|
+
)
|
|
1720
|
+
nn.init.trunc_normal_(weight, a=-bound, b=bound)
|
|
1721
|
+
with torch.no_grad():
|
|
1722
|
+
if isinstance(self.init_scale, int | float):
|
|
1723
|
+
weight *= self.init_scale
|
|
1724
|
+
else:
|
|
1725
|
+
init_scale = _get_tensor_pack(self.init_scale, pack_idx)
|
|
1726
|
+
weight *= init_scale[(slice(None), *((None,) * (weight.ndim - 1)))]
|
|
1727
|
+
if pack_idx is not None:
|
|
1728
|
+
with torch.no_grad():
|
|
1729
|
+
self.weight[pack_idx] = weight
|
|
1730
|
+
|
|
1731
|
+
for p in (
|
|
1732
|
+
self.bias,
|
|
1733
|
+
self.elementwise_affine_weight,
|
|
1734
|
+
self.elementwise_affine_bias,
|
|
1735
|
+
):
|
|
1736
|
+
if pack_idx is None:
|
|
1737
|
+
nn.init.zeros_(p)
|
|
1738
|
+
else:
|
|
1739
|
+
with torch.no_grad():
|
|
1740
|
+
p[pack_idx] = 0.0
|
|
1741
|
+
|
|
1742
|
+
def replace(
|
|
1743
|
+
self,
|
|
1744
|
+
*,
|
|
1745
|
+
pack_idx: Tensor,
|
|
1746
|
+
d_embedding: list[int] | None = None,
|
|
1747
|
+
init_scale: list[float] | None = None,
|
|
1748
|
+
) -> None:
|
|
1749
|
+
check_pack_idx(pack_idx, self.pack_size)
|
|
1750
|
+
_check_replace_arguments(pack_idx, d_embedding, init_scale)
|
|
1751
|
+
|
|
1752
|
+
updates = []
|
|
1753
|
+
if d_embedding is not None:
|
|
1754
|
+
if not isinstance(self.d_embedding, BufferPack):
|
|
1755
|
+
raise RuntimeError(
|
|
1756
|
+
'Cannot replace embedding sizes that are shared by all pack members'
|
|
1757
|
+
)
|
|
1758
|
+
if not all(0 < x <= self.max_d_embedding for x in d_embedding):
|
|
1759
|
+
raise ValueError(
|
|
1760
|
+
'Embedding sizes must be positive and must not exceed'
|
|
1761
|
+
' `max_d_embedding`'
|
|
1762
|
+
)
|
|
1763
|
+
|
|
1764
|
+
# Defer buffer updates until both hyperparameters have been checked.
|
|
1765
|
+
updates.append((self.d_embedding, d_embedding))
|
|
1766
|
+
if init_scale is not None:
|
|
1767
|
+
if not isinstance(self.init_scale, BufferPack):
|
|
1768
|
+
raise RuntimeError(
|
|
1769
|
+
'Cannot replace initialization scales that are shared by all pack'
|
|
1770
|
+
' members'
|
|
1771
|
+
)
|
|
1772
|
+
|
|
1773
|
+
updates.append((self.init_scale, init_scale))
|
|
1774
|
+
for buffer, values in updates:
|
|
1775
|
+
buffer[pack_idx] = torch.tensor(
|
|
1776
|
+
values, dtype=buffer.dtype, device=buffer.device
|
|
1777
|
+
)
|
|
1778
|
+
|
|
1779
|
+
# The parameters are reset _after_ updating the hyperparameters, because the
|
|
1780
|
+
# initialization depends on the hyperparameter values.
|
|
1781
|
+
self.reset_parameters(pack_idx=pack_idx)
|
|
1782
|
+
|
|
1783
|
+
def forward(self, x: Tensor, pack_idx: Tensor | None = None) -> Tensor:
|
|
1784
|
+
if x.ndim != 3:
|
|
1785
|
+
raise ValueError('The input must have three dimensions')
|
|
1786
|
+
if x.shape[-1] != self.weight.shape[-2]:
|
|
1787
|
+
raise ValueError(
|
|
1788
|
+
'The number of input features must match the number of features'
|
|
1789
|
+
' configured for the embeddings'
|
|
1790
|
+
)
|
|
1791
|
+
if pack_idx is not None:
|
|
1792
|
+
check_pack_idx(pack_idx, self.pack_size)
|
|
1793
|
+
|
|
1794
|
+
weight = _get_tensor_pack(self.weight, pack_idx)
|
|
1795
|
+
bias = _get_tensor_pack(self.bias, pack_idx)
|
|
1796
|
+
elementwise_affine_weight = _get_tensor_pack(
|
|
1797
|
+
self.elementwise_affine_weight, pack_idx
|
|
1798
|
+
)
|
|
1799
|
+
elementwise_affine_bias = _get_tensor_pack(
|
|
1800
|
+
self.elementwise_affine_bias, pack_idx
|
|
1801
|
+
)
|
|
1802
|
+
d_embedding = _get_tensor_pack(self.d_embedding, pack_idx)
|
|
1803
|
+
|
|
1804
|
+
if d_embedding is not None:
|
|
1805
|
+
# Masking parameters is significantly faster than masking activations.
|
|
1806
|
+
parameter_mask = (
|
|
1807
|
+
torch.arange(self.max_d_embedding, device=x.device)[None]
|
|
1808
|
+
< d_embedding[:, None]
|
|
1809
|
+
)
|
|
1810
|
+
parameter_mask = parameter_mask.float().unsqueeze(-2)
|
|
1811
|
+
elementwise_affine_weight = elementwise_affine_weight * parameter_mask
|
|
1812
|
+
elementwise_affine_bias = elementwise_affine_bias * parameter_mask
|
|
1813
|
+
|
|
1814
|
+
# Scaling parameters is usually faster than scaling the input, especially for
|
|
1815
|
+
# non-trivial batch sizes.
|
|
1816
|
+
weight = weight * (2 * torch.pi)
|
|
1817
|
+
bias = bias * (2 * torch.pi)
|
|
1818
|
+
|
|
1819
|
+
x_input = x
|
|
1820
|
+
x = torch.addcmul(
|
|
1821
|
+
bias.unsqueeze(BATCH_DIM), weight.unsqueeze(BATCH_DIM), x[..., None]
|
|
1822
|
+
)
|
|
1823
|
+
x = torch.cos(x)
|
|
1824
|
+
if self._concat_input:
|
|
1825
|
+
# The input goes first so it is always included by the width mask.
|
|
1826
|
+
x = torch.cat([x_input[..., None], x], dim=-1)
|
|
1827
|
+
x = torch.addcmul(
|
|
1828
|
+
elementwise_affine_bias.unsqueeze(BATCH_DIM),
|
|
1829
|
+
elementwise_affine_weight.unsqueeze(BATCH_DIM),
|
|
1830
|
+
x,
|
|
1831
|
+
)
|
|
1832
|
+
|
|
1833
|
+
return x
|
|
1834
|
+
|
|
1835
|
+
|
|
1836
|
+
class _OneHotEncoding(nn.Module):
|
|
1837
|
+
"""One-hot encoding of categorical features, with support of unknown categories.
|
|
1838
|
+
|
|
1839
|
+
For a categorical feature with cardinality `C` (the number of distinct values), the
|
|
1840
|
+
feature values must be from `range(C + 1)`, where the value `C` means "unknown"
|
|
1841
|
+
(out-of-vocabulary) category.
|
|
1842
|
+
|
|
1843
|
+
**Shape**
|
|
1844
|
+
|
|
1845
|
+
- Input: `(*, n_categorical_features=len(cardinalities))`.
|
|
1846
|
+
- Output: `(*, sum(cardinalities))`.
|
|
1847
|
+
"""
|
|
1848
|
+
|
|
1849
|
+
def __init__(self, cardinalities: list[int]) -> None:
|
|
1850
|
+
super().__init__()
|
|
1851
|
+
self._cardinalities = cardinalities
|
|
1852
|
+
|
|
1853
|
+
def forward(self, x: Tensor) -> Tensor:
|
|
1854
|
+
if x.ndim == 0:
|
|
1855
|
+
raise ValueError('The categorical input must have at least one dimension')
|
|
1856
|
+
if x.shape[-1] != len(self._cardinalities):
|
|
1857
|
+
raise ValueError(
|
|
1858
|
+
'The input must contain one categorical feature for each entry in'
|
|
1859
|
+
' `cardinalities`'
|
|
1860
|
+
)
|
|
1861
|
+
|
|
1862
|
+
return torch.cat(
|
|
1863
|
+
[
|
|
1864
|
+
# The following produces the standard one-hot encoding for in-vocabulary
|
|
1865
|
+
# values, and all-zeros encoding for out-of-vocabulary values. The
|
|
1866
|
+
# latter requires the unknown categories to be encoded as `cardinality`.
|
|
1867
|
+
nn.functional.one_hot(x[..., i], cardinality + 1)[..., :-1]
|
|
1868
|
+
for i, cardinality in enumerate(self._cardinalities)
|
|
1869
|
+
],
|
|
1870
|
+
-1,
|
|
1871
|
+
)
|
|
1872
|
+
|
|
1873
|
+
|
|
1874
|
+
def _make_num_module(
|
|
1875
|
+
type: type[NumericalFeatureEmbeddingsPack], **kwargs
|
|
1876
|
+
) -> ModulePack:
|
|
1877
|
+
cls = type
|
|
1878
|
+
del type
|
|
1879
|
+
|
|
1880
|
+
return cls(**kwargs)
|
|
1881
|
+
|
|
1882
|
+
|
|
1883
|
+
class TabPack(ModulePack):
|
|
1884
|
+
"""A pack of multilayer perceptrons for tabular data, with optional feature embeddings.
|
|
1885
|
+
|
|
1886
|
+
Args:
|
|
1887
|
+
n_numerical_features: the number of numerical features.
|
|
1888
|
+
categorical_cardinalities: the number of known categories for each categorical
|
|
1889
|
+
feature, in input column order. Use an empty list for numerical-only data.
|
|
1890
|
+
Encode categories as consecutive integers starting at zero; use the
|
|
1891
|
+
cardinality for unknown categories, which receive an all-zero encoding.
|
|
1892
|
+
d_out: the number of raw outputs per model.
|
|
1893
|
+
n_blocks: the number of MLP blocks, shared or per model.
|
|
1894
|
+
d_block: the MLP block width, shared or per model.
|
|
1895
|
+
dropout: the dropout rate, shared or per model.
|
|
1896
|
+
pack_size: the number of models evaluated in parallel.
|
|
1897
|
+
numerical_embeddings: the numerical embedding configuration. The `"type"` entry
|
|
1898
|
+
selects an embedding class, such as `CosineEmbeddingsPack`; the remaining
|
|
1899
|
+
entries are its keyword arguments, including `d_embedding`. The feature
|
|
1900
|
+
count and pack size are supplied automatically. `None` means passing the raw
|
|
1901
|
+
numerical features directly to the MLP backbones.
|
|
1902
|
+
**backbone_kwargs: optional arguments for `MLPBackbonePack`.
|
|
1903
|
+
""" # noqa: E501
|
|
1904
|
+
|
|
1905
|
+
def __init__(
|
|
1906
|
+
self,
|
|
1907
|
+
*,
|
|
1908
|
+
n_numerical_features: int,
|
|
1909
|
+
categorical_cardinalities: list[int],
|
|
1910
|
+
d_out: int,
|
|
1911
|
+
n_blocks: int | list[int],
|
|
1912
|
+
d_block: int | list[int],
|
|
1913
|
+
dropout: float | list[float],
|
|
1914
|
+
pack_size: int,
|
|
1915
|
+
numerical_embeddings: KWArgs | None = None,
|
|
1916
|
+
**backbone_kwargs,
|
|
1917
|
+
) -> None:
|
|
1918
|
+
super().__init__(pack_size=pack_size)
|
|
1919
|
+
|
|
1920
|
+
if numerical_embeddings is None:
|
|
1921
|
+
self.num_module = None
|
|
1922
|
+
# The same representation size of numerical features for all pack members.
|
|
1923
|
+
d_in_num = n_numerical_features
|
|
1924
|
+
max_d_in_num = None
|
|
1925
|
+
else:
|
|
1926
|
+
self.num_module = _make_num_module(
|
|
1927
|
+
n_features=n_numerical_features,
|
|
1928
|
+
pack_size=pack_size,
|
|
1929
|
+
**numerical_embeddings,
|
|
1930
|
+
)
|
|
1931
|
+
d_num_embedding = numerical_embeddings['d_embedding']
|
|
1932
|
+
if isinstance(d_num_embedding, int):
|
|
1933
|
+
# The same representation size of numerical features for all members.
|
|
1934
|
+
d_in_num = d_num_embedding * n_numerical_features
|
|
1935
|
+
max_d_in_num = None
|
|
1936
|
+
else:
|
|
1937
|
+
# The representation size of numerical features varies between members.
|
|
1938
|
+
if not isinstance(d_num_embedding, list):
|
|
1939
|
+
raise TypeError(
|
|
1940
|
+
'The `d_embedding` setting in `numerical_embeddings` must be an'
|
|
1941
|
+
' integer or a list'
|
|
1942
|
+
)
|
|
1943
|
+
|
|
1944
|
+
max_d_num_embedding = numerical_embeddings.get('max_d_embedding')
|
|
1945
|
+
if max_d_num_embedding is None:
|
|
1946
|
+
max_d_num_embedding = max(d_num_embedding)
|
|
1947
|
+
d_in_num = [x * n_numerical_features for x in d_num_embedding]
|
|
1948
|
+
max_d_in_num = max_d_num_embedding * n_numerical_features
|
|
1949
|
+
|
|
1950
|
+
self.cat_module = (
|
|
1951
|
+
_OneHotEncoding(categorical_cardinalities)
|
|
1952
|
+
if categorical_cardinalities
|
|
1953
|
+
else None
|
|
1954
|
+
)
|
|
1955
|
+
d_in_cat = sum(categorical_cardinalities)
|
|
1956
|
+
|
|
1957
|
+
self.pack_view = PackView(pack_size=pack_size)
|
|
1958
|
+
self.backbone = MLPBackbonePack(
|
|
1959
|
+
d_in=(
|
|
1960
|
+
d_in_num + d_in_cat
|
|
1961
|
+
if isinstance(d_in_num, int)
|
|
1962
|
+
else [x + d_in_cat for x in d_in_num]
|
|
1963
|
+
),
|
|
1964
|
+
max_d_in=None if max_d_in_num is None else max_d_in_num + d_in_cat,
|
|
1965
|
+
n_blocks=n_blocks,
|
|
1966
|
+
d_block=d_block,
|
|
1967
|
+
dropout=dropout,
|
|
1968
|
+
pack_size=pack_size,
|
|
1969
|
+
**backbone_kwargs,
|
|
1970
|
+
)
|
|
1971
|
+
self.output = LinearPack(
|
|
1972
|
+
d_block,
|
|
1973
|
+
d_out,
|
|
1974
|
+
max_in_features=backbone_kwargs.get('max_d_block'),
|
|
1975
|
+
pack_size=pack_size,
|
|
1976
|
+
)
|
|
1977
|
+
|
|
1978
|
+
self._n_numerical_features = n_numerical_features
|
|
1979
|
+
self._categorical_cardinalities = categorical_cardinalities
|
|
1980
|
+
|
|
1981
|
+
@property
|
|
1982
|
+
def pack_size(self) -> int:
|
|
1983
|
+
return self.backbone.pack_size
|
|
1984
|
+
|
|
1985
|
+
def replace(
|
|
1986
|
+
self,
|
|
1987
|
+
*,
|
|
1988
|
+
pack_idx: Tensor,
|
|
1989
|
+
numerical_embeddings: KWArgs | None = None,
|
|
1990
|
+
d_block: list[int] | None = None,
|
|
1991
|
+
**kwargs,
|
|
1992
|
+
) -> None:
|
|
1993
|
+
if numerical_embeddings is not None and self.num_module is None:
|
|
1994
|
+
raise RuntimeError(
|
|
1995
|
+
'Cannot replace numerical embeddings because this model has none'
|
|
1996
|
+
)
|
|
1997
|
+
|
|
1998
|
+
if self.num_module is not None:
|
|
1999
|
+
self.num_module.replace(
|
|
2000
|
+
pack_idx=pack_idx,
|
|
2001
|
+
**({} if numerical_embeddings is None else numerical_embeddings),
|
|
2002
|
+
) # ty: ignore[call-non-callable]
|
|
2003
|
+
|
|
2004
|
+
d_num_embedding = (
|
|
2005
|
+
None
|
|
2006
|
+
if numerical_embeddings is None
|
|
2007
|
+
else numerical_embeddings.get('d_embedding')
|
|
2008
|
+
)
|
|
2009
|
+
self.backbone.replace(
|
|
2010
|
+
pack_idx=pack_idx,
|
|
2011
|
+
d_in=(
|
|
2012
|
+
None
|
|
2013
|
+
if d_num_embedding is None
|
|
2014
|
+
else [
|
|
2015
|
+
x * self._n_numerical_features
|
|
2016
|
+
+ sum(self._categorical_cardinalities)
|
|
2017
|
+
for x in d_num_embedding
|
|
2018
|
+
]
|
|
2019
|
+
),
|
|
2020
|
+
d_block=d_block,
|
|
2021
|
+
**kwargs,
|
|
2022
|
+
)
|
|
2023
|
+
|
|
2024
|
+
self.output.replace(pack_idx=pack_idx, in_features=d_block)
|
|
2025
|
+
|
|
2026
|
+
def forward(
|
|
2027
|
+
self, x_num: Tensor | None, x_cat: Tensor | None, pack_idx: Tensor | None = None
|
|
2028
|
+
) -> Tensor:
|
|
2029
|
+
"""Return raw outputs for each model, with a leading model dimension.
|
|
2030
|
+
|
|
2031
|
+
Pass a shared batch of features, or a separate batch per model during training.
|
|
2032
|
+
Use `None` for a feature group that was not configured.
|
|
2033
|
+
"""
|
|
2034
|
+
if x_num is None and x_cat is None:
|
|
2035
|
+
raise ValueError('Provide at least one of `x_num` and `x_cat`')
|
|
2036
|
+
if pack_idx is not None:
|
|
2037
|
+
check_pack_idx(pack_idx, self.pack_size)
|
|
2038
|
+
|
|
2039
|
+
x_list: list[Tensor] = []
|
|
2040
|
+
|
|
2041
|
+
# For models without `num_module`, when the input batch is shared between pack
|
|
2042
|
+
# members (e.g., on inference) the input representations passed to the MLP
|
|
2043
|
+
# backbones are identical for all pack members, which makes it possible to pass
|
|
2044
|
+
# a repeated view of this representation to the backbones instead of a pack of
|
|
2045
|
+
# true representation copies. Technically, this is achieved by deferring the
|
|
2046
|
+
# use of the `pack_view` module.
|
|
2047
|
+
pack_view_used = False
|
|
2048
|
+
|
|
2049
|
+
if x_num is None:
|
|
2050
|
+
if self._n_numerical_features != 0:
|
|
2051
|
+
raise ValueError(
|
|
2052
|
+
'`x_num` is required when numerical features are configured'
|
|
2053
|
+
)
|
|
2054
|
+
if self.num_module is not None:
|
|
2055
|
+
raise ValueError(
|
|
2056
|
+
'`x_num` is required when numerical embeddings are configured'
|
|
2057
|
+
)
|
|
2058
|
+
else:
|
|
2059
|
+
if self._n_numerical_features <= 0:
|
|
2060
|
+
raise ValueError(
|
|
2061
|
+
'`x_num` can only be provided when numerical features are'
|
|
2062
|
+
' configured'
|
|
2063
|
+
)
|
|
2064
|
+
|
|
2065
|
+
if self.num_module is None:
|
|
2066
|
+
x_list.append(x_num)
|
|
2067
|
+
else:
|
|
2068
|
+
x_num_ = self.pack_view(x_num, pack_idx)
|
|
2069
|
+
pack_view_used = True
|
|
2070
|
+
x_num_ = self.num_module(x_num_, pack_idx)
|
|
2071
|
+
x_list.append(x_num_.flatten(-2))
|
|
2072
|
+
del x_num_
|
|
2073
|
+
|
|
2074
|
+
if x_cat is None:
|
|
2075
|
+
if self.cat_module is not None:
|
|
2076
|
+
raise ValueError(
|
|
2077
|
+
'`x_cat` is required when categorical features are configured'
|
|
2078
|
+
)
|
|
2079
|
+
else:
|
|
2080
|
+
if self.cat_module is None:
|
|
2081
|
+
raise ValueError(
|
|
2082
|
+
'`x_cat` can only be provided when categorical features are'
|
|
2083
|
+
' configured'
|
|
2084
|
+
)
|
|
2085
|
+
|
|
2086
|
+
x_cat_ = self.cat_module(x_cat).to(torch.get_default_dtype())
|
|
2087
|
+
if pack_view_used:
|
|
2088
|
+
x_cat_ = self.pack_view(x_cat_, pack_idx)
|
|
2089
|
+
x_list.append(x_cat_)
|
|
2090
|
+
del x_cat_
|
|
2091
|
+
|
|
2092
|
+
if len(x_list) > 1:
|
|
2093
|
+
if (
|
|
2094
|
+
x_list[0].device.type == 'mps'
|
|
2095
|
+
and _TORCH_IS_OLDER_THAN_2_15
|
|
2096
|
+
and sum(x.numel() for x in x_list) > 2**31
|
|
2097
|
+
):
|
|
2098
|
+
raise RuntimeError(
|
|
2099
|
+
'MPS concatenation output larger than INT_MAX is not supported'
|
|
2100
|
+
' with PyTorch < 2.15'
|
|
2101
|
+
)
|
|
2102
|
+
x = torch.cat(x_list, dim=-1)
|
|
2103
|
+
else:
|
|
2104
|
+
x = x_list[0]
|
|
2105
|
+
|
|
2106
|
+
if not pack_view_used:
|
|
2107
|
+
x = self.pack_view(x, pack_idx)
|
|
2108
|
+
x = self.backbone(x, pack_idx)
|
|
2109
|
+
x = self.output(x, pack_idx)
|
|
2110
|
+
return x
|