tilecdc 0.1.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.
- tilecdc-0.1.0/MANIFEST.in +2 -0
- tilecdc-0.1.0/PKG-INFO +20 -0
- tilecdc-0.1.0/README.md +3 -0
- tilecdc-0.1.0/_tilecdc.c +164 -0
- tilecdc-0.1.0/csrc/tilecdc.c +315 -0
- tilecdc-0.1.0/csrc/tilecdc.h +82 -0
- tilecdc-0.1.0/pyproject.toml +30 -0
- tilecdc-0.1.0/setup.cfg +4 -0
- tilecdc-0.1.0/setup.py +30 -0
- tilecdc-0.1.0/tests/test_python.py +93 -0
- tilecdc-0.1.0/tilecdc/__init__.py +81 -0
- tilecdc-0.1.0/tilecdc/__main__.py +37 -0
- tilecdc-0.1.0/tilecdc.egg-info/PKG-INFO +20 -0
- tilecdc-0.1.0/tilecdc.egg-info/SOURCES.txt +15 -0
- tilecdc-0.1.0/tilecdc.egg-info/dependency_links.txt +1 -0
- tilecdc-0.1.0/tilecdc.egg-info/entry_points.txt +2 -0
- tilecdc-0.1.0/tilecdc.egg-info/top_level.txt +1 -0
tilecdc-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tilecdc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Fast content-defined chunking on CPU matrix units (Intel AMX), with AVX-512 and portable fallbacks that give identical boundaries
|
|
5
|
+
Author: Ashutosh Rath
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/ashutosh-rath02/tilecdc
|
|
8
|
+
Keywords: deduplication,chunking,cdc,fastcdc,amx,avx512,backup
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: C
|
|
11
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Classifier: Topic :: System :: Archiving :: Backup
|
|
14
|
+
Classifier: Topic :: System :: Archiving :: Compression
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# tilecdc (Python)
|
|
19
|
+
|
|
20
|
+
Fast content-defined chunking on CPU matrix units. See the project README: https://github.com/ashutosh-rath02/tilecdc
|
tilecdc-0.1.0/README.md
ADDED
tilecdc-0.1.0/_tilecdc.c
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/* CPython binding for TileCDC. */
|
|
2
|
+
#define PY_SSIZE_T_CLEAN
|
|
3
|
+
#include <Python.h>
|
|
4
|
+
#include "structmember.h"
|
|
5
|
+
#include "tilecdc.h"
|
|
6
|
+
|
|
7
|
+
typedef struct {
|
|
8
|
+
PyObject_HEAD
|
|
9
|
+
tilecdc *t;
|
|
10
|
+
tilecdc_params p;
|
|
11
|
+
} ChunkerObject;
|
|
12
|
+
|
|
13
|
+
static void Chunker_dealloc(ChunkerObject *self) {
|
|
14
|
+
if (self->t) tilecdc_free(self->t);
|
|
15
|
+
Py_TYPE(self)->tp_free((PyObject *)self);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static int parse_impl(const char *s, tilecdc_impl *out) {
|
|
19
|
+
if (!s || !strcmp(s, "auto")) *out = TILECDC_IMPL_AUTO;
|
|
20
|
+
else if (!strcmp(s, "amx")) *out = TILECDC_IMPL_AMX;
|
|
21
|
+
else if (!strcmp(s, "avx512")) *out = TILECDC_IMPL_AVX512;
|
|
22
|
+
else if (!strcmp(s, "scalar")) *out = TILECDC_IMPL_SCALAR;
|
|
23
|
+
else return -1;
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static int Chunker_init(ChunkerObject *self, PyObject *args, PyObject *kw) {
|
|
28
|
+
static char *kwlist[] = {"min_size", "avg_size", "max_size", "seed", "norm", "impl", NULL};
|
|
29
|
+
tilecdc_params p;
|
|
30
|
+
tilecdc_default_params(&p);
|
|
31
|
+
Py_ssize_t mn = (Py_ssize_t)p.min_size, av = (Py_ssize_t)p.avg_size, mx = (Py_ssize_t)p.max_size;
|
|
32
|
+
PyObject *seed_obj = NULL;
|
|
33
|
+
int norm = p.norm;
|
|
34
|
+
const char *impl = "auto";
|
|
35
|
+
if (!PyArg_ParseTupleAndKeywords(args, kw, "|nnnOis", kwlist, &mn, &av, &mx, &seed_obj, &norm, &impl)) return -1;
|
|
36
|
+
if (mn <= 0 || av <= 0 || mx <= 0) { PyErr_SetString(PyExc_ValueError, "sizes must be positive"); return -1; }
|
|
37
|
+
unsigned long long seed = 0;
|
|
38
|
+
if (seed_obj) {
|
|
39
|
+
seed = PyLong_AsUnsignedLongLong(seed_obj);
|
|
40
|
+
if (PyErr_Occurred()) { PyErr_Clear(); PyErr_SetString(PyExc_ValueError, "seed must be an integer in [0, 2**64)"); return -1; }
|
|
41
|
+
}
|
|
42
|
+
p.min_size = (size_t)mn; p.avg_size = (size_t)av; p.max_size = (size_t)mx;
|
|
43
|
+
p.seed = seed; p.norm = norm;
|
|
44
|
+
if (parse_impl(impl, &p.impl)) { PyErr_SetString(PyExc_ValueError, "impl must be auto, amx, avx512 or scalar"); return -1; }
|
|
45
|
+
tilecdc *t = tilecdc_new(&p); /* build the new chunker before touching the old one */
|
|
46
|
+
if (!t) {
|
|
47
|
+
if (p.impl != TILECDC_IMPL_AUTO && !tilecdc_impl_available(p.impl))
|
|
48
|
+
PyErr_Format(PyExc_RuntimeError, "implementation '%s' is not available on this CPU", impl);
|
|
49
|
+
else
|
|
50
|
+
PyErr_Format(PyExc_ValueError, "invalid parameters: need 64 <= min_size < avg_size <= max_size, avg_size <= %d, 0 <= norm <= 4", TILECDC_MAX_AVG);
|
|
51
|
+
return -1;
|
|
52
|
+
}
|
|
53
|
+
if (self->t) tilecdc_free(self->t);
|
|
54
|
+
self->t = t;
|
|
55
|
+
self->p = p;
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
static int ready(ChunkerObject *self) {
|
|
60
|
+
if (self->t) return 1;
|
|
61
|
+
PyErr_SetString(PyExc_RuntimeError, "Chunker is not initialised");
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
static PyObject *Chunker_cut(ChunkerObject *self, PyObject *arg) {
|
|
66
|
+
if (!ready(self)) return NULL;
|
|
67
|
+
Py_buffer b;
|
|
68
|
+
if (PyObject_GetBuffer(arg, &b, PyBUF_SIMPLE) < 0) return NULL;
|
|
69
|
+
size_t r;
|
|
70
|
+
Py_BEGIN_ALLOW_THREADS
|
|
71
|
+
r = tilecdc_cut(self->t, (const uint8_t *)b.buf, (size_t)b.len);
|
|
72
|
+
Py_END_ALLOW_THREADS
|
|
73
|
+
PyBuffer_Release(&b);
|
|
74
|
+
return PyLong_FromSize_t(r);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* chunk(data) -> list of chunk lengths */
|
|
78
|
+
static PyObject *Chunker_chunk(ChunkerObject *self, PyObject *arg) {
|
|
79
|
+
if (!ready(self)) return NULL;
|
|
80
|
+
Py_buffer b;
|
|
81
|
+
if (PyObject_GetBuffer(arg, &b, PyBUF_SIMPLE) < 0) return NULL;
|
|
82
|
+
size_t cap = (size_t)b.len / self->p.min_size + 2;
|
|
83
|
+
size_t *ends = PyMem_RawMalloc(cap * sizeof(size_t));
|
|
84
|
+
if (!ends) { PyBuffer_Release(&b); return PyErr_NoMemory(); }
|
|
85
|
+
size_t n;
|
|
86
|
+
Py_BEGIN_ALLOW_THREADS
|
|
87
|
+
n = tilecdc_chunk_all(self->t, (const uint8_t *)b.buf, (size_t)b.len, ends, cap);
|
|
88
|
+
Py_END_ALLOW_THREADS
|
|
89
|
+
PyBuffer_Release(&b);
|
|
90
|
+
PyObject *out = PyList_New((Py_ssize_t)n);
|
|
91
|
+
if (!out) { PyMem_RawFree(ends); return NULL; }
|
|
92
|
+
size_t s = 0;
|
|
93
|
+
for (size_t i = 0; i < n; i++) {
|
|
94
|
+
PyObject *v = PyLong_FromSize_t(ends[i] - s);
|
|
95
|
+
if (!v) { Py_DECREF(out); PyMem_RawFree(ends); return NULL; }
|
|
96
|
+
PyList_SET_ITEM(out, (Py_ssize_t)i, v);
|
|
97
|
+
s = ends[i];
|
|
98
|
+
}
|
|
99
|
+
PyMem_RawFree(ends);
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
static PyObject *Chunker_get_impl(ChunkerObject *self, void *c) { (void)c; if (!ready(self)) return NULL; return PyUnicode_FromString(tilecdc_impl_name(tilecdc_get_impl(self->t))); }
|
|
104
|
+
static PyObject *Chunker_get_min(ChunkerObject *self, void *c) { (void)c; return PyLong_FromSize_t(self->p.min_size); }
|
|
105
|
+
static PyObject *Chunker_get_avg(ChunkerObject *self, void *c) { (void)c; return PyLong_FromSize_t(self->p.avg_size); }
|
|
106
|
+
static PyObject *Chunker_get_max(ChunkerObject *self, void *c) { (void)c; return PyLong_FromSize_t(self->p.max_size); }
|
|
107
|
+
|
|
108
|
+
static PyGetSetDef Chunker_getset[] = {
|
|
109
|
+
{"impl", (getter)Chunker_get_impl, NULL, "implementation in use: amx, avx512 or scalar", NULL},
|
|
110
|
+
{"min_size", (getter)Chunker_get_min, NULL, NULL, NULL},
|
|
111
|
+
{"avg_size", (getter)Chunker_get_avg, NULL, NULL, NULL},
|
|
112
|
+
{"max_size", (getter)Chunker_get_max, NULL, NULL, NULL},
|
|
113
|
+
{NULL, NULL, NULL, NULL, NULL},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
static PyMethodDef Chunker_methods[] = {
|
|
117
|
+
{"cut", (PyCFunction)Chunker_cut, METH_O, "cut(data) -> length of the first chunk (pass >= max_size bytes unless at end of stream)"},
|
|
118
|
+
{"chunk", (PyCFunction)Chunker_chunk, METH_O, "chunk(data) -> list of chunk lengths covering all of data"},
|
|
119
|
+
{NULL, NULL, 0, NULL},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
static PyTypeObject ChunkerType = {
|
|
123
|
+
PyVarObject_HEAD_INIT(NULL, 0)
|
|
124
|
+
.tp_name = "tilecdc._tilecdc.Chunker",
|
|
125
|
+
.tp_basicsize = sizeof(ChunkerObject),
|
|
126
|
+
.tp_dealloc = (destructor)Chunker_dealloc,
|
|
127
|
+
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
|
|
128
|
+
.tp_doc = "Chunker(min_size=2048, avg_size=8192, max_size=65536, seed=0, norm=1, impl='auto')",
|
|
129
|
+
.tp_methods = Chunker_methods,
|
|
130
|
+
.tp_getset = Chunker_getset,
|
|
131
|
+
.tp_init = (initproc)Chunker_init,
|
|
132
|
+
.tp_new = PyType_GenericNew,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
static PyObject *py_available(PyObject *self, PyObject *unused) {
|
|
136
|
+
(void)self; (void)unused;
|
|
137
|
+
PyObject *l = PyList_New(0);
|
|
138
|
+
tilecdc_impl all[3] = {TILECDC_IMPL_AMX, TILECDC_IMPL_AVX512, TILECDC_IMPL_SCALAR};
|
|
139
|
+
for (int i = 0; i < 3; i++)
|
|
140
|
+
if (tilecdc_impl_available(all[i])) {
|
|
141
|
+
PyObject *s = PyUnicode_FromString(tilecdc_impl_name(all[i]));
|
|
142
|
+
PyList_Append(l, s);
|
|
143
|
+
Py_DECREF(s);
|
|
144
|
+
}
|
|
145
|
+
return l;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
static PyMethodDef module_methods[] = {
|
|
149
|
+
{"available_impls", py_available, METH_NOARGS, "implementations usable on this CPU, fastest first"},
|
|
150
|
+
{NULL, NULL, 0, NULL},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
static struct PyModuleDef moddef = {PyModuleDef_HEAD_INIT, "_tilecdc", "TileCDC native core", -1, module_methods, NULL, NULL, NULL, NULL};
|
|
154
|
+
|
|
155
|
+
PyMODINIT_FUNC PyInit__tilecdc(void) {
|
|
156
|
+
if (PyType_Ready(&ChunkerType) < 0) return NULL;
|
|
157
|
+
PyObject *m = PyModule_Create(&moddef);
|
|
158
|
+
if (!m) return NULL;
|
|
159
|
+
Py_INCREF(&ChunkerType);
|
|
160
|
+
PyModule_AddObject(m, "Chunker", (PyObject *)&ChunkerType);
|
|
161
|
+
PyModule_AddStringConstant(m, "__version__", TILECDC_VERSION);
|
|
162
|
+
PyModule_AddIntConstant(m, "MAX_AVG_SIZE", TILECDC_MAX_AVG);
|
|
163
|
+
return m;
|
|
164
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* TileCDC: content-defined chunking on CPU matrix units (see include/tilecdc.h
|
|
3
|
+
* for the normative definition). Three implementations of one function:
|
|
4
|
+
*
|
|
5
|
+
* scalar reference, any CPU
|
|
6
|
+
* avx512 AVX-512 VBMI + VNNI: vpermb builds 4-byte sliding windows, vpdpbusd
|
|
7
|
+
* multiplies them by the key (12 of each per 16 hashes)
|
|
8
|
+
* amx Intel AMX: a tile load with a 16-byte stride over 64-byte rows makes
|
|
9
|
+
* the A tile a Hankel matrix of the byte stream; one TDPBUSD with a
|
|
10
|
+
* constant Toeplitz key tile gives 256 window hashes
|
|
11
|
+
*
|
|
12
|
+
* All three consume the same S-box-substituted block and produce bit-identical
|
|
13
|
+
* candidate bitmaps, so chunk boundaries never depend on the hardware.
|
|
14
|
+
*/
|
|
15
|
+
#define _GNU_SOURCE
|
|
16
|
+
#include "tilecdc.h"
|
|
17
|
+
|
|
18
|
+
#include <cpuid.h>
|
|
19
|
+
#include <errno.h>
|
|
20
|
+
#include <immintrin.h>
|
|
21
|
+
#include <pthread.h>
|
|
22
|
+
#include <stdlib.h>
|
|
23
|
+
#include <string.h>
|
|
24
|
+
#include <sys/syscall.h>
|
|
25
|
+
#include <unistd.h>
|
|
26
|
+
|
|
27
|
+
#define W TILECDC_WINDOW
|
|
28
|
+
#define BLOCK 1024 /* hashes per block */
|
|
29
|
+
#define BLKBYTES (BLOCK + 64) /* bytes needed per block (window + slack) */
|
|
30
|
+
#define MIXC 0x9E3779B1u
|
|
31
|
+
|
|
32
|
+
struct tilecdc {
|
|
33
|
+
size_t min, avg, max;
|
|
34
|
+
uint32_t smask, lmask; /* top-bit masks for the strict / loose test */
|
|
35
|
+
tilecdc_impl impl;
|
|
36
|
+
int8_t key[W];
|
|
37
|
+
uint8_t sbox[256] __attribute__((aligned(64)));
|
|
38
|
+
int8_t btile[16][64] __attribute__((aligned(64))); /* Toeplitz key tile for AMX */
|
|
39
|
+
uint8_t idx[12][64] __attribute__((aligned(64))); /* vpermb sliding-window indices */
|
|
40
|
+
int32_t keyq[12]; /* KEY[4g..4g+3] packed */
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
typedef void (*block_fn)(const tilecdc *t, const uint8_t *y, uint64_t S[16], uint64_t L[16]);
|
|
44
|
+
|
|
45
|
+
/* ------------------------------ constants ------------------------------ */
|
|
46
|
+
static uint64_t splitmix64(uint64_t *s) {
|
|
47
|
+
uint64_t z = (*s += 0x9E3779B97F4A7C15ull);
|
|
48
|
+
z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ull;
|
|
49
|
+
z = (z ^ (z >> 27)) * 0x94D049BB133111EBull;
|
|
50
|
+
return z ^ (z >> 31);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static void derive(tilecdc *t, uint64_t seed) {
|
|
54
|
+
uint64_t s = seed ^ 0x54696C6543444321ull; /* "TileCDC!" */
|
|
55
|
+
for (int i = 0; i < 256; i++) t->sbox[i] = (uint8_t)i;
|
|
56
|
+
for (int i = 255; i > 0; i--) {
|
|
57
|
+
int j = (int)(splitmix64(&s) % (uint64_t)(i + 1));
|
|
58
|
+
uint8_t x = t->sbox[i]; t->sbox[i] = t->sbox[j]; t->sbox[j] = x;
|
|
59
|
+
}
|
|
60
|
+
for (int d = 0; d < W; d++) {
|
|
61
|
+
int v;
|
|
62
|
+
do { v = (int)(splitmix64(&s) & 0xFF) - 128; } while (v == 0 || v == -128);
|
|
63
|
+
t->key[d] = (int8_t)v;
|
|
64
|
+
}
|
|
65
|
+
/* AMX: C[r][j] = sum_k A[r][k] * B[k][j] with A[r][k] = y[16r + k];
|
|
66
|
+
B[k][j] = KEY[k - j] gives C[r][j] = h(16r + j). TDPBUSD wants B packed
|
|
67
|
+
as 16 rows of (16 columns x 4 consecutive k). */
|
|
68
|
+
memset(t->btile, 0, sizeof t->btile);
|
|
69
|
+
for (int k = 0; k < 64; k++)
|
|
70
|
+
for (int j = 0; j < 16; j++) {
|
|
71
|
+
int d = k - j;
|
|
72
|
+
if (d >= 0 && d < W) t->btile[k / 4][j * 4 + k % 4] = t->key[d];
|
|
73
|
+
}
|
|
74
|
+
/* AVX-512: lane k of group g holds bytes y[b + 4g + k .. +3] */
|
|
75
|
+
for (int g = 0; g < 12; g++) {
|
|
76
|
+
for (int k = 0; k < 16; k++)
|
|
77
|
+
for (int j = 0; j < 4; j++) t->idx[g][k * 4 + j] = (uint8_t)(4 * g + k + j);
|
|
78
|
+
uint32_t q = 0;
|
|
79
|
+
for (int j = 0; j < 4; j++) q |= (uint32_t)(uint8_t)t->key[4 * g + j] << (8 * j);
|
|
80
|
+
t->keyq[g] = (int32_t)q;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* ------------------------------ CPU support ------------------------------ */
|
|
85
|
+
static int avx512_ok, amx_ok;
|
|
86
|
+
static pthread_once_t detect_once = PTHREAD_ONCE_INIT;
|
|
87
|
+
|
|
88
|
+
static void detect(void) {
|
|
89
|
+
__builtin_cpu_init();
|
|
90
|
+
avx512_ok = __builtin_cpu_supports("avx512f") && __builtin_cpu_supports("avx512bw") &&
|
|
91
|
+
__builtin_cpu_supports("avx512vbmi") && __builtin_cpu_supports("avx512vnni");
|
|
92
|
+
unsigned a, b, c, d;
|
|
93
|
+
if (avx512_ok && __get_cpuid_count(7, 0, &a, &b, &c, &d) && (d & (1u << 24)) && (d & (1u << 25))) {
|
|
94
|
+
/* ARCH_REQ_XCOMP_PERM, XFEATURE_XTILEDATA: ask the kernel for tile state */
|
|
95
|
+
amx_ok = syscall(SYS_arch_prctl, 0x1023, 18) == 0;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
static int has_avx512(void) { pthread_once(&detect_once, detect); return avx512_ok; }
|
|
99
|
+
static int has_amx(void) { pthread_once(&detect_once, detect); return amx_ok; }
|
|
100
|
+
|
|
101
|
+
int tilecdc_impl_available(tilecdc_impl impl) {
|
|
102
|
+
switch (impl) {
|
|
103
|
+
case TILECDC_IMPL_SCALAR: return 1;
|
|
104
|
+
case TILECDC_IMPL_AVX512: return has_avx512();
|
|
105
|
+
case TILECDC_IMPL_AMX: return has_amx();
|
|
106
|
+
case TILECDC_IMPL_AUTO: return 1;
|
|
107
|
+
}
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const char *tilecdc_impl_name(tilecdc_impl impl) {
|
|
112
|
+
switch (impl) {
|
|
113
|
+
case TILECDC_IMPL_SCALAR: return "scalar";
|
|
114
|
+
case TILECDC_IMPL_AVX512: return "avx512";
|
|
115
|
+
case TILECDC_IMPL_AMX: return "amx";
|
|
116
|
+
default: return "auto";
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* ------------------------------ S-box ------------------------------ */
|
|
121
|
+
static void sbox_scalar(const tilecdc *t, const uint8_t *src, uint8_t *dst, size_t n) {
|
|
122
|
+
for (size_t i = 0; i < n; i++) dst[i] = t->sbox[src[i]];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
__attribute__((target("avx512f,avx512bw,avx512vbmi")))
|
|
126
|
+
static void sbox_avx512(const tilecdc *t, const uint8_t *src, uint8_t *dst, size_t n) { /* n % 64 == 0 */
|
|
127
|
+
__m512i t0 = _mm512_load_si512(t->sbox), t1 = _mm512_load_si512(t->sbox + 64);
|
|
128
|
+
__m512i t2 = _mm512_load_si512(t->sbox + 128), t3 = _mm512_load_si512(t->sbox + 192);
|
|
129
|
+
for (size_t i = 0; i < n; i += 64) {
|
|
130
|
+
__m512i x = _mm512_loadu_si512(src + i);
|
|
131
|
+
__m512i lo = _mm512_permutex2var_epi8(t0, x, t1), hi = _mm512_permutex2var_epi8(t2, x, t3);
|
|
132
|
+
_mm512_storeu_si512(dst + i, _mm512_mask_blend_epi8(_mm512_movepi8_mask(x), lo, hi));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* ------------------------------ hash blocks ------------------------------ */
|
|
137
|
+
/* y holds BLKBYTES substituted bytes starting at hash index i0; set bit b of
|
|
138
|
+
S/L when hash i0+b passes the strict/loose test. */
|
|
139
|
+
static void block_scalar(const tilecdc *t, const uint8_t *y, uint64_t S[16], uint64_t L[16]) {
|
|
140
|
+
memset(S, 0, 16 * 8); memset(L, 0, 16 * 8);
|
|
141
|
+
for (int b = 0; b < BLOCK; b++) {
|
|
142
|
+
int32_t h = 0;
|
|
143
|
+
for (int d = 0; d < W; d++) h += (int32_t)t->key[d] * (int32_t)y[b + d];
|
|
144
|
+
uint32_t m = (uint32_t)h * MIXC;
|
|
145
|
+
if (!(m & t->smask)) S[b >> 6] |= 1ull << (b & 63);
|
|
146
|
+
if (!(m & t->lmask)) L[b >> 6] |= 1ull << (b & 63);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
__attribute__((target("avx512f,avx512bw,avx512vbmi,avx512vnni")))
|
|
151
|
+
static void block_avx512(const tilecdc *t, const uint8_t *y, uint64_t S[16], uint64_t L[16]) {
|
|
152
|
+
__m512i idx[12], kq[12];
|
|
153
|
+
for (int g = 0; g < 12; g++) { idx[g] = _mm512_load_si512(t->idx[g]); kq[g] = _mm512_set1_epi32(t->keyq[g]); }
|
|
154
|
+
const __m512i mul = _mm512_set1_epi32((int)MIXC), sm = _mm512_set1_epi32((int)t->smask), lm = _mm512_set1_epi32((int)t->lmask);
|
|
155
|
+
for (int w = 0; w < 16; w++) {
|
|
156
|
+
uint64_t s64 = 0, l64 = 0;
|
|
157
|
+
for (int u = 0; u < 4; u++) {
|
|
158
|
+
int b = w * 64 + u * 16;
|
|
159
|
+
__m512i base = _mm512_loadu_si512(y + b);
|
|
160
|
+
__m512i acc0 = _mm512_setzero_si512(), acc1 = _mm512_setzero_si512();
|
|
161
|
+
for (int g = 0; g < 12; g += 2) {
|
|
162
|
+
acc0 = _mm512_dpbusd_epi32(acc0, _mm512_permutexvar_epi8(idx[g], base), kq[g]);
|
|
163
|
+
acc1 = _mm512_dpbusd_epi32(acc1, _mm512_permutexvar_epi8(idx[g + 1], base), kq[g + 1]);
|
|
164
|
+
}
|
|
165
|
+
__m512i m = _mm512_mullo_epi32(_mm512_add_epi32(acc0, acc1), mul);
|
|
166
|
+
s64 |= (uint64_t)_mm512_testn_epi32_mask(m, sm) << (16 * u);
|
|
167
|
+
l64 |= (uint64_t)_mm512_testn_epi32_mask(m, lm) << (16 * u);
|
|
168
|
+
}
|
|
169
|
+
S[w] = s64; L[w] = l64;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
typedef struct { uint8_t palette, start; uint8_t r[14]; uint16_t colsb[16]; uint8_t rows[16]; } __attribute__((packed)) tilecfg;
|
|
174
|
+
|
|
175
|
+
/* Configure tiles for TileCDC, remembering any configuration the calling thread
|
|
176
|
+
had, and put it back afterwards. Tile register contents are not preserved:
|
|
177
|
+
do not call TileCDC while holding live data in tile registers. */
|
|
178
|
+
__attribute__((target("amx-tile")))
|
|
179
|
+
static void amx_configure(tilecfg *saved) {
|
|
180
|
+
tilecfg c;
|
|
181
|
+
memset(saved, 0, sizeof *saved);
|
|
182
|
+
_tile_storeconfig(saved);
|
|
183
|
+
memset(&c, 0, sizeof c);
|
|
184
|
+
c.palette = 1;
|
|
185
|
+
for (int i = 0; i < 8; i++) { c.colsb[i] = 64; c.rows[i] = 16; }
|
|
186
|
+
_tile_loadconfig(&c);
|
|
187
|
+
}
|
|
188
|
+
__attribute__((target("amx-tile")))
|
|
189
|
+
static void amx_release(const tilecfg *saved) {
|
|
190
|
+
if (saved->palette) _tile_loadconfig(saved);
|
|
191
|
+
else _tile_release();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
__attribute__((target("amx-tile,amx-int8,avx512f,avx512bw")))
|
|
195
|
+
static void block_amx(const tilecdc *t, const uint8_t *y, uint64_t S[16], uint64_t L[16]) {
|
|
196
|
+
int32_t C[4][16][16] __attribute__((aligned(64)));
|
|
197
|
+
_tile_loadd(7, t->btile, 64);
|
|
198
|
+
_tile_zero(0); _tile_zero(1); _tile_zero(2); _tile_zero(3);
|
|
199
|
+
/* stride 16 < row length 64: row r of each A tile = y[256q + 16r .. +64) */
|
|
200
|
+
_tile_loadd(4, y, 16); _tile_dpbusd(0, 4, 7);
|
|
201
|
+
_tile_loadd(5, y + 256, 16); _tile_dpbusd(1, 5, 7);
|
|
202
|
+
_tile_loadd(6, y + 512, 16); _tile_dpbusd(2, 6, 7);
|
|
203
|
+
_tile_loadd(4, y + 768, 16); _tile_dpbusd(3, 4, 7);
|
|
204
|
+
_tile_stored(0, C[0], 64); _tile_stored(1, C[1], 64); _tile_stored(2, C[2], 64); _tile_stored(3, C[3], 64);
|
|
205
|
+
const int32_t *h = &C[0][0][0]; /* row-major: h[b] = hash i0 + b */
|
|
206
|
+
const __m512i mul = _mm512_set1_epi32((int)MIXC), sm = _mm512_set1_epi32((int)t->smask), lm = _mm512_set1_epi32((int)t->lmask);
|
|
207
|
+
for (int w = 0; w < 16; w++) {
|
|
208
|
+
uint64_t s64 = 0, l64 = 0;
|
|
209
|
+
for (int u = 0; u < 4; u++) {
|
|
210
|
+
__m512i m = _mm512_mullo_epi32(_mm512_load_si512(h + w * 64 + u * 16), mul);
|
|
211
|
+
s64 |= (uint64_t)_mm512_testn_epi32_mask(m, sm) << (16 * u);
|
|
212
|
+
l64 |= (uint64_t)_mm512_testn_epi32_mask(m, lm) << (16 * u);
|
|
213
|
+
}
|
|
214
|
+
S[w] = s64; L[w] = l64;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* ------------------------------ cut search ------------------------------ */
|
|
219
|
+
static size_t cut_core(tilecdc *t, const uint8_t *p, size_t n) {
|
|
220
|
+
if (n <= t->min) return n;
|
|
221
|
+
size_t end = n < t->max ? n : t->max, mid = n < t->avg ? n : t->avg;
|
|
222
|
+
block_fn blk = t->impl == TILECDC_IMPL_AMX ? block_amx : t->impl == TILECDC_IMPL_AVX512 ? block_avx512 : block_scalar;
|
|
223
|
+
int vec = t->impl != TILECDC_IMPL_SCALAR;
|
|
224
|
+
uint8_t raw[BLKBYTES] __attribute__((aligned(64)));
|
|
225
|
+
uint8_t y[BLKBYTES] __attribute__((aligned(64)));
|
|
226
|
+
uint64_t S[16], L[16];
|
|
227
|
+
for (size_t i0 = t->min + 1 - W;; i0 += BLOCK) {
|
|
228
|
+
size_t c0 = i0 + W; /* chunk end tested by bit 0 */
|
|
229
|
+
if (c0 > end) return end;
|
|
230
|
+
/* substitute the bytes of this block; never read past n */
|
|
231
|
+
const uint8_t *src = p + i0;
|
|
232
|
+
size_t avail = n - i0;
|
|
233
|
+
if (avail < BLKBYTES) { memcpy(raw, src, avail); memset(raw + avail, 0, BLKBYTES - avail); src = raw; }
|
|
234
|
+
if (vec) sbox_avx512(t, src, y, BLKBYTES);
|
|
235
|
+
else sbox_scalar(t, src, y, BLKBYTES);
|
|
236
|
+
blk(t, y, S, L);
|
|
237
|
+
for (int w = 0; w < 16; w++) {
|
|
238
|
+
size_t cw = c0 + (size_t)w * 64; /* chunk end for bit 0 of this word */
|
|
239
|
+
if (cw > end) return end;
|
|
240
|
+
uint64_t sel_s = 0, sel_l = 0;
|
|
241
|
+
/* bits whose end c = cw + b lies in (min, mid] are strict, (mid, end] loose */
|
|
242
|
+
size_t ns = mid >= cw ? mid - cw + 1 : 0; /* number of strict-region bits */
|
|
243
|
+
size_t ne = end >= cw ? end - cw + 1 : 0; /* bits up to end */
|
|
244
|
+
uint64_t ms = ns >= 64 ? ~0ull : ((1ull << ns) - 1);
|
|
245
|
+
uint64_t me = ne >= 64 ? ~0ull : ((1ull << ne) - 1);
|
|
246
|
+
sel_s = S[w] & ms;
|
|
247
|
+
if (sel_s) return cw + (size_t)__builtin_ctzll(sel_s);
|
|
248
|
+
sel_l = L[w] & me & ~ms;
|
|
249
|
+
if (sel_l) {
|
|
250
|
+
/* a strict hit later in this word cannot precede a loose hit
|
|
251
|
+
only if the strict region already ended: guaranteed by ~ms */
|
|
252
|
+
return cw + (size_t)__builtin_ctzll(sel_l);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
size_t tilecdc_cut(tilecdc *t, const uint8_t *data, size_t len) {
|
|
259
|
+
if (t->impl == TILECDC_IMPL_AMX && len > t->min) {
|
|
260
|
+
tilecfg saved;
|
|
261
|
+
amx_configure(&saved);
|
|
262
|
+
size_t r = cut_core(t, data, len);
|
|
263
|
+
amx_release(&saved);
|
|
264
|
+
return r;
|
|
265
|
+
}
|
|
266
|
+
return cut_core(t, data, len);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
size_t tilecdc_chunk_all(tilecdc *t, const uint8_t *data, size_t len, size_t *ends, size_t cap) {
|
|
270
|
+
size_t n = 0, s = 0;
|
|
271
|
+
tilecfg saved;
|
|
272
|
+
if (t->impl == TILECDC_IMPL_AMX) amx_configure(&saved);
|
|
273
|
+
while (s < len) {
|
|
274
|
+
s += cut_core(t, data + s, len - s);
|
|
275
|
+
if (n < cap) ends[n] = s;
|
|
276
|
+
n++;
|
|
277
|
+
}
|
|
278
|
+
if (t->impl == TILECDC_IMPL_AMX) amx_release(&saved);
|
|
279
|
+
return n;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/* ------------------------------ setup ------------------------------ */
|
|
283
|
+
void tilecdc_default_params(tilecdc_params *p) {
|
|
284
|
+
p->min_size = 2048;
|
|
285
|
+
p->avg_size = 8192;
|
|
286
|
+
p->max_size = 65536;
|
|
287
|
+
p->seed = 0;
|
|
288
|
+
p->norm = 1;
|
|
289
|
+
p->impl = TILECDC_IMPL_AUTO;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
tilecdc *tilecdc_new(const tilecdc_params *params) {
|
|
293
|
+
tilecdc_params p;
|
|
294
|
+
if (params) p = *params; else tilecdc_default_params(&p);
|
|
295
|
+
if (p.min_size < 64 || p.avg_size <= p.min_size || p.max_size < p.avg_size) { errno = EINVAL; return NULL; }
|
|
296
|
+
if (p.avg_size > TILECDC_MAX_AVG || p.norm < 0 || p.norm > 4) { errno = EINVAL; return NULL; }
|
|
297
|
+
int lg = 63 - __builtin_clzll((unsigned long long)p.avg_size);
|
|
298
|
+
int sb = lg + p.norm, lb = lg - p.norm - 1;
|
|
299
|
+
if (sb > 31 || lb < 1) { errno = EINVAL; return NULL; }
|
|
300
|
+
tilecdc_impl impl = p.impl;
|
|
301
|
+
if (impl == TILECDC_IMPL_AUTO) impl = has_amx() ? TILECDC_IMPL_AMX : has_avx512() ? TILECDC_IMPL_AVX512 : TILECDC_IMPL_SCALAR;
|
|
302
|
+
else if (!tilecdc_impl_available(impl)) { errno = ENOTSUP; return NULL; }
|
|
303
|
+
tilecdc *t = aligned_alloc(64, (sizeof(tilecdc) + 63) & ~(size_t)63);
|
|
304
|
+
if (!t) return NULL;
|
|
305
|
+
memset(t, 0, sizeof *t);
|
|
306
|
+
t->min = p.min_size; t->avg = p.avg_size; t->max = p.max_size;
|
|
307
|
+
t->smask = 0xFFFFFFFFu << (32 - sb);
|
|
308
|
+
t->lmask = 0xFFFFFFFFu << (32 - lb);
|
|
309
|
+
t->impl = impl;
|
|
310
|
+
derive(t, p.seed);
|
|
311
|
+
return t;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
void tilecdc_free(tilecdc *t) { free(t); }
|
|
315
|
+
tilecdc_impl tilecdc_get_impl(const tilecdc *t) { return t->impl; }
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* TileCDC: content-defined chunking on CPU matrix units.
|
|
3
|
+
*
|
|
4
|
+
* TileCDC computes a windowed fingerprint at every byte offset with Intel AMX
|
|
5
|
+
* tile instructions: a tile load whose row stride (16 bytes) is shorter than
|
|
6
|
+
* its rows (64 bytes) forms a Hankel matrix of the byte stream without
|
|
7
|
+
* copying, and one TDPBUSD against a constant Toeplitz key tile yields 256
|
|
8
|
+
* window hashes. Boundaries follow FastCDC-style normalized chunking.
|
|
9
|
+
*
|
|
10
|
+
* The chunking function is defined independently of the hardware (below), and
|
|
11
|
+
* the AMX, AVX-512 and scalar implementations return identical boundaries.
|
|
12
|
+
*
|
|
13
|
+
* Definition (seed-dependent constants KEY[48] in [-127,127]\{0} and a byte
|
|
14
|
+
* permutation SBOX, both derived from the seed with splitmix64):
|
|
15
|
+
* y[j] = SBOX[x[j]]
|
|
16
|
+
* h(i) = sum_{d=0}^{47} KEY[d] * y[i+d] (exact, int32)
|
|
17
|
+
* m(i) = (uint32) h(i) * 0x9E3779B1 (mod 2^32)
|
|
18
|
+
* a cut is allowed after byte c-1 (chunk end c) under b bits when the top b
|
|
19
|
+
* bits of m(c-48) are zero.
|
|
20
|
+
* For a buffer of n bytes: if n <= min the chunk is the whole buffer;
|
|
21
|
+
* otherwise the chunk ends at the first allowed c in (min, min(n,avg)] under
|
|
22
|
+
* the strict bits, else the first in (min(n,avg), min(n,max)] under the
|
|
23
|
+
* loose bits, else at min(n,max).
|
|
24
|
+
* strict bits = floor(log2(avg)) + norm, loose = floor(log2(avg)) - norm - 1.
|
|
25
|
+
*/
|
|
26
|
+
#ifndef TILECDC_H
|
|
27
|
+
#define TILECDC_H
|
|
28
|
+
|
|
29
|
+
#include <stddef.h>
|
|
30
|
+
#include <stdint.h>
|
|
31
|
+
|
|
32
|
+
#ifdef __cplusplus
|
|
33
|
+
extern "C" {
|
|
34
|
+
#endif
|
|
35
|
+
|
|
36
|
+
#define TILECDC_WINDOW 48
|
|
37
|
+
/* Largest supported average chunk size. The window hash carries about 17 bits
|
|
38
|
+
of entropy, so masks of 17+ bits quantize: at a 64 KiB target the mean chunk
|
|
39
|
+
size varied from 55 to 74 KiB across seeds (32 KiB: 32 to 37 KiB). */
|
|
40
|
+
#define TILECDC_MAX_AVG 32768
|
|
41
|
+
#define TILECDC_VERSION "0.1.0"
|
|
42
|
+
|
|
43
|
+
typedef enum { TILECDC_IMPL_AUTO = 0, TILECDC_IMPL_SCALAR = 1, TILECDC_IMPL_AVX512 = 2, TILECDC_IMPL_AMX = 3 } tilecdc_impl;
|
|
44
|
+
|
|
45
|
+
typedef struct tilecdc tilecdc;
|
|
46
|
+
|
|
47
|
+
typedef struct tilecdc_params {
|
|
48
|
+
size_t min_size; /* default 2048; must be >= 64 (one 48-byte window plus slack) */
|
|
49
|
+
size_t avg_size; /* default 8192; at most TILECDC_MAX_AVG */
|
|
50
|
+
size_t max_size; /* default 65536 */
|
|
51
|
+
uint64_t seed; /* default 0; changes KEY and SBOX */
|
|
52
|
+
int norm; /* normalization level 0..4, default 1 */
|
|
53
|
+
tilecdc_impl impl; /* default AUTO: AMX if usable, else AVX-512, else scalar */
|
|
54
|
+
} tilecdc_params;
|
|
55
|
+
|
|
56
|
+
/* Fill p with defaults. */
|
|
57
|
+
void tilecdc_default_params(tilecdc_params *p);
|
|
58
|
+
|
|
59
|
+
/* Create a chunker; NULL params means defaults. Returns NULL with errno
|
|
60
|
+
EINVAL for invalid parameters, ENOTSUP when a forced implementation is
|
|
61
|
+
unavailable. Thread safety: a chunker holds no mutable state and may be
|
|
62
|
+
used from several threads at once. */
|
|
63
|
+
tilecdc *tilecdc_new(const tilecdc_params *params);
|
|
64
|
+
void tilecdc_free(tilecdc *t);
|
|
65
|
+
|
|
66
|
+
/* Length of the first chunk of data[0..len). In a stream, pass at least
|
|
67
|
+
max_size bytes unless the stream has ended (the last chunk may be short). */
|
|
68
|
+
size_t tilecdc_cut(tilecdc *t, const uint8_t *data, size_t len);
|
|
69
|
+
|
|
70
|
+
/* Chunk a whole buffer: writes chunk end offsets into ends (capacity cap)
|
|
71
|
+
and returns the number of chunks (which may exceed cap: only cap are written). */
|
|
72
|
+
size_t tilecdc_chunk_all(tilecdc *t, const uint8_t *data, size_t len, size_t *ends, size_t cap);
|
|
73
|
+
|
|
74
|
+
/* Which implementation this chunker uses, and what the CPU supports. */
|
|
75
|
+
tilecdc_impl tilecdc_get_impl(const tilecdc *t);
|
|
76
|
+
const char *tilecdc_impl_name(tilecdc_impl impl);
|
|
77
|
+
int tilecdc_impl_available(tilecdc_impl impl);
|
|
78
|
+
|
|
79
|
+
#ifdef __cplusplus
|
|
80
|
+
}
|
|
81
|
+
#endif
|
|
82
|
+
#endif
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tilecdc"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Fast content-defined chunking on CPU matrix units (Intel AMX), with AVX-512 and portable fallbacks that give identical boundaries"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "Apache-2.0" }
|
|
11
|
+
authors = [{ name = "Ashutosh Rath" }]
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
keywords = ["deduplication", "chunking", "cdc", "fastcdc", "amx", "avx512", "backup"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: C",
|
|
17
|
+
"License :: OSI Approved :: Apache Software License",
|
|
18
|
+
"Operating System :: POSIX :: Linux",
|
|
19
|
+
"Topic :: System :: Archiving :: Backup",
|
|
20
|
+
"Topic :: System :: Archiving :: Compression",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://github.com/ashutosh-rath02/tilecdc"
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
tilecdc = "tilecdc.__main__:main"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools]
|
|
30
|
+
packages = ["tilecdc"]
|
tilecdc-0.1.0/setup.cfg
ADDED
tilecdc-0.1.0/setup.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import platform
|
|
3
|
+
import shutil
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from setuptools import Extension, setup
|
|
7
|
+
|
|
8
|
+
if not sys.platform.startswith("linux") or platform.machine().lower() not in ("x86_64", "amd64"):
|
|
9
|
+
sys.exit("tilecdc 0.1 supports x86-64 Linux only (it uses AMX/AVX-512 intrinsics and Linux arch_prctl).")
|
|
10
|
+
|
|
11
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
12
|
+
ROOT = os.path.dirname(HERE)
|
|
13
|
+
|
|
14
|
+
# Vendor the C core so sdists build standalone.
|
|
15
|
+
VENDOR = os.path.join(HERE, "csrc")
|
|
16
|
+
if os.path.exists(os.path.join(ROOT, "src", "tilecdc.c")):
|
|
17
|
+
os.makedirs(VENDOR, exist_ok=True)
|
|
18
|
+
shutil.copy(os.path.join(ROOT, "src", "tilecdc.c"), VENDOR)
|
|
19
|
+
shutil.copy(os.path.join(ROOT, "include", "tilecdc.h"), VENDOR)
|
|
20
|
+
|
|
21
|
+
setup(
|
|
22
|
+
ext_modules=[
|
|
23
|
+
Extension(
|
|
24
|
+
"tilecdc._tilecdc",
|
|
25
|
+
sources=["_tilecdc.c", "csrc/tilecdc.c"],
|
|
26
|
+
include_dirs=["csrc"],
|
|
27
|
+
extra_compile_args=["-O3"],
|
|
28
|
+
)
|
|
29
|
+
],
|
|
30
|
+
)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Tests for the Python package: identical boundaries across implementations,
|
|
2
|
+
streaming equals in-memory chunking, and argument validation."""
|
|
3
|
+
import io
|
|
4
|
+
import os
|
|
5
|
+
import random
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
import tilecdc
|
|
10
|
+
|
|
11
|
+
DATA = random.Random(1).randbytes(6 << 20) + b"\x00" * 200_000 + b"abc" * 100_000
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_available():
|
|
15
|
+
impls = tilecdc.available_impls()
|
|
16
|
+
assert "scalar" in impls
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.mark.parametrize("impl", ["avx512", "amx"])
|
|
20
|
+
def test_impls_identical(impl):
|
|
21
|
+
if impl not in tilecdc.available_impls():
|
|
22
|
+
pytest.skip(f"{impl} not available")
|
|
23
|
+
ref = tilecdc.Chunker(impl="scalar").chunk(DATA[: 1 << 20])
|
|
24
|
+
assert tilecdc.Chunker(impl=impl).chunk(DATA[: 1 << 20]) == ref
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_cover_and_bounds():
|
|
28
|
+
c = tilecdc.Chunker()
|
|
29
|
+
lengths = c.chunk(DATA)
|
|
30
|
+
assert sum(lengths) == len(DATA)
|
|
31
|
+
assert all(l <= c.max_size for l in lengths)
|
|
32
|
+
assert all(l > c.min_size for l in lengths[:-1])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_stream_equals_memory():
|
|
36
|
+
c = tilecdc.Chunker()
|
|
37
|
+
mem = c.chunk(DATA)
|
|
38
|
+
for bs in (c.max_size, c.max_size + 1, 1 << 20, 3 << 20):
|
|
39
|
+
streamed = [l for _, l in c._iter_stream(io.BytesIO(DATA), False, bs)]
|
|
40
|
+
assert streamed == mem, bs
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_iter_file_with_data(tmp_path):
|
|
44
|
+
p = tmp_path / "f.bin"
|
|
45
|
+
p.write_bytes(DATA)
|
|
46
|
+
c = tilecdc.Chunker(avg_size=16384, min_size=4096, max_size=131072)
|
|
47
|
+
joined = b"".join(d for _, _, d in c.iter_file(str(p), with_data=True))
|
|
48
|
+
assert joined == DATA
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_seed_changes_boundaries():
|
|
52
|
+
a = tilecdc.Chunker(seed=1).chunk(DATA[: 1 << 20])
|
|
53
|
+
b = tilecdc.Chunker(seed=2).chunk(DATA[: 1 << 20])
|
|
54
|
+
assert a != b
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_small_inputs():
|
|
58
|
+
c = tilecdc.Chunker()
|
|
59
|
+
assert c.chunk(b"") == []
|
|
60
|
+
assert c.chunk(b"x") == [1]
|
|
61
|
+
assert c.chunk(b"y" * 2048) == [2048]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_bad_args():
|
|
65
|
+
with pytest.raises(ValueError):
|
|
66
|
+
tilecdc.Chunker(min_size=10)
|
|
67
|
+
with pytest.raises(ValueError):
|
|
68
|
+
tilecdc.Chunker(impl="gpu")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_uninitialised_and_failed_reinit_do_not_crash():
|
|
72
|
+
c = tilecdc.Chunker.__new__(tilecdc.Chunker)
|
|
73
|
+
with pytest.raises(RuntimeError):
|
|
74
|
+
c.chunk(b"x")
|
|
75
|
+
d = tilecdc.Chunker()
|
|
76
|
+
before = d.chunk(DATA[:100000])
|
|
77
|
+
with pytest.raises(ValueError):
|
|
78
|
+
d.__init__(min_size=10)
|
|
79
|
+
assert d.chunk(DATA[:100000]) == before # old chunker still intact
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_small_buffer_size_terminates():
|
|
83
|
+
c = tilecdc.Chunker()
|
|
84
|
+
out = [l for _, l in c.iter_file(io.BytesIO(DATA), buffer_size=1000)]
|
|
85
|
+
assert out == c.chunk(DATA)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_avg_cap_and_seed_validation():
|
|
89
|
+
with pytest.raises(ValueError):
|
|
90
|
+
tilecdc.Chunker(min_size=16384, avg_size=65536, max_size=524288)
|
|
91
|
+
with pytest.raises(ValueError):
|
|
92
|
+
tilecdc.Chunker(seed=-1)
|
|
93
|
+
assert tilecdc.Chunker(seed=2**64 - 1).chunk(b"a" * 5000)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""TileCDC: fast content-defined chunking on CPU matrix units.
|
|
2
|
+
|
|
3
|
+
import tilecdc
|
|
4
|
+
|
|
5
|
+
c = tilecdc.Chunker() # 2 KiB / 8 KiB / 64 KiB
|
|
6
|
+
lengths = c.chunk(data) # chunk an in-memory buffer
|
|
7
|
+
for off, length in c.iter_file("big.tar"):
|
|
8
|
+
...
|
|
9
|
+
|
|
10
|
+
Boundaries are identical on every CPU: AMX, AVX-512 and the portable scalar
|
|
11
|
+
path implement the same function, so chunks dedup across machines.
|
|
12
|
+
"""
|
|
13
|
+
import os
|
|
14
|
+
|
|
15
|
+
from ._tilecdc import MAX_AVG_SIZE, Chunker as _Chunker, available_impls, __version__
|
|
16
|
+
|
|
17
|
+
__all__ = ["Chunker", "available_impls", "chunk", "MAX_AVG_SIZE", "__version__"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Chunker(_Chunker):
|
|
21
|
+
"""Chunker(min_size=2048, avg_size=8192, max_size=65536, seed=0, norm=1, impl='auto')
|
|
22
|
+
|
|
23
|
+
impl is 'auto' (fastest available), 'amx', 'avx512' or 'scalar'.
|
|
24
|
+
The seed changes where boundaries fall; use the same seed everywhere
|
|
25
|
+
data should deduplicate. avg_size may be at most MAX_AVG_SIZE (32 KiB).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def iter_file(self, file, with_data=False, buffer_size=None):
|
|
29
|
+
"""Yield (offset, length) or (offset, length, bytes) for each chunk of
|
|
30
|
+
a path or a binary file object, reading it in large blocks."""
|
|
31
|
+
close = False
|
|
32
|
+
if isinstance(file, (str, bytes, os.PathLike)):
|
|
33
|
+
file = open(file, "rb")
|
|
34
|
+
close = True
|
|
35
|
+
try:
|
|
36
|
+
# the buffer must hold at least two maximum-size chunks to make progress
|
|
37
|
+
size = max(buffer_size or 4 << 20, 16 * self.max_size if buffer_size is None else 2 * self.max_size)
|
|
38
|
+
yield from self._iter_stream(file, with_data, size)
|
|
39
|
+
finally:
|
|
40
|
+
if close:
|
|
41
|
+
file.close()
|
|
42
|
+
|
|
43
|
+
def _iter_stream(self, f, with_data, bufsize):
|
|
44
|
+
buf = bytearray()
|
|
45
|
+
base = 0
|
|
46
|
+
eof = False
|
|
47
|
+
while True:
|
|
48
|
+
if not eof and len(buf) < bufsize:
|
|
49
|
+
block = f.read(bufsize - len(buf))
|
|
50
|
+
if not block:
|
|
51
|
+
eof = True
|
|
52
|
+
else:
|
|
53
|
+
buf += block
|
|
54
|
+
if not buf:
|
|
55
|
+
return
|
|
56
|
+
view = memoryview(buf)
|
|
57
|
+
used = 0
|
|
58
|
+
# One C call chunks the whole buffer. A chunk that starts at s is
|
|
59
|
+
# final once bytes up to s + max_size are visible (or at end of file),
|
|
60
|
+
# because its cut never depends on later bytes.
|
|
61
|
+
limit = len(buf) if eof else len(buf) - self.max_size
|
|
62
|
+
for n in self.chunk(view):
|
|
63
|
+
if used > limit or (not eof and used + n > len(buf)):
|
|
64
|
+
break
|
|
65
|
+
if not eof and used + self.max_size > len(buf):
|
|
66
|
+
break
|
|
67
|
+
if with_data:
|
|
68
|
+
yield base + used, n, bytes(view[used:used + n])
|
|
69
|
+
else:
|
|
70
|
+
yield base + used, n
|
|
71
|
+
used += n
|
|
72
|
+
view.release()
|
|
73
|
+
del buf[:used]
|
|
74
|
+
base += used
|
|
75
|
+
if eof and not buf:
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def chunk(data, **kwargs):
|
|
80
|
+
"""Chunk a bytes-like object; returns the list of chunk lengths."""
|
|
81
|
+
return Chunker(**kwargs).chunk(data)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""python -m tilecdc [-s] [--avg N] [--seed N] [--impl X] FILE...
|
|
2
|
+
|
|
3
|
+
Prints "file offset length" per chunk, or a per-file summary with -s.
|
|
4
|
+
"""
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
from . import Chunker, available_impls
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main(argv=None):
|
|
13
|
+
ap = argparse.ArgumentParser(prog="tilecdc", description="Content-defined chunking on CPU matrix units")
|
|
14
|
+
ap.add_argument("files", nargs="+")
|
|
15
|
+
ap.add_argument("-s", "--summary", action="store_true", help="print a summary per file instead of every chunk")
|
|
16
|
+
ap.add_argument("--avg", type=int, default=8192, help="average chunk size (min = avg/4, max = avg*8)")
|
|
17
|
+
ap.add_argument("--seed", type=int, default=0)
|
|
18
|
+
ap.add_argument("--impl", default="auto", choices=["auto", "amx", "avx512", "scalar"])
|
|
19
|
+
a = ap.parse_args(argv)
|
|
20
|
+
c = Chunker(min_size=a.avg // 4, avg_size=a.avg, max_size=a.avg * 8, seed=a.seed, impl=a.impl)
|
|
21
|
+
out = sys.stdout
|
|
22
|
+
for path in a.files:
|
|
23
|
+
t0 = time.perf_counter()
|
|
24
|
+
n = total = 0
|
|
25
|
+
for off, length in c.iter_file(path):
|
|
26
|
+
n += 1
|
|
27
|
+
total += length
|
|
28
|
+
if not a.summary:
|
|
29
|
+
out.write(f"{path} {off} {length}\n")
|
|
30
|
+
dt = time.perf_counter() - t0
|
|
31
|
+
if a.summary:
|
|
32
|
+
avg = total / n if n else 0
|
|
33
|
+
out.write(f"{path}: {total} bytes, {n} chunks, avg {avg:.0f} B, {total / dt / 1e9:.2f} GB/s incl. I/O ({c.impl}; available: {', '.join(available_impls())})\n")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
main()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tilecdc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Fast content-defined chunking on CPU matrix units (Intel AMX), with AVX-512 and portable fallbacks that give identical boundaries
|
|
5
|
+
Author: Ashutosh Rath
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/ashutosh-rath02/tilecdc
|
|
8
|
+
Keywords: deduplication,chunking,cdc,fastcdc,amx,avx512,backup
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: C
|
|
11
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Classifier: Topic :: System :: Archiving :: Backup
|
|
14
|
+
Classifier: Topic :: System :: Archiving :: Compression
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# tilecdc (Python)
|
|
19
|
+
|
|
20
|
+
Fast content-defined chunking on CPU matrix units. See the project README: https://github.com/ashutosh-rath02/tilecdc
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
README.md
|
|
3
|
+
_tilecdc.c
|
|
4
|
+
pyproject.toml
|
|
5
|
+
setup.py
|
|
6
|
+
csrc/tilecdc.c
|
|
7
|
+
csrc/tilecdc.h
|
|
8
|
+
tests/test_python.py
|
|
9
|
+
tilecdc/__init__.py
|
|
10
|
+
tilecdc/__main__.py
|
|
11
|
+
tilecdc.egg-info/PKG-INFO
|
|
12
|
+
tilecdc.egg-info/SOURCES.txt
|
|
13
|
+
tilecdc.egg-info/dependency_links.txt
|
|
14
|
+
tilecdc.egg-info/entry_points.txt
|
|
15
|
+
tilecdc.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tilecdc
|