portc 0.6.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.
- portc/__init__.py +29 -0
- portc/_group.py +295 -0
- portc/_kernels.py +199 -0
- portc/_plan.py +644 -0
- portc/_sites.py +315 -0
- portc/_store.py +355 -0
- portc/_sweep.py +589 -0
- portc/_tensor_train.py +429 -0
- portc/_validation.py +156 -0
- portc/_version.py +24 -0
- portc/apply.py +108 -0
- portc/compress.py +92 -0
- portc/py.typed +0 -0
- portc/stack.py +168 -0
- portc/utils/__init__.py +65 -0
- portc/utils/_backend.py +486 -0
- portc/utils/linalg.py +138 -0
- portc-0.6.0.dist-info/METADATA +238 -0
- portc-0.6.0.dist-info/RECORD +22 -0
- portc-0.6.0.dist-info/WHEEL +4 -0
- portc-0.6.0.dist-info/licenses/LICENSE +202 -0
- portc-0.6.0.dist-info/licenses/NOTICE +16 -0
portc/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Copyright (c) 2025 Algorithmiq Development Team. All rights reserved.
|
|
2
|
+
|
|
3
|
+
portc: Parallel Out-of-Core Randomized Tensor-Network Compression.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from ._plan import Resources
|
|
11
|
+
from ._tensor_train import SiteLike
|
|
12
|
+
from ._version import version as __version__
|
|
13
|
+
from ._version import version_tuple as __version_tuple__
|
|
14
|
+
from .apply import apply
|
|
15
|
+
from .compress import compress
|
|
16
|
+
from .stack import src
|
|
17
|
+
|
|
18
|
+
# Stay silent unless the application configures logging.
|
|
19
|
+
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"Resources",
|
|
23
|
+
"SiteLike",
|
|
24
|
+
"__version__",
|
|
25
|
+
"__version_tuple__",
|
|
26
|
+
"apply",
|
|
27
|
+
"compress",
|
|
28
|
+
"src",
|
|
29
|
+
]
|
portc/_group.py
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""The devices of one sweep, one thread per device, and the collectives between them.
|
|
2
|
+
|
|
3
|
+
A sweep over several devices of a node runs in one process: `DeviceGroup.run`
|
|
4
|
+
starts one thread per device, each running the same per-device function on its
|
|
5
|
+
*rank* (its position in the group). Collectives are called by every rank in the
|
|
6
|
+
same order. They exchange device arrays through peer copies, ordered with the
|
|
7
|
+
kernels by events. A rank that fails aborts the group: every other rank waiting in
|
|
8
|
+
a collective, or for an input core, raises `GroupAbortedError`, and `run` re-raises the
|
|
9
|
+
original error.
|
|
10
|
+
|
|
11
|
+
On the host backend the devices are simulated: the threads share the host, and the
|
|
12
|
+
collectives are plain copies. The result is that of a single device; this mode
|
|
13
|
+
exists to test the distributed logic without GPUs.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import threading
|
|
19
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
20
|
+
from typing import TYPE_CHECKING, Any, NamedTuple, TypeVar
|
|
21
|
+
|
|
22
|
+
from ._plan import device_ids
|
|
23
|
+
from .utils import (
|
|
24
|
+
copy_into,
|
|
25
|
+
current_device,
|
|
26
|
+
current_stream,
|
|
27
|
+
device_count,
|
|
28
|
+
enable_peer_access,
|
|
29
|
+
get_xp,
|
|
30
|
+
use_device,
|
|
31
|
+
use_stream,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from collections.abc import Callable, Sequence
|
|
36
|
+
|
|
37
|
+
from .utils import Array
|
|
38
|
+
|
|
39
|
+
T = TypeVar("T")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class GroupAbortedError(RuntimeError):
|
|
43
|
+
"""Raised on the ranks of a group after another rank failed."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def blocks(n: int, size: int) -> list[tuple[int, int]]:
|
|
47
|
+
"""Split ``range(n)`` into ``size`` contiguous ``(lo, hi)`` blocks, in rank order.
|
|
48
|
+
|
|
49
|
+
Block sizes differ by at most one, the larger blocks first. Blocks may be empty
|
|
50
|
+
when ``n < size``.
|
|
51
|
+
"""
|
|
52
|
+
base, extra = divmod(n, size)
|
|
53
|
+
out, lo = [], 0
|
|
54
|
+
for rank in range(size):
|
|
55
|
+
hi = lo + base + (rank < extra)
|
|
56
|
+
out.append((lo, hi))
|
|
57
|
+
lo = hi
|
|
58
|
+
return out
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class _Part(NamedTuple):
|
|
62
|
+
"""What a rank publishes in a collective: an array and the event it is ready at."""
|
|
63
|
+
|
|
64
|
+
array: Any
|
|
65
|
+
ready: Any
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class DeviceGroup:
|
|
69
|
+
"""The devices of one sweep and the collectives between them.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
device: ``"cpu"`` or ``"gpu"``, as in `portc.stack.src`.
|
|
73
|
+
devices: A count ``n`` for the first ``n`` visible devices, or the device
|
|
74
|
+
ids in rank order; rank 0 is the root. Defaults to the current device.
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
ValueError: If a device does not exist.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(self, device: str, devices: int | Sequence[int] | None = None) -> None:
|
|
81
|
+
self.xp = get_xp(device)
|
|
82
|
+
ids = device_ids(devices) or (current_device(self.xp),)
|
|
83
|
+
count = device_count(self.xp)
|
|
84
|
+
if count is not None and max(ids) >= count:
|
|
85
|
+
msg = f"Requested devices {list(ids)}, but only {count} are visible."
|
|
86
|
+
raise ValueError(msg)
|
|
87
|
+
self.devices = ids
|
|
88
|
+
self.size = len(self.devices)
|
|
89
|
+
# Uploading 1/size of each core and gathering the rest pays off only when
|
|
90
|
+
# the devices copy directly between them.
|
|
91
|
+
self.split_uploads = self.size > 1 and enable_peer_access(self.xp, ids)
|
|
92
|
+
self._barrier = threading.Barrier(self.size)
|
|
93
|
+
# Two sets of slots, alternating between consecutive collectives: a rank may
|
|
94
|
+
# publish into the next collective while slower ranks still read this one.
|
|
95
|
+
self._parts: list[list[Any]] = [[None] * self.size, [None] * self.size]
|
|
96
|
+
self._done: list[list[Any]] = [[None] * self.size, [None] * self.size]
|
|
97
|
+
self._calls = [0] * self.size
|
|
98
|
+
self._on_abort: list[Callable[[], None]] = []
|
|
99
|
+
self.aborted = threading.Event()
|
|
100
|
+
|
|
101
|
+
def on_abort(self, callback: Callable[[], None]) -> None:
|
|
102
|
+
"""Call ``callback`` when a rank fails, to release threads outside the ranks."""
|
|
103
|
+
self._on_abort.append(callback)
|
|
104
|
+
|
|
105
|
+
def abort(self) -> None:
|
|
106
|
+
"""Wake every rank waiting in a collective or on a registered resource."""
|
|
107
|
+
if self.aborted.is_set():
|
|
108
|
+
return
|
|
109
|
+
self.aborted.set()
|
|
110
|
+
self._barrier.abort()
|
|
111
|
+
for callback in self._on_abort:
|
|
112
|
+
callback()
|
|
113
|
+
|
|
114
|
+
def run(self, fn: Callable[[int], T]) -> list[T]:
|
|
115
|
+
"""Run ``fn(rank)`` on every device, each in its own thread.
|
|
116
|
+
|
|
117
|
+
A group of one runs ``fn(0)`` in the calling thread.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
fn: The per-device function.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
The results, in rank order.
|
|
124
|
+
|
|
125
|
+
Raises:
|
|
126
|
+
Exception: The first error raised by a rank, after every rank stopped.
|
|
127
|
+
"""
|
|
128
|
+
if self.size == 1:
|
|
129
|
+
with use_device(self.xp, self.devices[0]), use_stream(self.xp):
|
|
130
|
+
return [fn(0)]
|
|
131
|
+
first: list[BaseException] = []
|
|
132
|
+
lock = threading.Lock()
|
|
133
|
+
|
|
134
|
+
def worker(rank: int) -> T:
|
|
135
|
+
try:
|
|
136
|
+
with use_device(self.xp, self.devices[rank]), use_stream(self.xp):
|
|
137
|
+
return fn(rank)
|
|
138
|
+
except BaseException as err:
|
|
139
|
+
secondary = isinstance(
|
|
140
|
+
err, GroupAbortedError | threading.BrokenBarrierError
|
|
141
|
+
)
|
|
142
|
+
with lock:
|
|
143
|
+
if not first and not secondary:
|
|
144
|
+
first.append(err)
|
|
145
|
+
self.abort()
|
|
146
|
+
raise
|
|
147
|
+
|
|
148
|
+
with ThreadPoolExecutor(self.size, thread_name_prefix="src-device") as pool:
|
|
149
|
+
futures = [pool.submit(worker, rank) for rank in range(self.size)]
|
|
150
|
+
results, errors = [], []
|
|
151
|
+
for future in futures:
|
|
152
|
+
try:
|
|
153
|
+
results.append(future.result())
|
|
154
|
+
except BaseException as err: # noqa: BLE001 (re-raised below)
|
|
155
|
+
errors.append(err)
|
|
156
|
+
if first:
|
|
157
|
+
raise first[0]
|
|
158
|
+
if errors:
|
|
159
|
+
raise errors[0]
|
|
160
|
+
return results
|
|
161
|
+
|
|
162
|
+
# --- collectives ---
|
|
163
|
+
|
|
164
|
+
def _wait(self) -> None:
|
|
165
|
+
try:
|
|
166
|
+
self._barrier.wait()
|
|
167
|
+
except threading.BrokenBarrierError:
|
|
168
|
+
raise GroupAbortedError from None
|
|
169
|
+
|
|
170
|
+
def _publish(self, rank: int, array: Any) -> tuple[int, list[_Part]]: # noqa: ANN401
|
|
171
|
+
"""Publish ``array`` and return the parts of every rank."""
|
|
172
|
+
parity = self._calls[rank] % 2
|
|
173
|
+
self._calls[rank] += 1
|
|
174
|
+
self._parts[parity][rank] = _Part(array, current_stream(self.xp).record())
|
|
175
|
+
self._wait()
|
|
176
|
+
return parity, list(self._parts[parity])
|
|
177
|
+
|
|
178
|
+
def _settle(self, rank: int, parity: int) -> None:
|
|
179
|
+
"""Wait, on this rank's stream, until every rank has read what it published.
|
|
180
|
+
|
|
181
|
+
Until then the arrays published by this rank must not be overwritten; the
|
|
182
|
+
memory pool reuses freed blocks in stream order, so making the stream wait
|
|
183
|
+
is enough.
|
|
184
|
+
"""
|
|
185
|
+
stream = current_stream(self.xp)
|
|
186
|
+
self._done[parity][rank] = stream.record()
|
|
187
|
+
self._wait()
|
|
188
|
+
for event in self._done[parity]:
|
|
189
|
+
stream.wait_event(event)
|
|
190
|
+
self._parts[parity][rank] = None
|
|
191
|
+
|
|
192
|
+
def _pull(self, part: _Part, dst: Array) -> None:
|
|
193
|
+
stream = current_stream(self.xp)
|
|
194
|
+
stream.wait_event(part.ready)
|
|
195
|
+
copy_into(dst, part.array, stream)
|
|
196
|
+
|
|
197
|
+
def allgather(self, rank: int, local: Array) -> Array:
|
|
198
|
+
"""Concatenate the blocks of every rank along the first axis, on every rank.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
rank: The calling rank.
|
|
202
|
+
local: This rank's block, C-contiguous on its device; the blocks may
|
|
203
|
+
differ along the first axis only.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
The concatenation in rank order, on this rank's device.
|
|
207
|
+
"""
|
|
208
|
+
if self.size == 1:
|
|
209
|
+
return local
|
|
210
|
+
parity, parts = self._publish(rank, local)
|
|
211
|
+
full = self._empty_like_parts(parts)
|
|
212
|
+
lo = 0
|
|
213
|
+
for part in parts:
|
|
214
|
+
hi = lo + part.array.shape[0]
|
|
215
|
+
self._pull(part, full[lo:hi])
|
|
216
|
+
lo = hi
|
|
217
|
+
self._settle(rank, parity)
|
|
218
|
+
return full
|
|
219
|
+
|
|
220
|
+
def gather(self, rank: int, local: Array, root: int = 0) -> Array | None:
|
|
221
|
+
"""Concatenate the blocks of every rank along the first axis, on ``root``.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
rank: The calling rank.
|
|
225
|
+
local: This rank's block, C-contiguous on its device.
|
|
226
|
+
root: The rank that receives the result.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
The concatenation on ``root``, ``None`` on the other ranks.
|
|
230
|
+
"""
|
|
231
|
+
if self.size == 1:
|
|
232
|
+
return local
|
|
233
|
+
parity, parts = self._publish(rank, local)
|
|
234
|
+
full = None
|
|
235
|
+
if rank == root:
|
|
236
|
+
full = self._empty_like_parts(parts)
|
|
237
|
+
lo = 0
|
|
238
|
+
for part in parts:
|
|
239
|
+
hi = lo + part.array.shape[0]
|
|
240
|
+
self._pull(part, full[lo:hi])
|
|
241
|
+
lo = hi
|
|
242
|
+
self._settle(rank, parity)
|
|
243
|
+
return full
|
|
244
|
+
|
|
245
|
+
def scatter(self, rank: int, full: Array | None, root: int = 0) -> Array:
|
|
246
|
+
"""Give every rank its block (see `blocks`) of the first axis of ``full``.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
rank: The calling rank.
|
|
250
|
+
full: The array to split, C-contiguous on ``root``; ignored elsewhere.
|
|
251
|
+
root: The rank that holds ``full``.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
This rank's block, on its device.
|
|
255
|
+
"""
|
|
256
|
+
if self.size == 1:
|
|
257
|
+
assert full is not None # noqa: S101 (the root holds it)
|
|
258
|
+
return full
|
|
259
|
+
parity, parts = self._publish(rank, full if rank == root else None)
|
|
260
|
+
source = parts[root].array
|
|
261
|
+
lo, hi = blocks(source.shape[0], self.size)[rank]
|
|
262
|
+
local = self.xp.empty((hi - lo, *source.shape[1:]), dtype=source.dtype)
|
|
263
|
+
self._pull(_Part(source[lo:hi], parts[root].ready), local)
|
|
264
|
+
self._settle(rank, parity)
|
|
265
|
+
return local
|
|
266
|
+
|
|
267
|
+
def alltoall(self, rank: int, sends: Sequence[Array]) -> Array:
|
|
268
|
+
"""Send ``sends[s]`` to rank ``s``; concatenate what every rank sent here.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
rank: The calling rank.
|
|
272
|
+
sends: One C-contiguous block per rank, on this rank's device; the
|
|
273
|
+
blocks sent to one rank may differ along the first axis only.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
The blocks sent to this rank, concatenated along the first axis in rank
|
|
277
|
+
order, on this rank's device.
|
|
278
|
+
"""
|
|
279
|
+
if self.size == 1:
|
|
280
|
+
return sends[0]
|
|
281
|
+
parity, parts = self._publish(rank, list(sends))
|
|
282
|
+
mine = [_Part(part.array[rank], part.ready) for part in parts]
|
|
283
|
+
full = self._empty_like_parts(mine)
|
|
284
|
+
lo = 0
|
|
285
|
+
for part in mine:
|
|
286
|
+
hi = lo + part.array.shape[0]
|
|
287
|
+
self._pull(part, full[lo:hi])
|
|
288
|
+
lo = hi
|
|
289
|
+
self._settle(rank, parity)
|
|
290
|
+
return full
|
|
291
|
+
|
|
292
|
+
def _empty_like_parts(self, parts: Sequence[_Part]) -> Array:
|
|
293
|
+
first = parts[0].array
|
|
294
|
+
rows = sum(part.array.shape[0] for part in parts)
|
|
295
|
+
return self.xp.empty((rows, *first.shape[1:]), dtype=first.dtype)
|
portc/_kernels.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Site kernels of the SRC sweep and the memory they need.
|
|
2
|
+
|
|
3
|
+
Every kernel is one of the contractions of the sweep restricted to a batch: a slice
|
|
4
|
+
of the sketch index for the environments and the sketch, a slice of the rows of
|
|
5
|
+
the new projected environment otherwise. Batching never changes the result beyond
|
|
6
|
+
rounding, because the sketch index and those rows are free indices of their
|
|
7
|
+
contractions.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from functools import cache
|
|
13
|
+
from itertools import count
|
|
14
|
+
from math import prod
|
|
15
|
+
from typing import TYPE_CHECKING, NamedTuple, cast
|
|
16
|
+
|
|
17
|
+
import opt_einsum as oe
|
|
18
|
+
from opt_einsum import contract_expression, get_symbol
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from collections.abc import Sequence
|
|
22
|
+
|
|
23
|
+
from opt_einsum.contract import ContractExpression
|
|
24
|
+
from opt_einsum.paths import PathOptimizer
|
|
25
|
+
from opt_einsum.typing import OptimizeKind
|
|
26
|
+
|
|
27
|
+
from .utils import Array
|
|
28
|
+
|
|
29
|
+
Shape = tuple[int, ...]
|
|
30
|
+
|
|
31
|
+
# The default path optimizer of the kernels. ``dp`` finds the cheapest path: on an
|
|
32
|
+
# A100 it halved the projection time against ``auto``, which at large bonds also
|
|
33
|
+
# plans an intermediate too big for any batch above 16 rows.
|
|
34
|
+
DEFAULT_OPTIMIZE = "dp"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Equations(NamedTuple):
|
|
38
|
+
"""The einsum equations of one sweep, for a fixed stack depth."""
|
|
39
|
+
|
|
40
|
+
ltr: str
|
|
41
|
+
rtl_m: str
|
|
42
|
+
rtl_s: str
|
|
43
|
+
first: str
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@cache
|
|
47
|
+
def equations(depth: int) -> Equations:
|
|
48
|
+
"""Generate the sweep equations for a stack of ``depth`` layers.
|
|
49
|
+
|
|
50
|
+
Layer ``i`` at a site carries ``(a_i, b_i, x_i, x_{i+1})``: left and right
|
|
51
|
+
bonds, then its upper and lower physical legs, so that consecutive layers share
|
|
52
|
+
``x``. The output legs are ``x_0`` (up) and ``x_depth`` (down).
|
|
53
|
+
"""
|
|
54
|
+
symbols = map(get_symbol, count())
|
|
55
|
+
sketch, eta_right, eta_left = next(symbols), next(symbols), next(symbols)
|
|
56
|
+
left = "".join(next(symbols) for _ in range(depth))
|
|
57
|
+
right = "".join(next(symbols) for _ in range(depth))
|
|
58
|
+
phys = [next(symbols) for _ in range(depth + 1)]
|
|
59
|
+
up, down = phys[0], phys[-1]
|
|
60
|
+
layers = ",".join(
|
|
61
|
+
f"{left[i]}{right[i]}{phys[i]}{phys[i + 1]}" for i in range(depth)
|
|
62
|
+
)
|
|
63
|
+
return Equations(
|
|
64
|
+
ltr=f"{sketch}{left},{sketch}{up}{down},{layers}->{sketch}{right}",
|
|
65
|
+
rtl_m=f"{sketch}{left},{layers},{eta_right}{right}->{eta_right}{up}{down}{sketch}",
|
|
66
|
+
rtl_s=f"{eta_left}{eta_right}{up}{down},{layers},{eta_right}{right}->{eta_left}{left}",
|
|
67
|
+
first=f"{layers},{eta_right}{right}->{left}{eta_right}{up}{down}",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class _Contractions:
|
|
72
|
+
"""Compiled contractions for one sweep, keyed on equation and operand shapes.
|
|
73
|
+
|
|
74
|
+
Uniform bulk sites share one entry, so the path is planned once rather than at
|
|
75
|
+
every site. Kept per call: jagged bonds and short last batches add entries that
|
|
76
|
+
are not worth keeping.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
optimize: The opt_einsum path optimizer.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(self, optimize: str | PathOptimizer) -> None:
|
|
83
|
+
self._optimize = cast("OptimizeKind", optimize)
|
|
84
|
+
self._compiled: dict[tuple[str, tuple[Shape, ...]], ContractExpression] = {}
|
|
85
|
+
|
|
86
|
+
def __call__(self, eq: str, *operands: Array) -> Array:
|
|
87
|
+
shapes = tuple(op.shape for op in operands)
|
|
88
|
+
expr = self._compiled.get((eq, shapes))
|
|
89
|
+
if expr is None:
|
|
90
|
+
expr = self._compiled[eq, shapes] = contract_expression(
|
|
91
|
+
eq, *shapes, optimize=self._optimize
|
|
92
|
+
)
|
|
93
|
+
return expr(*operands)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class SiteKernels:
|
|
97
|
+
"""The four contractions of the sweep, each applied to one batch.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
depth: The number of layers of the stack.
|
|
101
|
+
optimize: The opt_einsum path optimizer, a name or a ``PathOptimizer``.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(
|
|
105
|
+
self, depth: int, optimize: str | PathOptimizer = DEFAULT_OPTIMIZE
|
|
106
|
+
) -> None:
|
|
107
|
+
self.eqs = equations(depth)
|
|
108
|
+
self._contract = _Contractions(optimize)
|
|
109
|
+
|
|
110
|
+
def env(self, env: Array, omega: Array, cores: Sequence[Array]) -> Array:
|
|
111
|
+
"""Advance a batch of sketch columns of the environment by one site.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
env: Columns ``lo:hi`` of ``C_j``, shape ``(b, *left_bonds)``.
|
|
115
|
+
omega: The same columns of the site's Gaussian tensor, ``(b, up, down)``.
|
|
116
|
+
cores: The padded cores of the site.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Columns ``lo:hi`` of ``C_{j+1}``, shape ``(b, *right_bonds)``.
|
|
120
|
+
"""
|
|
121
|
+
return self._contract(self.eqs.ltr, env, omega, *cores)
|
|
122
|
+
|
|
123
|
+
def sketch(self, env: Array, cores: Sequence[Array], proj: Array) -> Array:
|
|
124
|
+
"""Sketch a batch of columns of the site's running core.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
env: Columns ``lo:hi`` of ``C_j``, shape ``(b, *left_bonds)``.
|
|
128
|
+
cores: The padded cores of the site.
|
|
129
|
+
proj: The projected environment ``S``, ``(eta, *right_bonds)``.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Columns ``lo:hi`` of the sketch, shape ``(eta, up, down, b)``.
|
|
133
|
+
"""
|
|
134
|
+
return self._contract(self.eqs.rtl_m, env, *cores, proj)
|
|
135
|
+
|
|
136
|
+
def project(self, eta: Array, cores: Sequence[Array], proj: Array) -> Array:
|
|
137
|
+
"""Project a batch of rows of the new projected environment.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
eta: Rows ``lo:hi`` of the conjugated output core, ``(b, eta, up, down)``.
|
|
141
|
+
cores: The padded cores of the site.
|
|
142
|
+
proj: The projected environment ``S``, ``(eta, *right_bonds)``.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Rows ``lo:hi`` of the new ``S``, shape ``(b, *left_bonds)``.
|
|
146
|
+
"""
|
|
147
|
+
return self._contract(self.eqs.rtl_s, eta, *cores, proj)
|
|
148
|
+
|
|
149
|
+
def first(self, cores: Sequence[Array], proj: Array) -> Array:
|
|
150
|
+
"""Contract the first site with a batch of rows of ``S``.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
cores: The padded cores of the first site.
|
|
154
|
+
proj: Rows ``lo:hi`` of ``S``, ``(b, *right_bonds)``.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
The output core for those rows, ``(*left_bonds, b, up, down)``.
|
|
158
|
+
"""
|
|
159
|
+
return self._contract(self.eqs.first, *cores, proj)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@cache
|
|
163
|
+
def peak_elements(
|
|
164
|
+
eq: str,
|
|
165
|
+
shapes: tuple[Shape, ...],
|
|
166
|
+
optimize: str | PathOptimizer = DEFAULT_OPTIMIZE,
|
|
167
|
+
) -> int:
|
|
168
|
+
"""Estimate the peak elements a contraction allocates beyond its inputs.
|
|
169
|
+
|
|
170
|
+
Walks the path `opt_einsum` picks for these shapes with ``optimize``, the one
|
|
171
|
+
`SiteKernels` runs with it.
|
|
172
|
+
Each pairwise step holds the intermediates still alive, its output and a
|
|
173
|
+
possible contiguous copy of both operands (``tensordot`` transposes them), so
|
|
174
|
+
the estimate errs on the high side. The final output is included, the inputs
|
|
175
|
+
are not.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
eq: The einsum equation.
|
|
179
|
+
shapes: The operand shapes.
|
|
180
|
+
optimize: The opt_einsum path optimizer.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
The peak number of elements.
|
|
184
|
+
"""
|
|
185
|
+
_, info = oe.contract_path(
|
|
186
|
+
eq, *shapes, shapes=True, optimize=cast("OptimizeKind", optimize)
|
|
187
|
+
)
|
|
188
|
+
sizes = info.size_dict
|
|
189
|
+
# (elements, is_intermediate) for every operand still to be contracted.
|
|
190
|
+
operands = [(prod(shape), False) for shape in shapes]
|
|
191
|
+
peak = 0
|
|
192
|
+
for step in info.contraction_list:
|
|
193
|
+
positions, einsum_str = step[0], step[2]
|
|
194
|
+
live = sum(n for n, is_tmp in operands if is_tmp)
|
|
195
|
+
popped = [operands.pop(i) for i in positions]
|
|
196
|
+
out = prod(sizes[c] for c in einsum_str.split("->")[1])
|
|
197
|
+
peak = max(peak, live + out + sum(n for n, _ in popped))
|
|
198
|
+
operands.append((out, True))
|
|
199
|
+
return peak
|