loaderx 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {loaderx-0.2.0 → loaderx-0.3.0}/PKG-INFO +26 -27
- {loaderx-0.2.0 → loaderx-0.3.0}/README.md +19 -20
- {loaderx-0.2.0 → loaderx-0.3.0}/loaderx/__init__.py +1 -1
- loaderx-0.3.0/loaderx/_sampler.py +69 -0
- {loaderx-0.2.0 → loaderx-0.3.0}/loaderx/dataloader.py +4 -6
- loaderx-0.3.0/loaderx/lib/libsampler.so +0 -0
- {loaderx-0.2.0 → loaderx-0.3.0}/loaderx.egg-info/PKG-INFO +26 -27
- {loaderx-0.2.0 → loaderx-0.3.0}/pyproject.toml +6 -6
- loaderx-0.2.0/loaderx/_sampler.py +0 -99
- loaderx-0.2.0/loaderx/lib/libsampler.so +0 -0
- {loaderx-0.2.0 → loaderx-0.3.0}/LICENSE +0 -0
- {loaderx-0.2.0 → loaderx-0.3.0}/loaderx/dataset.py +0 -0
- {loaderx-0.2.0 → loaderx-0.3.0}/loaderx/utils.py +0 -0
- {loaderx-0.2.0 → loaderx-0.3.0}/loaderx.egg-info/SOURCES.txt +0 -0
- {loaderx-0.2.0 → loaderx-0.3.0}/loaderx.egg-info/dependency_links.txt +0 -0
- {loaderx-0.2.0 → loaderx-0.3.0}/loaderx.egg-info/requires.txt +0 -0
- {loaderx-0.2.0 → loaderx-0.3.0}/loaderx.egg-info/top_level.txt +0 -0
- {loaderx-0.2.0 → loaderx-0.3.0}/setup.cfg +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: loaderx
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: A record-based data runtime, focused on delivering extreme throughput and low latency
|
|
5
5
|
Author-email: Ben0i0d <ben0i0d@foxmail.com>
|
|
6
6
|
License: MIT License
|
|
7
7
|
|
|
@@ -25,16 +25,16 @@ License: MIT License
|
|
|
25
25
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
26
|
SOFTWARE.
|
|
27
27
|
|
|
28
|
-
Project-URL: Homepage, https://
|
|
29
|
-
Project-URL: Documentation, https://
|
|
30
|
-
Project-URL: Source, https://
|
|
31
|
-
Project-URL: Bug Tracker, https://
|
|
28
|
+
Project-URL: Homepage, https://codeberg.org/eoelab/loaderx
|
|
29
|
+
Project-URL: Documentation, https://codeberg.org/eoelab/loaderx
|
|
30
|
+
Project-URL: Source, https://codeberg.org/eoelab/loaderx
|
|
31
|
+
Project-URL: Bug Tracker, https://codeberg.org/eoelab/loaderx
|
|
32
32
|
Keywords: flax,python,dataloader
|
|
33
33
|
Classifier: Development Status :: 3 - Alpha
|
|
34
34
|
Classifier: Intended Audience :: Developers
|
|
35
35
|
Classifier: License :: OSI Approved :: MIT License
|
|
36
36
|
Classifier: Programming Language :: Python :: 3
|
|
37
|
-
Requires-Python:
|
|
37
|
+
Requires-Python: ==3.13
|
|
38
38
|
Description-Content-Type: text/markdown
|
|
39
39
|
License-File: LICENSE
|
|
40
40
|
Requires-Dist: numpy
|
|
@@ -45,19 +45,16 @@ Dynamic: license-file
|
|
|
45
45
|
# loaderx
|
|
46
46
|
A record-based data runtime, focused on delivering extreme throughput and low latency
|
|
47
47
|
|
|
48
|
-
**
|
|
48
|
+
**Only Python3.13_Linux_amd64**
|
|
49
49
|
|
|
50
50
|
## Sampler
|
|
51
51
|
a high-performance sampler implemented in Zig
|
|
52
|
-
### Build
|
|
53
|
-
zig build-lib -dynamic -OReleaseFast sampler/root.zig -femit-bin=loaderx/lib/libsampler.so
|
|
54
52
|
### Index Generator
|
|
55
53
|
1. Sequential generation: indices are produced by traversing the index space in order.
|
|
56
54
|
* Sliding traversal: indices are obtained using a fixed-size sliding window. Note that in this case, the index space is treated as a circular queue to avoid truncation at the tail.
|
|
57
55
|
2. Random generation: indices are sampled randomly from the index space.
|
|
58
56
|
* Global random: a set of samples is drawn randomly from the entire index space.
|
|
59
57
|
|
|
60
|
-
|
|
61
58
|
## zrecord
|
|
62
59
|
基于Zig实现一个并发友好、实现更简单、单机性能更优的 record 存储系统
|
|
63
60
|
|
|
@@ -85,27 +82,24 @@ zig build-lib -dynamic -OReleaseFast sampler/root.zig -femit-bin=loaderx/lib/lib
|
|
|
85
82
|
| flate | Deflate |
|
|
86
83
|
```
|
|
87
84
|
3. 通过offset支持高效随机访问
|
|
88
|
-
* offset表项是一个三元组 [[chunk_id :
|
|
89
|
-
* chunk_id为
|
|
90
|
-
* offset为
|
|
91
|
-
* physical_length为
|
|
92
|
-
*
|
|
85
|
+
* offset表项是一个三元组 [[chunk_id : u32, offset : u64, physical_length : u32], ...],长度为N。将外部索引空间(outside[0, N])指向一个实际存储地址
|
|
86
|
+
* chunk_id为u32,表示具体chunk
|
|
87
|
+
* offset为u64, 表示具体chunk内的偏移
|
|
88
|
+
* physical_length为u32,表示record数据大小
|
|
89
|
+
* offset表每项的字节大小最好为 8 的整数倍,提升对齐和缓存效率
|
|
90
|
+
* offset表是mmap访问模式,由于等长,直接将 idx 转换为 ptr + 16*idx
|
|
93
91
|
* 不同位宽表示的数值范围
|
|
94
92
|
```
|
|
95
93
|
u16 → 2^16 - 1 (65535)
|
|
96
|
-
u24 → 2^24 - 1 (16777215)
|
|
97
94
|
u32 → 2^32 - 1 (4.29e9)
|
|
98
95
|
u64 → 2^64 - 1 (1.84e19)
|
|
99
96
|
```
|
|
100
97
|
* 不同位宽表示的数据大小
|
|
101
98
|
```
|
|
102
99
|
u16 → 64 KiB
|
|
103
|
-
u24 → 16 MiB
|
|
104
100
|
u32 → 4 GiB
|
|
105
|
-
u40 → 1 TiB
|
|
106
101
|
u64 → 16 EiB
|
|
107
102
|
```
|
|
108
|
-
* offset表每项的字节大小最好为 8 的整数倍,提升对齐和缓存效率
|
|
109
103
|
```
|
|
110
104
|
outside idx (global)
|
|
111
105
|
↓
|
|
@@ -118,10 +112,9 @@ zig build-lib -dynamic -OReleaseFast sampler/root.zig -femit-bin=loaderx/lib/lib
|
|
|
118
112
|
* meta.json:描述全局元数据,包括全局参数与Field参数
|
|
119
113
|
```
|
|
120
114
|
{
|
|
121
|
-
"version": 1,
|
|
122
115
|
"length": 65536,
|
|
123
|
-
"chunks": {"num": 12, "size": 8192, "unfull": [8,
|
|
124
|
-
"fields": [{"name": "A", "dtype": "f32", "compress": "flate"}, {"name": "B", "dtype": "i32", "compress": "raw"}]
|
|
116
|
+
"chunks": { "num": 12, "size": 8192, "unfull": {[8, write_pos, record_count], ... } },
|
|
117
|
+
"fields": [ {"name": "A", "dtype": "f32", "compress": "flate"}, {"name": "B", "dtype": "i32", "compress": "raw"} ]
|
|
125
118
|
}
|
|
126
119
|
```
|
|
127
120
|
* Field_offset.zr:索引表
|
|
@@ -137,8 +130,8 @@ zig build-lib -dynamic -OReleaseFast sampler/root.zig -femit-bin=loaderx/lib/lib
|
|
|
137
130
|
```
|
|
138
131
|
dataset
|
|
139
132
|
├── meta.json
|
|
140
|
-
├──
|
|
141
|
-
├──
|
|
133
|
+
├── A.offset
|
|
134
|
+
├── B.offset
|
|
142
135
|
├── chunk
|
|
143
136
|
│ ├── 0.zr
|
|
144
137
|
└──└── 1.zr
|
|
@@ -168,6 +161,9 @@ dataset
|
|
|
168
161
|
1. mmap:维护全局offset、chunk文件的handle
|
|
169
162
|
* 初始化阶段注册,关闭前释放
|
|
170
163
|
* 对于full的chunk,使用mmap+close(只读),对于unfull的chunk使用mmap(读写)。一旦写满(writer提交),新建chunk并返回给writer,原mmap close。(避免过多占用fd)
|
|
164
|
+
* 对于chunk/offset文件,动态完成分块增长,基于ftruncate+mremap实现
|
|
165
|
+
* 扩容offset表,一次增长 1M项(16MiB)
|
|
166
|
+
* 扩容chunk文件,一次增长 1GiB
|
|
171
167
|
2. batch:维护每次返回batch的handle,也就是[ptr, logical_length]中ptr的合规性
|
|
172
168
|
* 工作线程完成后注册
|
|
173
169
|
* 必须调用函数来手动释放,释放时batch压入GC队列
|
|
@@ -175,9 +171,12 @@ dataset
|
|
|
175
171
|
#### 在线任务
|
|
176
172
|
|
|
177
173
|
1. 写任务:写入只允许chunk-level append-only,其余操作基于offset重定向,缺失的field构造为全为0的项(physical_length为0代表无)
|
|
178
|
-
1.
|
|
174
|
+
1. 追加:在chunk当前写入位置追加一个record,offset表更新length+1条目,全局长度增加
|
|
175
|
+
* 写满:写入完成时检查,如果record_count达到chunk_size, 新建chunk
|
|
176
|
+
* 写入越界:捕捉错误,并对chunk扩容
|
|
177
|
+
* offset[ length+1 ]不存在:捕捉错误,并对offset扩容
|
|
179
178
|
2. 修改:追加一个record,并修改offset项
|
|
180
|
-
3. 删除:删除offset项(将目标项替换为最后一项)
|
|
179
|
+
3. 删除:删除offset项(将目标项替换为最后一项),length减一(越界访问属于UB行为)
|
|
181
180
|
|
|
182
181
|
2. 读任务:用一个indices(索引数组),从原数组里访问指定位置的元素(gather)
|
|
183
182
|
1. 对于无压缩的数据,将直接返回文件内地址实现zero-copy
|
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
# loaderx
|
|
2
2
|
A record-based data runtime, focused on delivering extreme throughput and low latency
|
|
3
3
|
|
|
4
|
-
**
|
|
4
|
+
**Only Python3.13_Linux_amd64**
|
|
5
5
|
|
|
6
6
|
## Sampler
|
|
7
7
|
a high-performance sampler implemented in Zig
|
|
8
|
-
### Build
|
|
9
|
-
zig build-lib -dynamic -OReleaseFast sampler/root.zig -femit-bin=loaderx/lib/libsampler.so
|
|
10
8
|
### Index Generator
|
|
11
9
|
1. Sequential generation: indices are produced by traversing the index space in order.
|
|
12
10
|
* Sliding traversal: indices are obtained using a fixed-size sliding window. Note that in this case, the index space is treated as a circular queue to avoid truncation at the tail.
|
|
13
11
|
2. Random generation: indices are sampled randomly from the index space.
|
|
14
12
|
* Global random: a set of samples is drawn randomly from the entire index space.
|
|
15
13
|
|
|
16
|
-
|
|
17
14
|
## zrecord
|
|
18
15
|
基于Zig实现一个并发友好、实现更简单、单机性能更优的 record 存储系统
|
|
19
16
|
|
|
@@ -41,27 +38,24 @@ zig build-lib -dynamic -OReleaseFast sampler/root.zig -femit-bin=loaderx/lib/lib
|
|
|
41
38
|
| flate | Deflate |
|
|
42
39
|
```
|
|
43
40
|
3. 通过offset支持高效随机访问
|
|
44
|
-
* offset表项是一个三元组 [[chunk_id :
|
|
45
|
-
* chunk_id为
|
|
46
|
-
* offset为
|
|
47
|
-
* physical_length为
|
|
48
|
-
*
|
|
41
|
+
* offset表项是一个三元组 [[chunk_id : u32, offset : u64, physical_length : u32], ...],长度为N。将外部索引空间(outside[0, N])指向一个实际存储地址
|
|
42
|
+
* chunk_id为u32,表示具体chunk
|
|
43
|
+
* offset为u64, 表示具体chunk内的偏移
|
|
44
|
+
* physical_length为u32,表示record数据大小
|
|
45
|
+
* offset表每项的字节大小最好为 8 的整数倍,提升对齐和缓存效率
|
|
46
|
+
* offset表是mmap访问模式,由于等长,直接将 idx 转换为 ptr + 16*idx
|
|
49
47
|
* 不同位宽表示的数值范围
|
|
50
48
|
```
|
|
51
49
|
u16 → 2^16 - 1 (65535)
|
|
52
|
-
u24 → 2^24 - 1 (16777215)
|
|
53
50
|
u32 → 2^32 - 1 (4.29e9)
|
|
54
51
|
u64 → 2^64 - 1 (1.84e19)
|
|
55
52
|
```
|
|
56
53
|
* 不同位宽表示的数据大小
|
|
57
54
|
```
|
|
58
55
|
u16 → 64 KiB
|
|
59
|
-
u24 → 16 MiB
|
|
60
56
|
u32 → 4 GiB
|
|
61
|
-
u40 → 1 TiB
|
|
62
57
|
u64 → 16 EiB
|
|
63
58
|
```
|
|
64
|
-
* offset表每项的字节大小最好为 8 的整数倍,提升对齐和缓存效率
|
|
65
59
|
```
|
|
66
60
|
outside idx (global)
|
|
67
61
|
↓
|
|
@@ -74,10 +68,9 @@ zig build-lib -dynamic -OReleaseFast sampler/root.zig -femit-bin=loaderx/lib/lib
|
|
|
74
68
|
* meta.json:描述全局元数据,包括全局参数与Field参数
|
|
75
69
|
```
|
|
76
70
|
{
|
|
77
|
-
"version": 1,
|
|
78
71
|
"length": 65536,
|
|
79
|
-
"chunks": {"num": 12, "size": 8192, "unfull": [8,
|
|
80
|
-
"fields": [{"name": "A", "dtype": "f32", "compress": "flate"}, {"name": "B", "dtype": "i32", "compress": "raw"}]
|
|
72
|
+
"chunks": { "num": 12, "size": 8192, "unfull": {[8, write_pos, record_count], ... } },
|
|
73
|
+
"fields": [ {"name": "A", "dtype": "f32", "compress": "flate"}, {"name": "B", "dtype": "i32", "compress": "raw"} ]
|
|
81
74
|
}
|
|
82
75
|
```
|
|
83
76
|
* Field_offset.zr:索引表
|
|
@@ -93,8 +86,8 @@ zig build-lib -dynamic -OReleaseFast sampler/root.zig -femit-bin=loaderx/lib/lib
|
|
|
93
86
|
```
|
|
94
87
|
dataset
|
|
95
88
|
├── meta.json
|
|
96
|
-
├──
|
|
97
|
-
├──
|
|
89
|
+
├── A.offset
|
|
90
|
+
├── B.offset
|
|
98
91
|
├── chunk
|
|
99
92
|
│ ├── 0.zr
|
|
100
93
|
└──└── 1.zr
|
|
@@ -124,6 +117,9 @@ dataset
|
|
|
124
117
|
1. mmap:维护全局offset、chunk文件的handle
|
|
125
118
|
* 初始化阶段注册,关闭前释放
|
|
126
119
|
* 对于full的chunk,使用mmap+close(只读),对于unfull的chunk使用mmap(读写)。一旦写满(writer提交),新建chunk并返回给writer,原mmap close。(避免过多占用fd)
|
|
120
|
+
* 对于chunk/offset文件,动态完成分块增长,基于ftruncate+mremap实现
|
|
121
|
+
* 扩容offset表,一次增长 1M项(16MiB)
|
|
122
|
+
* 扩容chunk文件,一次增长 1GiB
|
|
127
123
|
2. batch:维护每次返回batch的handle,也就是[ptr, logical_length]中ptr的合规性
|
|
128
124
|
* 工作线程完成后注册
|
|
129
125
|
* 必须调用函数来手动释放,释放时batch压入GC队列
|
|
@@ -131,9 +127,12 @@ dataset
|
|
|
131
127
|
#### 在线任务
|
|
132
128
|
|
|
133
129
|
1. 写任务:写入只允许chunk-level append-only,其余操作基于offset重定向,缺失的field构造为全为0的项(physical_length为0代表无)
|
|
134
|
-
1.
|
|
130
|
+
1. 追加:在chunk当前写入位置追加一个record,offset表更新length+1条目,全局长度增加
|
|
131
|
+
* 写满:写入完成时检查,如果record_count达到chunk_size, 新建chunk
|
|
132
|
+
* 写入越界:捕捉错误,并对chunk扩容
|
|
133
|
+
* offset[ length+1 ]不存在:捕捉错误,并对offset扩容
|
|
135
134
|
2. 修改:追加一个record,并修改offset项
|
|
136
|
-
3. 删除:删除offset项(将目标项替换为最后一项)
|
|
135
|
+
3. 删除:删除offset项(将目标项替换为最后一项),length减一(越界访问属于UB行为)
|
|
137
136
|
|
|
138
137
|
2. 读任务:用一个indices(索引数组),从原数组里访问指定位置的元素(gather)
|
|
139
138
|
1. 对于无压缩的数据,将直接返回文件内地址实现zero-copy
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
import ctypes
|
|
4
|
+
import numpy as np
|
|
5
|
+
from typing import Union
|
|
6
|
+
|
|
7
|
+
from lib import libsampler
|
|
8
|
+
|
|
9
|
+
class Sampler:
|
|
10
|
+
class Mode:
|
|
11
|
+
SEQUENTIAL = 0
|
|
12
|
+
IID = 1
|
|
13
|
+
|
|
14
|
+
def __init__(self, length: int, batch_size: int, mode: int, seed: int = 42):
|
|
15
|
+
# sampler
|
|
16
|
+
self.sampler = libsampler.Sampler.init(length, batch_size, mode, seed)
|
|
17
|
+
# indices
|
|
18
|
+
self.indices = np.zeros(batch_size, dtype=np.uint64)
|
|
19
|
+
|
|
20
|
+
# step
|
|
21
|
+
def next(self):
|
|
22
|
+
self.sampler.next(self.indices)
|
|
23
|
+
|
|
24
|
+
# iterator
|
|
25
|
+
def __iter__(self):
|
|
26
|
+
return self
|
|
27
|
+
def __next__(self):
|
|
28
|
+
self.next()
|
|
29
|
+
return self.indices
|
|
30
|
+
|
|
31
|
+
def run():
|
|
32
|
+
from itertools import islice
|
|
33
|
+
length = 10
|
|
34
|
+
batch_size = 4
|
|
35
|
+
n_steps = 5
|
|
36
|
+
|
|
37
|
+
print("=== Sampler SEQUENTIAL ===")
|
|
38
|
+
sampler = Sampler(length, batch_size, Sampler.Mode.SEQUENTIAL)
|
|
39
|
+
for indices in islice(sampler, n_steps):
|
|
40
|
+
print(indices)
|
|
41
|
+
|
|
42
|
+
print("=== Sampler IID ===")
|
|
43
|
+
sampler = Sampler(length, batch_size, Sampler.Mode.IID)
|
|
44
|
+
for indices in islice(sampler, n_steps):
|
|
45
|
+
print(indices)
|
|
46
|
+
|
|
47
|
+
# Benchmark, zig-sampler speeder 2.2x
|
|
48
|
+
def bench():
|
|
49
|
+
length = 1_000_000
|
|
50
|
+
batch_size = 8192
|
|
51
|
+
n_steps = 10_000
|
|
52
|
+
|
|
53
|
+
print("=== NumPy IID ===")
|
|
54
|
+
t0 = time.time()
|
|
55
|
+
batch = np.zeros(batch_size, dtype=np.uint64)
|
|
56
|
+
for _ in range(n_steps):
|
|
57
|
+
batch[:] = np.random.randint(0, length, size=batch_size, dtype=np.uint64)
|
|
58
|
+
print(f"{(time.time() - t0)*1000:.2f} ms")
|
|
59
|
+
|
|
60
|
+
print("=== Sampler IID ===")
|
|
61
|
+
t0 = time.time()
|
|
62
|
+
sampler = Sampler(length, batch_size, Sampler.Mode.IID)
|
|
63
|
+
for _ in range(n_steps):
|
|
64
|
+
sampler.next()
|
|
65
|
+
print(f"{(time.time() - t0)*1000:.2f} ms")
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
run()
|
|
69
|
+
bench()
|
|
@@ -26,12 +26,10 @@ class DataLoader:
|
|
|
26
26
|
if len(dataset) != len(labelset):
|
|
27
27
|
raise ValueError("dataset and labelset must have the same length")
|
|
28
28
|
|
|
29
|
-
self.rng = np.random.default_rng(seed)
|
|
30
|
-
|
|
31
29
|
self.indices = Queue(maxsize=prefetch_size)
|
|
32
30
|
self.batches = Queue(maxsize=prefetch_size)
|
|
33
31
|
|
|
34
|
-
self.sampler = Sampler(len(dataset), batch_size, mode)
|
|
32
|
+
self.sampler = Sampler(len(dataset), batch_size, mode, seed)
|
|
35
33
|
|
|
36
34
|
self.stop_signal = threading.Event()
|
|
37
35
|
|
|
@@ -49,8 +47,8 @@ class DataLoader:
|
|
|
49
47
|
Sample indices from the dataset and put them into the index queue.
|
|
50
48
|
"""
|
|
51
49
|
while not self.stop_signal.is_set():
|
|
52
|
-
self.sampler.next()
|
|
53
|
-
self.indices.put(self.sampler.
|
|
50
|
+
self.sampler.next()
|
|
51
|
+
self.indices.put(self.sampler.indices.copy())
|
|
54
52
|
|
|
55
53
|
def _prefetch(self, transform):
|
|
56
54
|
"""
|
|
@@ -73,7 +71,7 @@ class DataLoader:
|
|
|
73
71
|
return self
|
|
74
72
|
def __next__(self):
|
|
75
73
|
# debug: monitor bottlenecks
|
|
76
|
-
# print(self.indices.qsize(), self.
|
|
74
|
+
# print(self.indices.qsize(), self.batches.qsize())
|
|
77
75
|
return self.batches.get()
|
|
78
76
|
|
|
79
77
|
# statement
|
|
Binary file
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: loaderx
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: A record-based data runtime, focused on delivering extreme throughput and low latency
|
|
5
5
|
Author-email: Ben0i0d <ben0i0d@foxmail.com>
|
|
6
6
|
License: MIT License
|
|
7
7
|
|
|
@@ -25,16 +25,16 @@ License: MIT License
|
|
|
25
25
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
26
|
SOFTWARE.
|
|
27
27
|
|
|
28
|
-
Project-URL: Homepage, https://
|
|
29
|
-
Project-URL: Documentation, https://
|
|
30
|
-
Project-URL: Source, https://
|
|
31
|
-
Project-URL: Bug Tracker, https://
|
|
28
|
+
Project-URL: Homepage, https://codeberg.org/eoelab/loaderx
|
|
29
|
+
Project-URL: Documentation, https://codeberg.org/eoelab/loaderx
|
|
30
|
+
Project-URL: Source, https://codeberg.org/eoelab/loaderx
|
|
31
|
+
Project-URL: Bug Tracker, https://codeberg.org/eoelab/loaderx
|
|
32
32
|
Keywords: flax,python,dataloader
|
|
33
33
|
Classifier: Development Status :: 3 - Alpha
|
|
34
34
|
Classifier: Intended Audience :: Developers
|
|
35
35
|
Classifier: License :: OSI Approved :: MIT License
|
|
36
36
|
Classifier: Programming Language :: Python :: 3
|
|
37
|
-
Requires-Python:
|
|
37
|
+
Requires-Python: ==3.13
|
|
38
38
|
Description-Content-Type: text/markdown
|
|
39
39
|
License-File: LICENSE
|
|
40
40
|
Requires-Dist: numpy
|
|
@@ -45,19 +45,16 @@ Dynamic: license-file
|
|
|
45
45
|
# loaderx
|
|
46
46
|
A record-based data runtime, focused on delivering extreme throughput and low latency
|
|
47
47
|
|
|
48
|
-
**
|
|
48
|
+
**Only Python3.13_Linux_amd64**
|
|
49
49
|
|
|
50
50
|
## Sampler
|
|
51
51
|
a high-performance sampler implemented in Zig
|
|
52
|
-
### Build
|
|
53
|
-
zig build-lib -dynamic -OReleaseFast sampler/root.zig -femit-bin=loaderx/lib/libsampler.so
|
|
54
52
|
### Index Generator
|
|
55
53
|
1. Sequential generation: indices are produced by traversing the index space in order.
|
|
56
54
|
* Sliding traversal: indices are obtained using a fixed-size sliding window. Note that in this case, the index space is treated as a circular queue to avoid truncation at the tail.
|
|
57
55
|
2. Random generation: indices are sampled randomly from the index space.
|
|
58
56
|
* Global random: a set of samples is drawn randomly from the entire index space.
|
|
59
57
|
|
|
60
|
-
|
|
61
58
|
## zrecord
|
|
62
59
|
基于Zig实现一个并发友好、实现更简单、单机性能更优的 record 存储系统
|
|
63
60
|
|
|
@@ -85,27 +82,24 @@ zig build-lib -dynamic -OReleaseFast sampler/root.zig -femit-bin=loaderx/lib/lib
|
|
|
85
82
|
| flate | Deflate |
|
|
86
83
|
```
|
|
87
84
|
3. 通过offset支持高效随机访问
|
|
88
|
-
* offset表项是一个三元组 [[chunk_id :
|
|
89
|
-
* chunk_id为
|
|
90
|
-
* offset为
|
|
91
|
-
* physical_length为
|
|
92
|
-
*
|
|
85
|
+
* offset表项是一个三元组 [[chunk_id : u32, offset : u64, physical_length : u32], ...],长度为N。将外部索引空间(outside[0, N])指向一个实际存储地址
|
|
86
|
+
* chunk_id为u32,表示具体chunk
|
|
87
|
+
* offset为u64, 表示具体chunk内的偏移
|
|
88
|
+
* physical_length为u32,表示record数据大小
|
|
89
|
+
* offset表每项的字节大小最好为 8 的整数倍,提升对齐和缓存效率
|
|
90
|
+
* offset表是mmap访问模式,由于等长,直接将 idx 转换为 ptr + 16*idx
|
|
93
91
|
* 不同位宽表示的数值范围
|
|
94
92
|
```
|
|
95
93
|
u16 → 2^16 - 1 (65535)
|
|
96
|
-
u24 → 2^24 - 1 (16777215)
|
|
97
94
|
u32 → 2^32 - 1 (4.29e9)
|
|
98
95
|
u64 → 2^64 - 1 (1.84e19)
|
|
99
96
|
```
|
|
100
97
|
* 不同位宽表示的数据大小
|
|
101
98
|
```
|
|
102
99
|
u16 → 64 KiB
|
|
103
|
-
u24 → 16 MiB
|
|
104
100
|
u32 → 4 GiB
|
|
105
|
-
u40 → 1 TiB
|
|
106
101
|
u64 → 16 EiB
|
|
107
102
|
```
|
|
108
|
-
* offset表每项的字节大小最好为 8 的整数倍,提升对齐和缓存效率
|
|
109
103
|
```
|
|
110
104
|
outside idx (global)
|
|
111
105
|
↓
|
|
@@ -118,10 +112,9 @@ zig build-lib -dynamic -OReleaseFast sampler/root.zig -femit-bin=loaderx/lib/lib
|
|
|
118
112
|
* meta.json:描述全局元数据,包括全局参数与Field参数
|
|
119
113
|
```
|
|
120
114
|
{
|
|
121
|
-
"version": 1,
|
|
122
115
|
"length": 65536,
|
|
123
|
-
"chunks": {"num": 12, "size": 8192, "unfull": [8,
|
|
124
|
-
"fields": [{"name": "A", "dtype": "f32", "compress": "flate"}, {"name": "B", "dtype": "i32", "compress": "raw"}]
|
|
116
|
+
"chunks": { "num": 12, "size": 8192, "unfull": {[8, write_pos, record_count], ... } },
|
|
117
|
+
"fields": [ {"name": "A", "dtype": "f32", "compress": "flate"}, {"name": "B", "dtype": "i32", "compress": "raw"} ]
|
|
125
118
|
}
|
|
126
119
|
```
|
|
127
120
|
* Field_offset.zr:索引表
|
|
@@ -137,8 +130,8 @@ zig build-lib -dynamic -OReleaseFast sampler/root.zig -femit-bin=loaderx/lib/lib
|
|
|
137
130
|
```
|
|
138
131
|
dataset
|
|
139
132
|
├── meta.json
|
|
140
|
-
├──
|
|
141
|
-
├──
|
|
133
|
+
├── A.offset
|
|
134
|
+
├── B.offset
|
|
142
135
|
├── chunk
|
|
143
136
|
│ ├── 0.zr
|
|
144
137
|
└──└── 1.zr
|
|
@@ -168,6 +161,9 @@ dataset
|
|
|
168
161
|
1. mmap:维护全局offset、chunk文件的handle
|
|
169
162
|
* 初始化阶段注册,关闭前释放
|
|
170
163
|
* 对于full的chunk,使用mmap+close(只读),对于unfull的chunk使用mmap(读写)。一旦写满(writer提交),新建chunk并返回给writer,原mmap close。(避免过多占用fd)
|
|
164
|
+
* 对于chunk/offset文件,动态完成分块增长,基于ftruncate+mremap实现
|
|
165
|
+
* 扩容offset表,一次增长 1M项(16MiB)
|
|
166
|
+
* 扩容chunk文件,一次增长 1GiB
|
|
171
167
|
2. batch:维护每次返回batch的handle,也就是[ptr, logical_length]中ptr的合规性
|
|
172
168
|
* 工作线程完成后注册
|
|
173
169
|
* 必须调用函数来手动释放,释放时batch压入GC队列
|
|
@@ -175,9 +171,12 @@ dataset
|
|
|
175
171
|
#### 在线任务
|
|
176
172
|
|
|
177
173
|
1. 写任务:写入只允许chunk-level append-only,其余操作基于offset重定向,缺失的field构造为全为0的项(physical_length为0代表无)
|
|
178
|
-
1.
|
|
174
|
+
1. 追加:在chunk当前写入位置追加一个record,offset表更新length+1条目,全局长度增加
|
|
175
|
+
* 写满:写入完成时检查,如果record_count达到chunk_size, 新建chunk
|
|
176
|
+
* 写入越界:捕捉错误,并对chunk扩容
|
|
177
|
+
* offset[ length+1 ]不存在:捕捉错误,并对offset扩容
|
|
179
178
|
2. 修改:追加一个record,并修改offset项
|
|
180
|
-
3. 删除:删除offset项(将目标项替换为最后一项)
|
|
179
|
+
3. 删除:删除offset项(将目标项替换为最后一项),length减一(越界访问属于UB行为)
|
|
181
180
|
|
|
182
181
|
2. 读任务:用一个indices(索引数组),从原数组里访问指定位置的元素(gather)
|
|
183
182
|
1. 对于无压缩的数据,将直接返回文件内地址实现zero-copy
|
|
@@ -5,9 +5,9 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "loaderx"
|
|
7
7
|
dynamic = ["version"]
|
|
8
|
-
description = "
|
|
8
|
+
description = "A record-based data runtime, focused on delivering extreme throughput and low latency"
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
requires-python = "
|
|
10
|
+
requires-python = "==3.13"
|
|
11
11
|
authors = [
|
|
12
12
|
{name = "Ben0i0d", email = "ben0i0d@foxmail.com"},
|
|
13
13
|
]
|
|
@@ -31,7 +31,7 @@ packages = ["loaderx"]
|
|
|
31
31
|
version = {attr = "loaderx.__version__"}
|
|
32
32
|
|
|
33
33
|
[project.urls]
|
|
34
|
-
"Homepage" = "https://
|
|
35
|
-
"Documentation" = "https://
|
|
36
|
-
"Source" = "https://
|
|
37
|
-
"Bug Tracker" = "https://
|
|
34
|
+
"Homepage" = "https://codeberg.org/eoelab/loaderx"
|
|
35
|
+
"Documentation" = "https://codeberg.org/eoelab/loaderx"
|
|
36
|
+
"Source" = "https://codeberg.org/eoelab/loaderx"
|
|
37
|
+
"Bug Tracker" = "https://codeberg.org/eoelab/loaderx"
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import time
|
|
3
|
-
import ctypes
|
|
4
|
-
import numpy as np
|
|
5
|
-
from typing import Union
|
|
6
|
-
|
|
7
|
-
class Sampler:
|
|
8
|
-
class Mode:
|
|
9
|
-
SEQUENTIAL = 0
|
|
10
|
-
IID = 1
|
|
11
|
-
# lazy import
|
|
12
|
-
_lib = None
|
|
13
|
-
@classmethod
|
|
14
|
-
def _load_lib(cls):
|
|
15
|
-
if cls._lib is None:
|
|
16
|
-
# linux/windows/macos
|
|
17
|
-
names = ['lib/libsampler.so', 'lib/sampler.dll', 'lib/libsampler.dylib']
|
|
18
|
-
lib_paths = [os.path.join(os.path.dirname(__file__), name) for name in names]
|
|
19
|
-
|
|
20
|
-
for path in lib_paths:
|
|
21
|
-
if os.path.exists(path):
|
|
22
|
-
cls._lib = ctypes.CDLL(path)
|
|
23
|
-
cls._setup_function_signatures()
|
|
24
|
-
return cls._lib
|
|
25
|
-
|
|
26
|
-
@classmethod
|
|
27
|
-
def _setup_function_signatures(cls):
|
|
28
|
-
lib = cls._lib
|
|
29
|
-
|
|
30
|
-
# init
|
|
31
|
-
lib.init.argtypes = [
|
|
32
|
-
ctypes.c_uint64, # length
|
|
33
|
-
ctypes.c_uint32, # batch_size
|
|
34
|
-
ctypes.c_uint8, # mode
|
|
35
|
-
ctypes.c_uint64 # seed
|
|
36
|
-
]
|
|
37
|
-
lib.init.restype = ctypes.c_void_p
|
|
38
|
-
|
|
39
|
-
# deinit
|
|
40
|
-
lib.deinit.argtypes = [ctypes.c_void_p] # sampler
|
|
41
|
-
lib.deinit.restype = None
|
|
42
|
-
|
|
43
|
-
# next
|
|
44
|
-
lib.next.argtypes = [
|
|
45
|
-
ctypes.c_void_p, # sampler
|
|
46
|
-
ctypes.POINTER(ctypes.c_uint64) # batch_indices
|
|
47
|
-
]
|
|
48
|
-
lib.next.restype = None
|
|
49
|
-
|
|
50
|
-
def __init__(self, length: int, batch_size: int, mode: int, seed: int = 42):
|
|
51
|
-
# import lib
|
|
52
|
-
self.lib = self._load_lib()
|
|
53
|
-
|
|
54
|
-
# init sampler
|
|
55
|
-
self.ptr = self.lib.init(length, batch_size, mode, seed)
|
|
56
|
-
if not self.ptr:
|
|
57
|
-
raise RuntimeError(f"Failed to create sampler with params: length={length}, batch_size={batch_size}, mode={mode}, seed={seed}")
|
|
58
|
-
|
|
59
|
-
# batch_indices
|
|
60
|
-
self.batch_indices = np.zeros(batch_size, dtype=np.uint64)
|
|
61
|
-
|
|
62
|
-
# step
|
|
63
|
-
def next(self):
|
|
64
|
-
self.lib.next(self.ptr, self.batch_indices.ctypes.data_as(ctypes.POINTER(ctypes.c_uint64)))
|
|
65
|
-
|
|
66
|
-
# iterator
|
|
67
|
-
def __iter__(self):
|
|
68
|
-
return self
|
|
69
|
-
def __next__(self):
|
|
70
|
-
self.next()
|
|
71
|
-
return self.batch_indices
|
|
72
|
-
|
|
73
|
-
# close
|
|
74
|
-
def deinit(self):
|
|
75
|
-
self.lib.deinit(self.ptr)
|
|
76
|
-
def __del__(self):
|
|
77
|
-
if hasattr(self, 'ptr') and self.ptr:
|
|
78
|
-
self.deinit()
|
|
79
|
-
|
|
80
|
-
# Benchmark, zig-sampler is better
|
|
81
|
-
if __name__ == "__main__":
|
|
82
|
-
length = 1_000_000
|
|
83
|
-
batch_size = 8192
|
|
84
|
-
n_steps = 10_000
|
|
85
|
-
|
|
86
|
-
print("=== Python NumPy IID ===")
|
|
87
|
-
t0 = time.time()
|
|
88
|
-
batch = np.zeros(batch_size, dtype=np.uint64)
|
|
89
|
-
for _ in range(n_steps):
|
|
90
|
-
batch[:] = np.random.randint(0, length, size=batch_size, dtype=np.uint64)
|
|
91
|
-
print(f"{(time.time() - t0)*1000:.2f} ms")
|
|
92
|
-
|
|
93
|
-
print("=== Sampler IID ===")
|
|
94
|
-
t0 = time.time()
|
|
95
|
-
s = Sampler(length, batch_size, Sampler.Mode.IID)
|
|
96
|
-
for _ in range(n_steps):
|
|
97
|
-
s.next()
|
|
98
|
-
print(f"{(time.time() - t0)*1000:.2f} ms")
|
|
99
|
-
s.deinit()
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|