newch 0.2.0__py3-none-win_amd64.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.
- newch/__init__.py +29 -0
- newch/_lib.py +168 -0
- newch/callbacks.py +91 -0
- newch/data.py +227 -0
- newch/nc.dll +0 -0
- newch/net.py +449 -0
- newch/optim.py +64 -0
- newch/tensor.py +759 -0
- newch/training.py +301 -0
- newch-0.2.0.dist-info/METADATA +166 -0
- newch-0.2.0.dist-info/RECORD +15 -0
- newch-0.2.0.dist-info/WHEEL +5 -0
- newch-0.2.0.dist-info/licenses/LICENSE +202 -0
- newch-0.2.0.dist-info/licenses/NOTICE +8 -0
- newch-0.2.0.dist-info/top_level.txt +1 -0
newch/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""newch: a small machine-learning library for low-spec machines.
|
|
2
|
+
|
|
3
|
+
PyTorch-style tensors and autograd, Keras-style ``compile``/``fit``, backed
|
|
4
|
+
by a portable C engine. No Python dependencies (numpy is used if present).
|
|
5
|
+
|
|
6
|
+
import newch as nc
|
|
7
|
+
|
|
8
|
+
model = nc.Sequential([nc.net.Linear(2, 32), nc.net.ReLU(), nc.net.Linear(32, 3)])
|
|
9
|
+
model.compile(optimizer="adam", loss="cross_entropy", metrics=["accuracy"])
|
|
10
|
+
model.fit(x, y, epochs=20, batch_size=32)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from . import callbacks, data, net, optim
|
|
14
|
+
from .net import Model, Module, Sequential, quantize_
|
|
15
|
+
from .tensor import (Context, NcError, Tensor, avg_pool2d, batch_norm, conv2d, cross_entropy, default_context,
|
|
16
|
+
dropout, exp, float32, full, int8, int32, linear, load_tensors, log, log_softmax, manual_seed,
|
|
17
|
+
matmul, max_pool2d, mem_stats, mse_loss, no_grad, ones, parameter, persistent, rand, randn,
|
|
18
|
+
release, relu, save_tensors, set_default_context, sigmoid, softmax, sqrt, step_end, tanh, tensor,
|
|
19
|
+
zeros)
|
|
20
|
+
|
|
21
|
+
__version__ = "0.2.0"
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"Context", "Model", "Module", "NcError", "Sequential", "Tensor", "avg_pool2d", "batch_norm", "callbacks",
|
|
25
|
+
"conv2d", "cross_entropy", "data", "default_context", "dropout", "exp", "float32", "full", "int8", "int32",
|
|
26
|
+
"linear", "load_tensors", "log", "log_softmax", "manual_seed", "matmul", "max_pool2d", "mem_stats", "mse_loss",
|
|
27
|
+
"net", "no_grad", "ones", "optim", "parameter", "persistent", "quantize_", "rand", "randn", "release", "relu",
|
|
28
|
+
"save_tensors", "set_default_context", "sigmoid", "softmax", "sqrt", "step_end", "tanh", "tensor", "zeros",
|
|
29
|
+
]
|
newch/_lib.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""ctypes bindings to the C engine (nc.h). Internal: use the public API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ctypes
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from ctypes import POINTER, Structure, c_char_p, c_float, c_int, c_int64, c_size_t, c_uint64, c_void_p
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
ALL_AXES = -128
|
|
12
|
+
F32, I32, I8 = 0, 1, 3
|
|
13
|
+
DTYPE_NAMES = {F32: "float32", I32: "int32", I8: "int8"}
|
|
14
|
+
DTYPE_CODES = {"float32": F32, "int32": I32, "int8": I8}
|
|
15
|
+
DTYPE_SIZE = {"float32": 4, "int32": 4, "int8": 1}
|
|
16
|
+
ARRAY_CODE = {"float32": "f", "int32": "i", "int8": "b"}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Config(Structure):
|
|
20
|
+
_fields_ = [("arena_chunk_bytes", c_size_t), ("seed", c_uint64), ("force_reference", c_int), ("threads", c_int)]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CpuInfo(Structure):
|
|
24
|
+
_fields_ = [
|
|
25
|
+
("arch", c_char_p),
|
|
26
|
+
("has_neon", c_int),
|
|
27
|
+
("has_dotprod", c_int),
|
|
28
|
+
("has_fp16", c_int),
|
|
29
|
+
("has_avx2", c_int),
|
|
30
|
+
("has_fma", c_int),
|
|
31
|
+
("has_rvv", c_int),
|
|
32
|
+
("n_cores", c_int),
|
|
33
|
+
("n_big_cores", c_int),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class MemStats(Structure):
|
|
38
|
+
_fields_ = [
|
|
39
|
+
("persistent_bytes", c_size_t),
|
|
40
|
+
("arena_bytes_in_use", c_size_t),
|
|
41
|
+
("arena_bytes_reserved", c_size_t),
|
|
42
|
+
("arena_peak_bytes", c_size_t),
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class OptimOpts(Structure):
|
|
47
|
+
_fields_ = [(n, c_float) for n in ("lr", "momentum", "weight_decay", "beta1", "beta2", "eps")]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _lib_names() -> list[str]:
|
|
51
|
+
if sys.platform == "win32":
|
|
52
|
+
return ["nc.dll"]
|
|
53
|
+
if sys.platform == "darwin":
|
|
54
|
+
return ["libnc.dylib"]
|
|
55
|
+
return ["libnc.so"]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _find_library() -> str:
|
|
59
|
+
env = os.environ.get("NEWCH_LIB")
|
|
60
|
+
if env:
|
|
61
|
+
return env
|
|
62
|
+
here = Path(__file__).resolve().parent
|
|
63
|
+
for d in (here, here.parent / "build" / "host"):
|
|
64
|
+
for name in _lib_names():
|
|
65
|
+
if (d / name).exists():
|
|
66
|
+
return str(d / name)
|
|
67
|
+
raise OSError(
|
|
68
|
+
"newch: native library not found. Build it with `python tools/build.py` "
|
|
69
|
+
"or set NEWCH_LIB to the path of nc.dll / libnc.so."
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
lib = ctypes.CDLL(_find_library())
|
|
74
|
+
|
|
75
|
+
P = c_void_p
|
|
76
|
+
I64P = POINTER(c_int64)
|
|
77
|
+
|
|
78
|
+
_SIGNATURES = {
|
|
79
|
+
# context
|
|
80
|
+
"nc_init": (P, [POINTER(Config)]),
|
|
81
|
+
"nc_free_ctx": (None, [P]),
|
|
82
|
+
"nc_manual_seed": (None, [P, c_uint64]),
|
|
83
|
+
"nc_linear_fn": (P, [P, P, P]),
|
|
84
|
+
"nc_step_end": (None, [P]),
|
|
85
|
+
"nc_arena_trim": (None, [P]),
|
|
86
|
+
"nc_num_threads": (c_int, [P]),
|
|
87
|
+
"nc_persistent_begin": (None, [P]),
|
|
88
|
+
"nc_persistent_end": (None, [P]),
|
|
89
|
+
"nc_set_grad_enabled": (c_int, [P, c_int]),
|
|
90
|
+
"nc_ok": (c_int, [P]),
|
|
91
|
+
"nc_status_code": (c_int, [P]),
|
|
92
|
+
"nc_error": (c_char_p, [P]),
|
|
93
|
+
"nc_clear_error": (None, [P]),
|
|
94
|
+
"nc_get_mem_stats": (MemStats, [P]),
|
|
95
|
+
"nc_reset_peak": (None, [P]),
|
|
96
|
+
"nc_cpu": (POINTER(CpuInfo), [P]),
|
|
97
|
+
"nc_kernel_backend": (c_char_p, [P]),
|
|
98
|
+
# tensors
|
|
99
|
+
"nc_empty": (P, [P, c_int, c_int, I64P]),
|
|
100
|
+
"nc_zeros": (P, [P, c_int, c_int, I64P]),
|
|
101
|
+
"nc_full": (P, [P, c_float, c_int, I64P]),
|
|
102
|
+
"nc_randn": (P, [P, c_float, c_int, I64P]),
|
|
103
|
+
"nc_rand_uniform": (P, [P, c_float, c_float, c_int, I64P]),
|
|
104
|
+
"nc_from_data": (P, [P, c_int, c_void_p, c_int, I64P]),
|
|
105
|
+
"nc_scalar": (P, [P, c_float]),
|
|
106
|
+
"nc_parameter": (P, [P, P, c_int, I64P]),
|
|
107
|
+
"nc_ndim": (c_int, [P]),
|
|
108
|
+
"nc_shape": (I64P, [P]),
|
|
109
|
+
"nc_numel": (c_int64, [P]),
|
|
110
|
+
"nc_get_dtype": (c_int, [P]),
|
|
111
|
+
"nc_is_contiguous": (c_int, [P]),
|
|
112
|
+
"nc_requires_grad": (c_int, [P]),
|
|
113
|
+
"nc_set_requires_grad": (None, [P, c_int]),
|
|
114
|
+
"nc_is_persistent": (c_int, [P]),
|
|
115
|
+
"nc_release": (None, [P]),
|
|
116
|
+
"nc_grad": (P, [P]),
|
|
117
|
+
"nc_item": (c_float, [P]),
|
|
118
|
+
"nc_data_f32": (c_void_p, [P]),
|
|
119
|
+
"nc_copy_to": (c_int, [P, c_void_p, c_size_t]),
|
|
120
|
+
"nc_copy_from": (c_int, [P, c_void_p, c_size_t]),
|
|
121
|
+
# ops
|
|
122
|
+
**{f"nc_{n}": (P, [P, P]) for n in ("add", "sub", "mul", "div", "matmul", "cross_entropy", "mse_loss",
|
|
123
|
+
"index_select")},
|
|
124
|
+
**{f"nc_{n}": (P, [P]) for n in ("neg", "exp", "log", "sqrt", "relu", "sigmoid", "tanh", "contiguous",
|
|
125
|
+
"log_softmax", "softmax", "detach")},
|
|
126
|
+
**{f"nc_{n}": (P, [P, c_float]) for n in ("scale", "add_scalar", "pow_scalar")},
|
|
127
|
+
**{f"nc_{n}": (P, [P, c_int, c_int]) for n in ("sum", "mean", "max", "transpose")},
|
|
128
|
+
"nc_argmax": (P, [P, c_int]),
|
|
129
|
+
"nc_conv2d": (P, [P, P, P, c_int, c_int, c_int, c_int]),
|
|
130
|
+
"nc_max_pool2d": (P, [P, c_int, c_int, c_int, c_int, c_int, c_int]),
|
|
131
|
+
"nc_avg_pool2d": (P, [P, c_int, c_int, c_int, c_int, c_int, c_int]),
|
|
132
|
+
"nc_dropout": (P, [P, c_float]),
|
|
133
|
+
"nc_batch_norm": (P, [P, P, P, P, P, c_int, c_float, c_float]),
|
|
134
|
+
"nc_quantize_rows": (P, [P, POINTER(c_void_p)]),
|
|
135
|
+
"nc_linear_int8": (P, [P, P, P, P]),
|
|
136
|
+
"nc_save": (c_int, [P, c_char_p, c_int, POINTER(c_char_p), POINTER(c_void_p)]),
|
|
137
|
+
"nc_load": (c_int, [P, c_char_p, c_int, POINTER(c_char_p), POINTER(c_void_p)]),
|
|
138
|
+
"nc_reshape": (P, [P, c_int, I64P]),
|
|
139
|
+
# autograd
|
|
140
|
+
"nc_backward": (c_int, [P]),
|
|
141
|
+
"nc_zero_grad": (None, [P]),
|
|
142
|
+
# optimizers
|
|
143
|
+
"nc_sgd": (P, [P, POINTER(c_void_p), c_int, OptimOpts]),
|
|
144
|
+
"nc_adam": (P, [P, POINTER(c_void_p), c_int, OptimOpts]),
|
|
145
|
+
"nc_optim_zero_grad": (None, [P]),
|
|
146
|
+
"nc_optim_step": (None, [P]),
|
|
147
|
+
"nc_optim_set_lr": (None, [P, c_float]),
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for _name, (_res, _args) in _SIGNATURES.items():
|
|
151
|
+
_fn = getattr(lib, _name)
|
|
152
|
+
_fn.restype = _res
|
|
153
|
+
_fn.argtypes = _args
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def shape_arg(shape) -> tuple[int, ctypes.Array]:
|
|
157
|
+
shape = tuple(int(s) for s in shape)
|
|
158
|
+
return len(shape), (c_int64 * max(len(shape), 1))(*shape)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def ptr_array(ptrs) -> ctypes.Array:
|
|
162
|
+
ptrs = list(ptrs)
|
|
163
|
+
return (c_void_p * max(len(ptrs), 1))(*ptrs)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def name_array(names) -> ctypes.Array:
|
|
167
|
+
names = [n.encode("utf-8") for n in names]
|
|
168
|
+
return (c_char_p * max(len(names), 1))(*names)
|
newch/callbacks.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Keras-style training callbacks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ctypes
|
|
6
|
+
import math
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Callback:
|
|
10
|
+
model = None
|
|
11
|
+
|
|
12
|
+
def set_model(self, model) -> None:
|
|
13
|
+
self.model = model
|
|
14
|
+
|
|
15
|
+
def on_train_begin(self, logs: dict) -> None: ...
|
|
16
|
+
def on_epoch_end(self, epoch: int, logs: dict) -> None: ...
|
|
17
|
+
def on_train_end(self, logs: dict) -> None: ...
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class History(Callback):
|
|
21
|
+
"""Returned by fit(): ``history.history["loss"]`` is one value per epoch."""
|
|
22
|
+
|
|
23
|
+
def on_train_begin(self, logs):
|
|
24
|
+
self.epoch: list[int] = []
|
|
25
|
+
self.history: dict[str, list[float]] = {}
|
|
26
|
+
|
|
27
|
+
def on_epoch_end(self, epoch, logs):
|
|
28
|
+
self.epoch.append(epoch)
|
|
29
|
+
for k, v in logs.items():
|
|
30
|
+
self.history.setdefault(k, []).append(v)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class EarlyStopping(Callback):
|
|
34
|
+
"""Stops when ``monitor`` has not improved by ``min_delta`` for ``patience`` epochs."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, monitor: str = "val_loss", min_delta: float = 0.0, patience: int = 0,
|
|
37
|
+
mode: str = "auto", restore_best_weights: bool = False):
|
|
38
|
+
self.monitor, self.min_delta, self.patience = monitor, abs(min_delta), patience
|
|
39
|
+
if mode == "auto":
|
|
40
|
+
mode = "max" if "acc" in monitor else "min"
|
|
41
|
+
if mode not in ("min", "max"):
|
|
42
|
+
raise ValueError("mode must be 'auto', 'min' or 'max'")
|
|
43
|
+
self.sign = 1.0 if mode == "min" else -1.0
|
|
44
|
+
self.restore_best_weights = restore_best_weights
|
|
45
|
+
self.stopped_epoch = None
|
|
46
|
+
|
|
47
|
+
def on_train_begin(self, logs):
|
|
48
|
+
self.best = math.inf
|
|
49
|
+
self.wait = 0
|
|
50
|
+
self.best_weights = None
|
|
51
|
+
self.stopped_epoch = None
|
|
52
|
+
|
|
53
|
+
def on_epoch_end(self, epoch, logs):
|
|
54
|
+
if self.monitor not in logs:
|
|
55
|
+
raise KeyError(f"EarlyStopping: '{self.monitor}' not in logs (available: {sorted(logs)})")
|
|
56
|
+
value = self.sign * logs[self.monitor]
|
|
57
|
+
if value < self.best - self.min_delta:
|
|
58
|
+
self.best, self.wait = value, 0
|
|
59
|
+
if self.restore_best_weights:
|
|
60
|
+
self.best_weights = [p._flat() for p in self.model.parameters()]
|
|
61
|
+
return
|
|
62
|
+
self.wait += 1
|
|
63
|
+
if self.wait >= self.patience:
|
|
64
|
+
self.model.stop_training = True
|
|
65
|
+
self.stopped_epoch = epoch
|
|
66
|
+
|
|
67
|
+
def on_train_end(self, logs):
|
|
68
|
+
if self.restore_best_weights and self.best_weights is not None:
|
|
69
|
+
from ._lib import lib
|
|
70
|
+
|
|
71
|
+
for p, saved in zip(self.model.parameters(), self.best_weights):
|
|
72
|
+
ctypes.memmove(lib.nc_data_f32(p.ptr), saved.buffer_info()[0], len(saved) * 4)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class LambdaCallback(Callback):
|
|
76
|
+
"""Keyword-only, with Keras' argument names: on_epoch_end(epoch, logs), ..."""
|
|
77
|
+
|
|
78
|
+
def __init__(self, *, on_epoch_end=None, on_train_begin=None, on_train_end=None):
|
|
79
|
+
self._e, self._b, self._t = on_epoch_end, on_train_begin, on_train_end
|
|
80
|
+
|
|
81
|
+
def on_train_begin(self, logs):
|
|
82
|
+
if self._b:
|
|
83
|
+
self._b(logs)
|
|
84
|
+
|
|
85
|
+
def on_epoch_end(self, epoch, logs):
|
|
86
|
+
if self._e:
|
|
87
|
+
self._e(epoch, logs)
|
|
88
|
+
|
|
89
|
+
def on_train_end(self, logs):
|
|
90
|
+
if self._t:
|
|
91
|
+
self._t(logs)
|
newch/data.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Datasets and a streaming DataLoader (torch.utils.data style).
|
|
2
|
+
|
|
3
|
+
For data larger than RAM, store samples in a raw binary file and open it
|
|
4
|
+
with FileArray: rows are read through mmap one batch at a time, so memory use
|
|
5
|
+
stays around one batch regardless of the dataset size.
|
|
6
|
+
|
|
7
|
+
FileArray.create("x.bin", rows, row_shape=(28, 28)) # write once
|
|
8
|
+
ds = TensorDataset(FileArray("x.bin", (60000, 28, 28)), labels)
|
|
9
|
+
for xb, yb in DataLoader(ds, batch_size=64, shuffle=True): ...
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import array
|
|
15
|
+
import mmap
|
|
16
|
+
import random
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from . import _lib as L
|
|
20
|
+
from .tensor import (Context, Tensor, _flatten, _is_numpy, default_context, float32, int8, int32, tensor,
|
|
21
|
+
tensor_from_bytes)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Dataset:
|
|
25
|
+
"""Map-style dataset: implement ``__len__`` and ``__getitem__``."""
|
|
26
|
+
|
|
27
|
+
def __len__(self) -> int:
|
|
28
|
+
raise NotImplementedError
|
|
29
|
+
|
|
30
|
+
def __getitem__(self, index):
|
|
31
|
+
raise NotImplementedError
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# --------------------------------------------------------------------------
|
|
35
|
+
# Row sources: array-likes that can hand out a batch of rows as raw bytes
|
|
36
|
+
# --------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class FileArray:
|
|
40
|
+
"""A read-only array stored row-major in a raw binary file (like numpy.memmap).
|
|
41
|
+
|
|
42
|
+
shape includes the number of rows; dtype is "float32", "int32" or "int8".
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, path, shape, dtype: str = float32, offset: int = 0):
|
|
46
|
+
if dtype not in L.DTYPE_CODES:
|
|
47
|
+
raise ValueError(f"unsupported dtype {dtype!r}")
|
|
48
|
+
self.path, self.shape, self.dtype, self.offset = Path(path), tuple(int(s) for s in shape), dtype, offset
|
|
49
|
+
self.row_shape = self.shape[1:]
|
|
50
|
+
n = 1
|
|
51
|
+
for d in self.row_shape:
|
|
52
|
+
n *= d
|
|
53
|
+
self.row_bytes = n * L.DTYPE_SIZE[dtype]
|
|
54
|
+
need = offset + self.shape[0] * self.row_bytes
|
|
55
|
+
size = self.path.stat().st_size
|
|
56
|
+
if size < need:
|
|
57
|
+
raise ValueError(f"{path}: {size} bytes, but shape {self.shape} {dtype} needs {need}")
|
|
58
|
+
self._f = open(self.path, "rb")
|
|
59
|
+
self._mm = mmap.mmap(self._f.fileno(), 0, access=mmap.ACCESS_READ) if need else None
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def create(cls, path, rows, row_shape, dtype: str = float32) -> "FileArray":
|
|
63
|
+
"""Writes an iterable of rows (sequences / numpy rows) without holding them all in memory."""
|
|
64
|
+
code = L.ARRAY_CODE[dtype]
|
|
65
|
+
count = 0
|
|
66
|
+
with open(path, "wb") as f:
|
|
67
|
+
for row in rows:
|
|
68
|
+
if _is_numpy(row):
|
|
69
|
+
import numpy as np
|
|
70
|
+
|
|
71
|
+
f.write(np.ascontiguousarray(row, dtype={float32: np.float32, int32: np.int32,
|
|
72
|
+
int8: np.int8}[dtype]).tobytes())
|
|
73
|
+
else:
|
|
74
|
+
flat: list = []
|
|
75
|
+
_flatten(row, [], flat) if isinstance(row, (list, tuple)) else flat.append(row)
|
|
76
|
+
f.write(array.array(code, flat if dtype == float32 else map(int, flat)).tobytes())
|
|
77
|
+
count += 1
|
|
78
|
+
return cls(path, (count, *row_shape), dtype)
|
|
79
|
+
|
|
80
|
+
def __len__(self) -> int:
|
|
81
|
+
return self.shape[0]
|
|
82
|
+
|
|
83
|
+
def rows(self, indices) -> bytes:
|
|
84
|
+
rb, off, mm = self.row_bytes, self.offset, self._mm
|
|
85
|
+
if not indices:
|
|
86
|
+
return b""
|
|
87
|
+
first = indices[0]
|
|
88
|
+
if indices == list(range(first, first + len(indices))): # contiguous: one slice
|
|
89
|
+
return mm[off + first * rb: off + (first + len(indices)) * rb]
|
|
90
|
+
return b"".join(mm[off + i * rb: off + (i + 1) * rb] for i in indices)
|
|
91
|
+
|
|
92
|
+
def __getitem__(self, i: int):
|
|
93
|
+
return array.array(L.ARRAY_CODE[self.dtype], self.rows([i % len(self)])).tolist()
|
|
94
|
+
|
|
95
|
+
def close(self) -> None:
|
|
96
|
+
if self._mm is not None:
|
|
97
|
+
self._mm.close()
|
|
98
|
+
self._mm = None
|
|
99
|
+
self._f.close()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class _MemoryRows:
|
|
103
|
+
"""In-memory row source built once from lists, array.array, numpy or a Tensor."""
|
|
104
|
+
|
|
105
|
+
def __init__(self, data):
|
|
106
|
+
if isinstance(data, Tensor):
|
|
107
|
+
self.dtype, shape = data.dtype, data.shape
|
|
108
|
+
flat = data._flat()
|
|
109
|
+
self.buf = flat.tobytes()
|
|
110
|
+
elif _is_numpy(data):
|
|
111
|
+
import numpy as np
|
|
112
|
+
|
|
113
|
+
kind = int32 if np.issubdtype(data.dtype, np.integer) or data.dtype == np.bool_ else float32
|
|
114
|
+
arr = np.ascontiguousarray(data, dtype=np.int32 if kind == int32 else np.float32)
|
|
115
|
+
self.dtype, shape, self.buf = kind, arr.shape, arr.tobytes()
|
|
116
|
+
else:
|
|
117
|
+
shape_l: list = []
|
|
118
|
+
flat_l: list = []
|
|
119
|
+
_flatten(list(data), shape_l, flat_l)
|
|
120
|
+
self.dtype = float32 if any(isinstance(v, float) for v in flat_l) else int32
|
|
121
|
+
self.buf = array.array(L.ARRAY_CODE[self.dtype],
|
|
122
|
+
flat_l if self.dtype == float32 else map(int, flat_l)).tobytes()
|
|
123
|
+
shape = tuple(shape_l)
|
|
124
|
+
if not shape:
|
|
125
|
+
raise ValueError("dataset arrays need at least one dimension (the sample axis)")
|
|
126
|
+
self.shape = tuple(shape)
|
|
127
|
+
self.row_shape = self.shape[1:]
|
|
128
|
+
n = 1
|
|
129
|
+
for d in self.row_shape:
|
|
130
|
+
n *= d
|
|
131
|
+
self.row_bytes = n * L.DTYPE_SIZE[self.dtype]
|
|
132
|
+
self._mv = memoryview(self.buf)
|
|
133
|
+
|
|
134
|
+
def __len__(self) -> int:
|
|
135
|
+
return self.shape[0]
|
|
136
|
+
|
|
137
|
+
def rows(self, indices) -> bytes:
|
|
138
|
+
rb, mv = self.row_bytes, self._mv
|
|
139
|
+
if not indices:
|
|
140
|
+
return b""
|
|
141
|
+
first = indices[0]
|
|
142
|
+
if indices == list(range(first, first + len(indices))):
|
|
143
|
+
return bytes(mv[first * rb:(first + len(indices)) * rb])
|
|
144
|
+
return b"".join(mv[i * rb:(i + 1) * rb] for i in indices)
|
|
145
|
+
|
|
146
|
+
def __getitem__(self, i: int):
|
|
147
|
+
return array.array(L.ARRAY_CODE[self.dtype], self.rows([i % len(self)])).tolist()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _row_source(data):
|
|
151
|
+
return data if isinstance(data, FileArray) else _MemoryRows(data)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class TensorDataset(Dataset):
|
|
155
|
+
"""Samples are rows of each array (torch.utils.data.TensorDataset).
|
|
156
|
+
|
|
157
|
+
Arrays may be nested lists, array.array, numpy arrays, Tensors or FileArray
|
|
158
|
+
(disk-backed). Integer data becomes int32, floating data float32.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
def __init__(self, *arrays):
|
|
162
|
+
if not arrays:
|
|
163
|
+
raise ValueError("TensorDataset needs at least one array")
|
|
164
|
+
self.sources = [_row_source(a) for a in arrays]
|
|
165
|
+
n = len(self.sources[0])
|
|
166
|
+
if any(len(s) != n for s in self.sources):
|
|
167
|
+
raise ValueError("all arrays must have the same number of rows")
|
|
168
|
+
|
|
169
|
+
def __len__(self) -> int:
|
|
170
|
+
return len(self.sources[0])
|
|
171
|
+
|
|
172
|
+
def __getitem__(self, index):
|
|
173
|
+
return tuple(s[index] for s in self.sources)
|
|
174
|
+
|
|
175
|
+
def get_batch(self, indices, ctx: Context) -> list[Tensor]:
|
|
176
|
+
return [tensor_from_bytes(s.rows(indices), (len(indices), *s.row_shape), s.dtype, ctx)
|
|
177
|
+
for s in self.sources]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class DataLoader:
|
|
181
|
+
"""Iterates a dataset in batches (torch.utils.data.DataLoader).
|
|
182
|
+
|
|
183
|
+
Yields a list with one temporary Tensor per field, valid until the next
|
|
184
|
+
``step_end()``. TensorDataset batches are gathered directly as bytes; other
|
|
185
|
+
datasets are collated from ``__getitem__``.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
def __init__(self, dataset, batch_size: int = 1, shuffle: bool = False, drop_last: bool = False,
|
|
189
|
+
generator: random.Random | None = None, ctx: Context | None = None):
|
|
190
|
+
if batch_size < 1:
|
|
191
|
+
raise ValueError("batch_size must be >= 1")
|
|
192
|
+
self.dataset, self.batch_size, self.shuffle, self.drop_last = dataset, batch_size, shuffle, drop_last
|
|
193
|
+
self.generator = generator or random.Random()
|
|
194
|
+
self._ctx = ctx
|
|
195
|
+
|
|
196
|
+
def __len__(self) -> int:
|
|
197
|
+
n = len(self.dataset)
|
|
198
|
+
return n // self.batch_size if self.drop_last else -(-n // self.batch_size)
|
|
199
|
+
|
|
200
|
+
def __iter__(self):
|
|
201
|
+
ctx = self._ctx or default_context()
|
|
202
|
+
order = list(range(len(self.dataset)))
|
|
203
|
+
if self.shuffle:
|
|
204
|
+
self.generator.shuffle(order)
|
|
205
|
+
bs = self.batch_size
|
|
206
|
+
stop = len(order) - (len(order) % bs if self.drop_last else 0)
|
|
207
|
+
for s in range(0, stop, bs):
|
|
208
|
+
idx = order[s:s + bs]
|
|
209
|
+
if hasattr(self.dataset, "get_batch"):
|
|
210
|
+
yield self.dataset.get_batch(idx, ctx)
|
|
211
|
+
else:
|
|
212
|
+
yield _collate([self.dataset[i] for i in idx], ctx)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _stack(items, ctx: Context) -> Tensor:
|
|
216
|
+
if _is_numpy(items[0]):
|
|
217
|
+
import numpy as np
|
|
218
|
+
|
|
219
|
+
return tensor(np.stack(items), ctx=ctx)
|
|
220
|
+
return tensor(list(items), ctx=ctx)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _collate(samples, ctx: Context):
|
|
224
|
+
"""Tuple samples -> list of batched fields (as torch's default_collate); else one Tensor."""
|
|
225
|
+
if isinstance(samples[0], tuple):
|
|
226
|
+
return [_stack([s[k] for s in samples], ctx) for k in range(len(samples[0]))]
|
|
227
|
+
return _stack(samples, ctx)
|
newch/nc.dll
ADDED
|
Binary file
|