rns-engine 0.4.1__tar.gz → 0.5.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.
- {rns_engine-0.4.1/src/rns_engine.egg-info → rns_engine-0.5.0}/PKG-INFO +99 -5
- {rns_engine-0.4.1 → rns_engine-0.5.0}/README.md +98 -4
- {rns_engine-0.4.1 → rns_engine-0.5.0}/pyproject.toml +1 -1
- {rns_engine-0.4.1 → rns_engine-0.5.0}/src/rns_engine/__init__.py +21 -2
- rns_engine-0.5.0/src/rns_engine/signed.py +211 -0
- {rns_engine-0.4.1 → rns_engine-0.5.0/src/rns_engine.egg-info}/PKG-INFO +99 -5
- {rns_engine-0.4.1 → rns_engine-0.5.0}/src/rns_engine.egg-info/SOURCES.txt +3 -1
- rns_engine-0.5.0/tests/test_signed.py +138 -0
- {rns_engine-0.4.1 → rns_engine-0.5.0}/LICENSE +0 -0
- {rns_engine-0.4.1 → rns_engine-0.5.0}/setup.cfg +0 -0
- {rns_engine-0.4.1 → rns_engine-0.5.0}/setup.py +0 -0
- {rns_engine-0.4.1 → rns_engine-0.5.0}/src/rns_engine/_core.cpp +0 -0
- {rns_engine-0.4.1 → rns_engine-0.5.0}/src/rns_engine/engine.py +0 -0
- {rns_engine-0.4.1 → rns_engine-0.5.0}/src/rns_engine.egg-info/dependency_links.txt +0 -0
- {rns_engine-0.4.1 → rns_engine-0.5.0}/src/rns_engine.egg-info/not-zip-safe +0 -0
- {rns_engine-0.4.1 → rns_engine-0.5.0}/src/rns_engine.egg-info/requires.txt +0 -0
- {rns_engine-0.4.1 → rns_engine-0.5.0}/src/rns_engine.egg-info/top_level.txt +0 -0
- {rns_engine-0.4.1 → rns_engine-0.5.0}/tests/test_packaging.py +0 -0
- {rns_engine-0.4.1 → rns_engine-0.5.0}/tests/test_rns.py +0 -0
- {rns_engine-0.4.1 → rns_engine-0.5.0}/tests/test_session.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rns_engine
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Exact RNS engine with session/cache API.
|
|
5
5
|
Author: Evan Wesley
|
|
6
6
|
License-Expression: AGPL-3.0-only
|
|
@@ -109,12 +109,86 @@ hot = s.hot_loop_affine(x, multiplier=1_000_003, addend=7, iterations=1000)
|
|
|
109
109
|
|
|
110
110
|
---
|
|
111
111
|
|
|
112
|
+
## Centered signed arithmetic and range certificates
|
|
113
|
+
|
|
114
|
+
The native core computes exact residues modulo `M`. Version 0.5 adds a canonical signed view over:
|
|
115
|
+
|
|
116
|
+
```text
|
|
117
|
+
[-M/2, M/2 - 1]
|
|
118
|
+
=
|
|
119
|
+
[-17,871,445,090,598,912, 17,871,445,090,598,911]
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
import numpy as np
|
|
124
|
+
import rns_engine as rns
|
|
125
|
+
|
|
126
|
+
values = np.array([-5, 0, 7], dtype=np.int64)
|
|
127
|
+
rails = rns.encode_signed(values)
|
|
128
|
+
round_trip = rns.decode_signed(*rails)
|
|
129
|
+
|
|
130
|
+
assert np.array_equal(round_trip, values)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`encode_signed(...)` rejects values outside the centered interval instead of silently wrapping them modulo `M`.
|
|
134
|
+
|
|
135
|
+
A centered decoding is a canonical interpretation of a residue. It is **not by itself proof that an unknown mathematical result did not wrap**. Unique signed reconstruction requires an independent strict magnitude bound:
|
|
136
|
+
|
|
137
|
+
```text
|
|
138
|
+
|result| < M/2
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Use a range certificate before claiming that a residue is the unique signed integer result:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
certificate = rns.certify_signed_bound(max_abs_bound=1_000_000)
|
|
145
|
+
certificate.require_unique()
|
|
146
|
+
|
|
147
|
+
print(certificate.headroom)
|
|
148
|
+
print(certificate.minimum_required_modulus)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
For a signed dot product or one GEMM output entry, the engine can build the worst-case receipt directly:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
certificate = rns.certify_signed_dot_bound(
|
|
155
|
+
inner_dimension=5120,
|
|
156
|
+
left_abs_bound=127,
|
|
157
|
+
right_abs_bound=127,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
assert certificate.max_abs_bound == 82_580_480
|
|
161
|
+
assert certificate.unique
|
|
162
|
+
certificate.require_unique()
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
This uses the exact conservative law:
|
|
166
|
+
|
|
167
|
+
```text
|
|
168
|
+
output bound
|
|
169
|
+
=
|
|
170
|
+
inner_dimension × left bound × right bound + addend bound
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`SignedSession` exposes the existing residue arithmetic with signed external values:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
s = rns.SignedSession()
|
|
177
|
+
a = s.encode_signed([-5, 7])
|
|
178
|
+
b = s.encode_signed([3, -10])
|
|
179
|
+
out = s.decode_signed(s.add(a, b))
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
112
184
|
## Core API
|
|
113
185
|
|
|
114
186
|
### Encoded rail API
|
|
115
187
|
|
|
116
188
|
* `rns.encode(x)` → `(r0, r1, r2, r3)`
|
|
117
189
|
* `rns.decode(r0, r1, r2, r3)` → `uint64[]`
|
|
190
|
+
* `rns.encode_signed(x)` → strict centered signed encoding
|
|
191
|
+
* `rns.decode_signed(r0, r1, r2, r3)` → canonical `int64[]`
|
|
118
192
|
* `rns.add(*ea, *eb)`
|
|
119
193
|
* `rns.sub(*ea, *eb)`
|
|
120
194
|
* `rns.mul(*ea, *eb)`
|
|
@@ -122,6 +196,15 @@ hot = s.hot_loop_affine(x, multiplier=1_000_003, addend=7, iterations=1000)
|
|
|
122
196
|
* `rns.fma(*ea, *eb, *ec)`
|
|
123
197
|
* `rns.op(*ea, *eb, code)` where `0=add 1=mul 2=sub 3=div`
|
|
124
198
|
|
|
199
|
+
### Range-certificate API
|
|
200
|
+
|
|
201
|
+
* `rns.certify_signed_bound(max_abs_bound)`
|
|
202
|
+
* `rns.certify_signed_dot_bound(inner_dimension, left_abs_bound, right_abs_bound, addend_abs_bound=0)`
|
|
203
|
+
* `SignedRangeCertificate.unique`
|
|
204
|
+
* `SignedRangeCertificate.headroom`
|
|
205
|
+
* `SignedRangeCertificate.minimum_required_modulus`
|
|
206
|
+
* `SignedRangeCertificate.require_unique()`
|
|
207
|
+
|
|
125
208
|
### Scalar-broadcast encoded API
|
|
126
209
|
|
|
127
210
|
These avoid materializing full constant arrays in Python:
|
|
@@ -159,6 +242,7 @@ Auto-dispatch variants:
|
|
|
159
242
|
### High-level API
|
|
160
243
|
|
|
161
244
|
* `rns.Session`
|
|
245
|
+
* `rns.SignedSession`
|
|
162
246
|
* `rns.SessionCache`
|
|
163
247
|
* `rns.EncodedArray`
|
|
164
248
|
|
|
@@ -208,7 +292,9 @@ def make_invertible_divisor(b):
|
|
|
208
292
|
## Data model
|
|
209
293
|
|
|
210
294
|
* input arrays to `encode(...)` are treated as `uint64`
|
|
211
|
-
* values outside `[0, M)` are reduced mod `M` during encode
|
|
295
|
+
* values outside `[0, M)` are reduced mod `M` during unsigned encode
|
|
296
|
+
* `encode_signed(...)` accepts only integers in `[-M/2, M/2 - 1]` and refuses silent wrapping
|
|
297
|
+
* `decode_signed(...)` returns the canonical centered `int64` view
|
|
212
298
|
* rails are returned as:
|
|
213
299
|
* `r0`: `uint16`
|
|
214
300
|
* `r1`: `uint16`
|
|
@@ -251,7 +337,8 @@ Workload: `affine_repeat_u64_io(x, 1_000_003, 7, iterations=1000)`
|
|
|
251
337
|
### Verification status
|
|
252
338
|
|
|
253
339
|
* correctness sanity checks passed
|
|
254
|
-
* full
|
|
340
|
+
* the full suite is exercised on Linux, macOS, and Windows across Python 3.10–3.13
|
|
341
|
+
* built wheels are installed and tested before release
|
|
255
342
|
|
|
256
343
|
---
|
|
257
344
|
|
|
@@ -310,6 +397,9 @@ import rns_engine as rns
|
|
|
310
397
|
rns.info()
|
|
311
398
|
|
|
312
399
|
rns.M
|
|
400
|
+
rns.HALF_M
|
|
401
|
+
rns.SIGNED_MIN
|
|
402
|
+
rns.SIGNED_MAX
|
|
313
403
|
rns.M0
|
|
314
404
|
rns.M1
|
|
315
405
|
rns.M2
|
|
@@ -321,9 +411,13 @@ rns.HAS_AVX2
|
|
|
321
411
|
|
|
322
412
|
## Current release
|
|
323
413
|
|
|
324
|
-
### v0.
|
|
414
|
+
### v0.5.0
|
|
325
415
|
|
|
326
|
-
*
|
|
416
|
+
* canonical centered signed encoding and decoding
|
|
417
|
+
* strict signed input validation with no silent modular wrapping
|
|
418
|
+
* signed uniqueness certificates using the law `|result| < M/2`
|
|
419
|
+
* exact dot-product and GEMM output-bound receipts
|
|
420
|
+
* `SignedSession` high-level API
|
|
327
421
|
* 4-rail engine (`127 × 8191 × 65536 × 524287`)
|
|
328
422
|
* AVX2-accelerated encoded kernels
|
|
329
423
|
* fused `fma(...)`
|
|
@@ -86,12 +86,86 @@ hot = s.hot_loop_affine(x, multiplier=1_000_003, addend=7, iterations=1000)
|
|
|
86
86
|
|
|
87
87
|
---
|
|
88
88
|
|
|
89
|
+
## Centered signed arithmetic and range certificates
|
|
90
|
+
|
|
91
|
+
The native core computes exact residues modulo `M`. Version 0.5 adds a canonical signed view over:
|
|
92
|
+
|
|
93
|
+
```text
|
|
94
|
+
[-M/2, M/2 - 1]
|
|
95
|
+
=
|
|
96
|
+
[-17,871,445,090,598,912, 17,871,445,090,598,911]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
import numpy as np
|
|
101
|
+
import rns_engine as rns
|
|
102
|
+
|
|
103
|
+
values = np.array([-5, 0, 7], dtype=np.int64)
|
|
104
|
+
rails = rns.encode_signed(values)
|
|
105
|
+
round_trip = rns.decode_signed(*rails)
|
|
106
|
+
|
|
107
|
+
assert np.array_equal(round_trip, values)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`encode_signed(...)` rejects values outside the centered interval instead of silently wrapping them modulo `M`.
|
|
111
|
+
|
|
112
|
+
A centered decoding is a canonical interpretation of a residue. It is **not by itself proof that an unknown mathematical result did not wrap**. Unique signed reconstruction requires an independent strict magnitude bound:
|
|
113
|
+
|
|
114
|
+
```text
|
|
115
|
+
|result| < M/2
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Use a range certificate before claiming that a residue is the unique signed integer result:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
certificate = rns.certify_signed_bound(max_abs_bound=1_000_000)
|
|
122
|
+
certificate.require_unique()
|
|
123
|
+
|
|
124
|
+
print(certificate.headroom)
|
|
125
|
+
print(certificate.minimum_required_modulus)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
For a signed dot product or one GEMM output entry, the engine can build the worst-case receipt directly:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
certificate = rns.certify_signed_dot_bound(
|
|
132
|
+
inner_dimension=5120,
|
|
133
|
+
left_abs_bound=127,
|
|
134
|
+
right_abs_bound=127,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
assert certificate.max_abs_bound == 82_580_480
|
|
138
|
+
assert certificate.unique
|
|
139
|
+
certificate.require_unique()
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
This uses the exact conservative law:
|
|
143
|
+
|
|
144
|
+
```text
|
|
145
|
+
output bound
|
|
146
|
+
=
|
|
147
|
+
inner_dimension × left bound × right bound + addend bound
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`SignedSession` exposes the existing residue arithmetic with signed external values:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
s = rns.SignedSession()
|
|
154
|
+
a = s.encode_signed([-5, 7])
|
|
155
|
+
b = s.encode_signed([3, -10])
|
|
156
|
+
out = s.decode_signed(s.add(a, b))
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
89
161
|
## Core API
|
|
90
162
|
|
|
91
163
|
### Encoded rail API
|
|
92
164
|
|
|
93
165
|
* `rns.encode(x)` → `(r0, r1, r2, r3)`
|
|
94
166
|
* `rns.decode(r0, r1, r2, r3)` → `uint64[]`
|
|
167
|
+
* `rns.encode_signed(x)` → strict centered signed encoding
|
|
168
|
+
* `rns.decode_signed(r0, r1, r2, r3)` → canonical `int64[]`
|
|
95
169
|
* `rns.add(*ea, *eb)`
|
|
96
170
|
* `rns.sub(*ea, *eb)`
|
|
97
171
|
* `rns.mul(*ea, *eb)`
|
|
@@ -99,6 +173,15 @@ hot = s.hot_loop_affine(x, multiplier=1_000_003, addend=7, iterations=1000)
|
|
|
99
173
|
* `rns.fma(*ea, *eb, *ec)`
|
|
100
174
|
* `rns.op(*ea, *eb, code)` where `0=add 1=mul 2=sub 3=div`
|
|
101
175
|
|
|
176
|
+
### Range-certificate API
|
|
177
|
+
|
|
178
|
+
* `rns.certify_signed_bound(max_abs_bound)`
|
|
179
|
+
* `rns.certify_signed_dot_bound(inner_dimension, left_abs_bound, right_abs_bound, addend_abs_bound=0)`
|
|
180
|
+
* `SignedRangeCertificate.unique`
|
|
181
|
+
* `SignedRangeCertificate.headroom`
|
|
182
|
+
* `SignedRangeCertificate.minimum_required_modulus`
|
|
183
|
+
* `SignedRangeCertificate.require_unique()`
|
|
184
|
+
|
|
102
185
|
### Scalar-broadcast encoded API
|
|
103
186
|
|
|
104
187
|
These avoid materializing full constant arrays in Python:
|
|
@@ -136,6 +219,7 @@ Auto-dispatch variants:
|
|
|
136
219
|
### High-level API
|
|
137
220
|
|
|
138
221
|
* `rns.Session`
|
|
222
|
+
* `rns.SignedSession`
|
|
139
223
|
* `rns.SessionCache`
|
|
140
224
|
* `rns.EncodedArray`
|
|
141
225
|
|
|
@@ -185,7 +269,9 @@ def make_invertible_divisor(b):
|
|
|
185
269
|
## Data model
|
|
186
270
|
|
|
187
271
|
* input arrays to `encode(...)` are treated as `uint64`
|
|
188
|
-
* values outside `[0, M)` are reduced mod `M` during encode
|
|
272
|
+
* values outside `[0, M)` are reduced mod `M` during unsigned encode
|
|
273
|
+
* `encode_signed(...)` accepts only integers in `[-M/2, M/2 - 1]` and refuses silent wrapping
|
|
274
|
+
* `decode_signed(...)` returns the canonical centered `int64` view
|
|
189
275
|
* rails are returned as:
|
|
190
276
|
* `r0`: `uint16`
|
|
191
277
|
* `r1`: `uint16`
|
|
@@ -228,7 +314,8 @@ Workload: `affine_repeat_u64_io(x, 1_000_003, 7, iterations=1000)`
|
|
|
228
314
|
### Verification status
|
|
229
315
|
|
|
230
316
|
* correctness sanity checks passed
|
|
231
|
-
* full
|
|
317
|
+
* the full suite is exercised on Linux, macOS, and Windows across Python 3.10–3.13
|
|
318
|
+
* built wheels are installed and tested before release
|
|
232
319
|
|
|
233
320
|
---
|
|
234
321
|
|
|
@@ -287,6 +374,9 @@ import rns_engine as rns
|
|
|
287
374
|
rns.info()
|
|
288
375
|
|
|
289
376
|
rns.M
|
|
377
|
+
rns.HALF_M
|
|
378
|
+
rns.SIGNED_MIN
|
|
379
|
+
rns.SIGNED_MAX
|
|
290
380
|
rns.M0
|
|
291
381
|
rns.M1
|
|
292
382
|
rns.M2
|
|
@@ -298,9 +388,13 @@ rns.HAS_AVX2
|
|
|
298
388
|
|
|
299
389
|
## Current release
|
|
300
390
|
|
|
301
|
-
### v0.
|
|
391
|
+
### v0.5.0
|
|
302
392
|
|
|
303
|
-
*
|
|
393
|
+
* canonical centered signed encoding and decoding
|
|
394
|
+
* strict signed input validation with no silent modular wrapping
|
|
395
|
+
* signed uniqueness certificates using the law `|result| < M/2`
|
|
396
|
+
* exact dot-product and GEMM output-bound receipts
|
|
397
|
+
* `SignedSession` high-level API
|
|
304
398
|
* 4-rail engine (`127 × 8191 × 65536 × 524287`)
|
|
305
399
|
* AVX2-accelerated encoded kernels
|
|
306
400
|
* fused `fma(...)`
|
|
@@ -39,6 +39,19 @@ from ._core import (
|
|
|
39
39
|
omp_num_procs,
|
|
40
40
|
)
|
|
41
41
|
from .engine import EncodedArray, Session, SessionCache
|
|
42
|
+
from .signed import (
|
|
43
|
+
HALF_M,
|
|
44
|
+
SIGNED_MAX,
|
|
45
|
+
SIGNED_MIN,
|
|
46
|
+
UNIQUE_SIGNED_MAX,
|
|
47
|
+
UNIQUE_SIGNED_MIN,
|
|
48
|
+
SignedRangeCertificate,
|
|
49
|
+
SignedSession,
|
|
50
|
+
certify_signed_bound,
|
|
51
|
+
certify_signed_dot_bound,
|
|
52
|
+
decode_signed,
|
|
53
|
+
encode_signed,
|
|
54
|
+
)
|
|
42
55
|
|
|
43
56
|
try:
|
|
44
57
|
__version__ = _distribution_version("rns_engine")
|
|
@@ -50,7 +63,9 @@ except PackageNotFoundError:
|
|
|
50
63
|
__all__ = [
|
|
51
64
|
"HAS_AVX2",
|
|
52
65
|
"M", "M0", "M1", "M2", "M3",
|
|
53
|
-
"
|
|
66
|
+
"HALF_M", "SIGNED_MIN", "SIGNED_MAX",
|
|
67
|
+
"UNIQUE_SIGNED_MIN", "UNIQUE_SIGNED_MAX",
|
|
68
|
+
"encode", "decode", "encode_signed", "decode_signed", "op",
|
|
54
69
|
"add", "sub", "mul", "div_", "fma",
|
|
55
70
|
"affine_repeat",
|
|
56
71
|
"mul_u64", "fma_u64", "affine_repeat_u64",
|
|
@@ -60,13 +75,17 @@ __all__ = [
|
|
|
60
75
|
"fma_u64_io", "fma_u64_io_omp", "fma_u64_auto",
|
|
61
76
|
"affine_repeat_u64_io", "affine_repeat_u64_io_omp", "affine_repeat_u64_auto",
|
|
62
77
|
"omp_max_threads", "omp_set_num_threads", "omp_num_procs",
|
|
63
|
-
"EncodedArray", "SessionCache", "Session",
|
|
78
|
+
"EncodedArray", "SessionCache", "Session", "SignedSession",
|
|
79
|
+
"SignedRangeCertificate", "certify_signed_bound", "certify_signed_dot_bound",
|
|
64
80
|
"info",
|
|
65
81
|
]
|
|
66
82
|
|
|
83
|
+
|
|
67
84
|
def info():
|
|
68
85
|
print(f"rns_engine v{__version__}")
|
|
69
86
|
print(f" Dynamic range : [0, {M:,})")
|
|
87
|
+
print(f" Signed view : [{SIGNED_MIN:,}, {SIGNED_MAX:,}]")
|
|
88
|
+
print(f" Unique bound : |x| < {HALF_M:,}")
|
|
70
89
|
print(f" Moduli : {M0} x {M1} x {M2} x {M3}")
|
|
71
90
|
print(f" AVX2 : {'yes' if HAS_AVX2 else 'no'}")
|
|
72
91
|
print(" Core APIs : add/sub/mul/fma + raw/omp/auto scalar-broadcast family")
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from operator import index
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from ._core import M
|
|
10
|
+
from ._core import decode as _decode
|
|
11
|
+
from ._core import encode as _encode
|
|
12
|
+
from .engine import EncodedArray, Session
|
|
13
|
+
|
|
14
|
+
HALF_M = int(M) // 2
|
|
15
|
+
SIGNED_MIN = -HALF_M
|
|
16
|
+
SIGNED_MAX = HALF_M - 1
|
|
17
|
+
UNIQUE_SIGNED_MIN = -HALF_M + 1
|
|
18
|
+
UNIQUE_SIGNED_MAX = HALF_M - 1
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _require_nonnegative_integer(value: Any, *, name: str) -> int:
|
|
22
|
+
try:
|
|
23
|
+
integer = index(value)
|
|
24
|
+
except TypeError as exc:
|
|
25
|
+
raise TypeError(f"{name} must be an integer") from exc
|
|
26
|
+
if integer < 0:
|
|
27
|
+
raise ValueError(f"{name} must be >= 0")
|
|
28
|
+
return integer
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _as_signed_int64_array(x: Any) -> np.ndarray:
|
|
32
|
+
raw = np.asarray(x)
|
|
33
|
+
if raw.ndim != 1:
|
|
34
|
+
raise ValueError("expected a 1D array-like of signed integer values")
|
|
35
|
+
|
|
36
|
+
if np.issubdtype(raw.dtype, np.integer):
|
|
37
|
+
if np.issubdtype(raw.dtype, np.unsignedinteger):
|
|
38
|
+
if raw.size and np.any(raw > np.uint64(SIGNED_MAX)):
|
|
39
|
+
raise OverflowError(
|
|
40
|
+
f"signed RNS input must be in [{SIGNED_MIN}, {SIGNED_MAX}]"
|
|
41
|
+
)
|
|
42
|
+
else:
|
|
43
|
+
if raw.size and (
|
|
44
|
+
np.any(raw < np.int64(SIGNED_MIN))
|
|
45
|
+
or np.any(raw > np.int64(SIGNED_MAX))
|
|
46
|
+
):
|
|
47
|
+
raise OverflowError(
|
|
48
|
+
f"signed RNS input must be in [{SIGNED_MIN}, {SIGNED_MAX}]"
|
|
49
|
+
)
|
|
50
|
+
return np.ascontiguousarray(raw, dtype=np.int64)
|
|
51
|
+
|
|
52
|
+
if raw.dtype == object:
|
|
53
|
+
values: list[int] = []
|
|
54
|
+
for position, value in enumerate(raw):
|
|
55
|
+
try:
|
|
56
|
+
integer = index(value)
|
|
57
|
+
except TypeError as exc:
|
|
58
|
+
raise TypeError(
|
|
59
|
+
f"signed RNS input at index {position} is not an integer"
|
|
60
|
+
) from exc
|
|
61
|
+
if integer < SIGNED_MIN or integer > SIGNED_MAX:
|
|
62
|
+
raise OverflowError(
|
|
63
|
+
f"signed RNS input at index {position} must be in "
|
|
64
|
+
f"[{SIGNED_MIN}, {SIGNED_MAX}]"
|
|
65
|
+
)
|
|
66
|
+
values.append(integer)
|
|
67
|
+
return np.ascontiguousarray(values, dtype=np.int64)
|
|
68
|
+
|
|
69
|
+
raise TypeError("signed RNS input must contain integers")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def encode_signed(x: Any) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
|
73
|
+
"""Encode canonical centered integers without silent modular wrapping.
|
|
74
|
+
|
|
75
|
+
Accepted values are in ``[-M/2, M/2 - 1]``. Negative values are mapped to
|
|
76
|
+
their canonical residues before entering the native four-rail encoder.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
signed = _as_signed_int64_array(x)
|
|
80
|
+
residues = signed.copy()
|
|
81
|
+
negative = residues < 0
|
|
82
|
+
residues[negative] += int(M)
|
|
83
|
+
return _encode(np.ascontiguousarray(residues, dtype=np.uint64))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def decode_signed(
|
|
87
|
+
r0: Any,
|
|
88
|
+
r1: Any,
|
|
89
|
+
r2: Any,
|
|
90
|
+
r3: Any,
|
|
91
|
+
) -> np.ndarray:
|
|
92
|
+
"""Decode residues into the canonical centered interval.
|
|
93
|
+
|
|
94
|
+
The residue ``M/2`` is represented as ``-M/2``. This is a canonical
|
|
95
|
+
interpretation, not proof that an unknown mathematical result did not wrap;
|
|
96
|
+
use :func:`certify_signed_bound` for that guarantee.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
residues = _decode(r0, r1, r2, r3)
|
|
100
|
+
centered = np.ascontiguousarray(residues, dtype=np.int64)
|
|
101
|
+
upper_half = residues >= np.uint64(HALF_M)
|
|
102
|
+
centered[upper_half] -= int(M)
|
|
103
|
+
return centered
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass(frozen=True, slots=True)
|
|
107
|
+
class SignedRangeCertificate:
|
|
108
|
+
"""Certificate that a known absolute bound has one signed RNS decoding."""
|
|
109
|
+
|
|
110
|
+
max_abs_bound: int
|
|
111
|
+
|
|
112
|
+
def __post_init__(self) -> None:
|
|
113
|
+
bound = _require_nonnegative_integer(
|
|
114
|
+
self.max_abs_bound,
|
|
115
|
+
name="max_abs_bound",
|
|
116
|
+
)
|
|
117
|
+
object.__setattr__(self, "max_abs_bound", bound)
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def modulus(self) -> int:
|
|
121
|
+
return int(M)
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def half_modulus(self) -> int:
|
|
125
|
+
return HALF_M
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def unique(self) -> bool:
|
|
129
|
+
return self.max_abs_bound < HALF_M
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def headroom(self) -> int:
|
|
133
|
+
"""Remaining strict signed headroom; negative means the bound exceeds it."""
|
|
134
|
+
|
|
135
|
+
return UNIQUE_SIGNED_MAX - self.max_abs_bound
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def minimum_required_modulus(self) -> int:
|
|
139
|
+
"""Smallest modulus that uniquely supports ``[-bound, +bound]``."""
|
|
140
|
+
|
|
141
|
+
return 2 * self.max_abs_bound + 1
|
|
142
|
+
|
|
143
|
+
def require_unique(self) -> SignedRangeCertificate:
|
|
144
|
+
if not self.unique:
|
|
145
|
+
raise OverflowError(
|
|
146
|
+
"signed result is modular-only: the certified absolute bound "
|
|
147
|
+
f"{self.max_abs_bound} must be less than M/2 ({HALF_M})"
|
|
148
|
+
)
|
|
149
|
+
return self
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def certify_signed_bound(max_abs_bound: int) -> SignedRangeCertificate:
|
|
153
|
+
"""Return a strict uniqueness certificate for a known absolute bound."""
|
|
154
|
+
|
|
155
|
+
return SignedRangeCertificate(max_abs_bound=max_abs_bound)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def certify_signed_dot_bound(
|
|
159
|
+
inner_dimension: int,
|
|
160
|
+
left_abs_bound: int,
|
|
161
|
+
right_abs_bound: int,
|
|
162
|
+
addend_abs_bound: int = 0,
|
|
163
|
+
) -> SignedRangeCertificate:
|
|
164
|
+
"""Certify the worst-case magnitude of an exact signed dot product.
|
|
165
|
+
|
|
166
|
+
For ``sum(a[k] * b[k]) + c`` with ``inner_dimension`` terms, this uses the
|
|
167
|
+
conservative exact bound::
|
|
168
|
+
|
|
169
|
+
inner_dimension * left_abs_bound * right_abs_bound + addend_abs_bound
|
|
170
|
+
|
|
171
|
+
This is also the per-output bound for a GEMM whose entries satisfy the same
|
|
172
|
+
magnitude limits. The returned certificate is unique only when that bound
|
|
173
|
+
is strictly less than ``M/2``.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
k = _require_nonnegative_integer(inner_dimension, name="inner_dimension")
|
|
177
|
+
left = _require_nonnegative_integer(left_abs_bound, name="left_abs_bound")
|
|
178
|
+
right = _require_nonnegative_integer(
|
|
179
|
+
right_abs_bound,
|
|
180
|
+
name="right_abs_bound",
|
|
181
|
+
)
|
|
182
|
+
addend = _require_nonnegative_integer(
|
|
183
|
+
addend_abs_bound,
|
|
184
|
+
name="addend_abs_bound",
|
|
185
|
+
)
|
|
186
|
+
return certify_signed_bound(k * left * right + addend)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class SignedSession(Session):
|
|
190
|
+
"""Session API whose external values use centered signed RNS semantics."""
|
|
191
|
+
|
|
192
|
+
def encode_signed(self, x: Any) -> EncodedArray:
|
|
193
|
+
return EncodedArray(*encode_signed(x))
|
|
194
|
+
|
|
195
|
+
def decode_signed(self, x: EncodedArray) -> np.ndarray:
|
|
196
|
+
return decode_signed(*x.rails())
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
__all__ = [
|
|
200
|
+
"HALF_M",
|
|
201
|
+
"SIGNED_MIN",
|
|
202
|
+
"SIGNED_MAX",
|
|
203
|
+
"UNIQUE_SIGNED_MIN",
|
|
204
|
+
"UNIQUE_SIGNED_MAX",
|
|
205
|
+
"SignedRangeCertificate",
|
|
206
|
+
"SignedSession",
|
|
207
|
+
"certify_signed_bound",
|
|
208
|
+
"certify_signed_dot_bound",
|
|
209
|
+
"encode_signed",
|
|
210
|
+
"decode_signed",
|
|
211
|
+
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rns_engine
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Exact RNS engine with session/cache API.
|
|
5
5
|
Author: Evan Wesley
|
|
6
6
|
License-Expression: AGPL-3.0-only
|
|
@@ -109,12 +109,86 @@ hot = s.hot_loop_affine(x, multiplier=1_000_003, addend=7, iterations=1000)
|
|
|
109
109
|
|
|
110
110
|
---
|
|
111
111
|
|
|
112
|
+
## Centered signed arithmetic and range certificates
|
|
113
|
+
|
|
114
|
+
The native core computes exact residues modulo `M`. Version 0.5 adds a canonical signed view over:
|
|
115
|
+
|
|
116
|
+
```text
|
|
117
|
+
[-M/2, M/2 - 1]
|
|
118
|
+
=
|
|
119
|
+
[-17,871,445,090,598,912, 17,871,445,090,598,911]
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
import numpy as np
|
|
124
|
+
import rns_engine as rns
|
|
125
|
+
|
|
126
|
+
values = np.array([-5, 0, 7], dtype=np.int64)
|
|
127
|
+
rails = rns.encode_signed(values)
|
|
128
|
+
round_trip = rns.decode_signed(*rails)
|
|
129
|
+
|
|
130
|
+
assert np.array_equal(round_trip, values)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`encode_signed(...)` rejects values outside the centered interval instead of silently wrapping them modulo `M`.
|
|
134
|
+
|
|
135
|
+
A centered decoding is a canonical interpretation of a residue. It is **not by itself proof that an unknown mathematical result did not wrap**. Unique signed reconstruction requires an independent strict magnitude bound:
|
|
136
|
+
|
|
137
|
+
```text
|
|
138
|
+
|result| < M/2
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Use a range certificate before claiming that a residue is the unique signed integer result:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
certificate = rns.certify_signed_bound(max_abs_bound=1_000_000)
|
|
145
|
+
certificate.require_unique()
|
|
146
|
+
|
|
147
|
+
print(certificate.headroom)
|
|
148
|
+
print(certificate.minimum_required_modulus)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
For a signed dot product or one GEMM output entry, the engine can build the worst-case receipt directly:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
certificate = rns.certify_signed_dot_bound(
|
|
155
|
+
inner_dimension=5120,
|
|
156
|
+
left_abs_bound=127,
|
|
157
|
+
right_abs_bound=127,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
assert certificate.max_abs_bound == 82_580_480
|
|
161
|
+
assert certificate.unique
|
|
162
|
+
certificate.require_unique()
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
This uses the exact conservative law:
|
|
166
|
+
|
|
167
|
+
```text
|
|
168
|
+
output bound
|
|
169
|
+
=
|
|
170
|
+
inner_dimension × left bound × right bound + addend bound
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`SignedSession` exposes the existing residue arithmetic with signed external values:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
s = rns.SignedSession()
|
|
177
|
+
a = s.encode_signed([-5, 7])
|
|
178
|
+
b = s.encode_signed([3, -10])
|
|
179
|
+
out = s.decode_signed(s.add(a, b))
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
112
184
|
## Core API
|
|
113
185
|
|
|
114
186
|
### Encoded rail API
|
|
115
187
|
|
|
116
188
|
* `rns.encode(x)` → `(r0, r1, r2, r3)`
|
|
117
189
|
* `rns.decode(r0, r1, r2, r3)` → `uint64[]`
|
|
190
|
+
* `rns.encode_signed(x)` → strict centered signed encoding
|
|
191
|
+
* `rns.decode_signed(r0, r1, r2, r3)` → canonical `int64[]`
|
|
118
192
|
* `rns.add(*ea, *eb)`
|
|
119
193
|
* `rns.sub(*ea, *eb)`
|
|
120
194
|
* `rns.mul(*ea, *eb)`
|
|
@@ -122,6 +196,15 @@ hot = s.hot_loop_affine(x, multiplier=1_000_003, addend=7, iterations=1000)
|
|
|
122
196
|
* `rns.fma(*ea, *eb, *ec)`
|
|
123
197
|
* `rns.op(*ea, *eb, code)` where `0=add 1=mul 2=sub 3=div`
|
|
124
198
|
|
|
199
|
+
### Range-certificate API
|
|
200
|
+
|
|
201
|
+
* `rns.certify_signed_bound(max_abs_bound)`
|
|
202
|
+
* `rns.certify_signed_dot_bound(inner_dimension, left_abs_bound, right_abs_bound, addend_abs_bound=0)`
|
|
203
|
+
* `SignedRangeCertificate.unique`
|
|
204
|
+
* `SignedRangeCertificate.headroom`
|
|
205
|
+
* `SignedRangeCertificate.minimum_required_modulus`
|
|
206
|
+
* `SignedRangeCertificate.require_unique()`
|
|
207
|
+
|
|
125
208
|
### Scalar-broadcast encoded API
|
|
126
209
|
|
|
127
210
|
These avoid materializing full constant arrays in Python:
|
|
@@ -159,6 +242,7 @@ Auto-dispatch variants:
|
|
|
159
242
|
### High-level API
|
|
160
243
|
|
|
161
244
|
* `rns.Session`
|
|
245
|
+
* `rns.SignedSession`
|
|
162
246
|
* `rns.SessionCache`
|
|
163
247
|
* `rns.EncodedArray`
|
|
164
248
|
|
|
@@ -208,7 +292,9 @@ def make_invertible_divisor(b):
|
|
|
208
292
|
## Data model
|
|
209
293
|
|
|
210
294
|
* input arrays to `encode(...)` are treated as `uint64`
|
|
211
|
-
* values outside `[0, M)` are reduced mod `M` during encode
|
|
295
|
+
* values outside `[0, M)` are reduced mod `M` during unsigned encode
|
|
296
|
+
* `encode_signed(...)` accepts only integers in `[-M/2, M/2 - 1]` and refuses silent wrapping
|
|
297
|
+
* `decode_signed(...)` returns the canonical centered `int64` view
|
|
212
298
|
* rails are returned as:
|
|
213
299
|
* `r0`: `uint16`
|
|
214
300
|
* `r1`: `uint16`
|
|
@@ -251,7 +337,8 @@ Workload: `affine_repeat_u64_io(x, 1_000_003, 7, iterations=1000)`
|
|
|
251
337
|
### Verification status
|
|
252
338
|
|
|
253
339
|
* correctness sanity checks passed
|
|
254
|
-
* full
|
|
340
|
+
* the full suite is exercised on Linux, macOS, and Windows across Python 3.10–3.13
|
|
341
|
+
* built wheels are installed and tested before release
|
|
255
342
|
|
|
256
343
|
---
|
|
257
344
|
|
|
@@ -310,6 +397,9 @@ import rns_engine as rns
|
|
|
310
397
|
rns.info()
|
|
311
398
|
|
|
312
399
|
rns.M
|
|
400
|
+
rns.HALF_M
|
|
401
|
+
rns.SIGNED_MIN
|
|
402
|
+
rns.SIGNED_MAX
|
|
313
403
|
rns.M0
|
|
314
404
|
rns.M1
|
|
315
405
|
rns.M2
|
|
@@ -321,9 +411,13 @@ rns.HAS_AVX2
|
|
|
321
411
|
|
|
322
412
|
## Current release
|
|
323
413
|
|
|
324
|
-
### v0.
|
|
414
|
+
### v0.5.0
|
|
325
415
|
|
|
326
|
-
*
|
|
416
|
+
* canonical centered signed encoding and decoding
|
|
417
|
+
* strict signed input validation with no silent modular wrapping
|
|
418
|
+
* signed uniqueness certificates using the law `|result| < M/2`
|
|
419
|
+
* exact dot-product and GEMM output-bound receipts
|
|
420
|
+
* `SignedSession` high-level API
|
|
327
421
|
* 4-rail engine (`127 × 8191 × 65536 × 524287`)
|
|
328
422
|
* AVX2-accelerated encoded kernels
|
|
329
423
|
* fused `fma(...)`
|
|
@@ -5,6 +5,7 @@ setup.py
|
|
|
5
5
|
src/rns_engine/__init__.py
|
|
6
6
|
src/rns_engine/_core.cpp
|
|
7
7
|
src/rns_engine/engine.py
|
|
8
|
+
src/rns_engine/signed.py
|
|
8
9
|
src/rns_engine.egg-info/PKG-INFO
|
|
9
10
|
src/rns_engine.egg-info/SOURCES.txt
|
|
10
11
|
src/rns_engine.egg-info/dependency_links.txt
|
|
@@ -13,4 +14,5 @@ src/rns_engine.egg-info/requires.txt
|
|
|
13
14
|
src/rns_engine.egg-info/top_level.txt
|
|
14
15
|
tests/test_packaging.py
|
|
15
16
|
tests/test_rns.py
|
|
16
|
-
tests/test_session.py
|
|
17
|
+
tests/test_session.py
|
|
18
|
+
tests/test_signed.py
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
import rns_engine as rns
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_signed_constants_match_four_rail_modulus():
|
|
8
|
+
assert rns.M == 35_742_890_181_197_824
|
|
9
|
+
assert rns.HALF_M == rns.M // 2
|
|
10
|
+
assert rns.SIGNED_MIN == -rns.HALF_M
|
|
11
|
+
assert rns.SIGNED_MAX == rns.HALF_M - 1
|
|
12
|
+
assert rns.UNIQUE_SIGNED_MIN == -rns.HALF_M + 1
|
|
13
|
+
assert rns.UNIQUE_SIGNED_MAX == rns.HALF_M - 1
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_signed_round_trip_including_canonical_boundaries():
|
|
17
|
+
values = np.array(
|
|
18
|
+
[rns.SIGNED_MIN, -1_000_003, -1, 0, 1, 1_000_003, rns.SIGNED_MAX],
|
|
19
|
+
dtype=np.int64,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
decoded = rns.decode_signed(*rns.encode_signed(values))
|
|
23
|
+
|
|
24
|
+
np.testing.assert_array_equal(decoded, values)
|
|
25
|
+
assert decoded.dtype == np.int64
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_negative_one_uses_the_same_residue_as_m_minus_one():
|
|
29
|
+
signed_rails = rns.encode_signed(np.array([-1], dtype=np.int64))
|
|
30
|
+
unsigned_rails = rns.encode(np.array([rns.M - 1], dtype=np.uint64))
|
|
31
|
+
|
|
32
|
+
for signed_rail, unsigned_rail in zip(signed_rails, unsigned_rails):
|
|
33
|
+
np.testing.assert_array_equal(signed_rail, unsigned_rail)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_half_modulus_residue_has_canonical_negative_interpretation():
|
|
37
|
+
rails = rns.encode(np.array([rns.HALF_M], dtype=np.uint64))
|
|
38
|
+
|
|
39
|
+
decoded = rns.decode_signed(*rails)
|
|
40
|
+
|
|
41
|
+
np.testing.assert_array_equal(decoded, np.array([rns.SIGNED_MIN], dtype=np.int64))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_signed_encoder_refuses_silent_wraparound():
|
|
45
|
+
with pytest.raises(OverflowError, match="signed RNS input"):
|
|
46
|
+
rns.encode_signed([rns.SIGNED_MIN - 1])
|
|
47
|
+
|
|
48
|
+
with pytest.raises(OverflowError, match="signed RNS input"):
|
|
49
|
+
rns.encode_signed([rns.SIGNED_MAX + 1])
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_signed_encoder_rejects_non_integer_and_non_vector_input():
|
|
53
|
+
with pytest.raises(TypeError, match="must contain integers"):
|
|
54
|
+
rns.encode_signed([1.5, 2.5])
|
|
55
|
+
|
|
56
|
+
with pytest.raises(ValueError, match="1D"):
|
|
57
|
+
rns.encode_signed([[1, 2], [3, 4]])
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_signed_session_uses_existing_residue_arithmetic():
|
|
61
|
+
session = rns.SignedSession()
|
|
62
|
+
a = session.encode_signed([-5, 7])
|
|
63
|
+
b = session.encode_signed([3, -10])
|
|
64
|
+
|
|
65
|
+
np.testing.assert_array_equal(
|
|
66
|
+
session.decode_signed(session.add(a, b)),
|
|
67
|
+
np.array([-2, -3], dtype=np.int64),
|
|
68
|
+
)
|
|
69
|
+
np.testing.assert_array_equal(
|
|
70
|
+
session.decode_signed(session.mul(a, b)),
|
|
71
|
+
np.array([-15, -70], dtype=np.int64),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_strict_signed_bound_certificate():
|
|
76
|
+
safe = rns.certify_signed_bound(rns.HALF_M - 1)
|
|
77
|
+
assert safe.unique is True
|
|
78
|
+
assert safe.headroom == 0
|
|
79
|
+
assert safe.minimum_required_modulus == rns.M - 1
|
|
80
|
+
assert safe.require_unique() is safe
|
|
81
|
+
|
|
82
|
+
ambiguous = rns.certify_signed_bound(rns.HALF_M)
|
|
83
|
+
assert ambiguous.unique is False
|
|
84
|
+
assert ambiguous.headroom == -1
|
|
85
|
+
assert ambiguous.minimum_required_modulus == rns.M + 1
|
|
86
|
+
with pytest.raises(OverflowError, match="modular-only"):
|
|
87
|
+
ambiguous.require_unique()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_signed_bound_certificate_validates_the_bound():
|
|
91
|
+
with pytest.raises(ValueError, match=">= 0"):
|
|
92
|
+
rns.certify_signed_bound(-1)
|
|
93
|
+
|
|
94
|
+
with pytest.raises(TypeError, match="must be an integer"):
|
|
95
|
+
rns.certify_signed_bound(1.5)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_warpfrac_int8_dot_bound_is_uniquely_signed():
|
|
99
|
+
certificate = rns.certify_signed_dot_bound(
|
|
100
|
+
inner_dimension=5120,
|
|
101
|
+
left_abs_bound=127,
|
|
102
|
+
right_abs_bound=127,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
assert certificate.max_abs_bound == 82_580_480
|
|
106
|
+
assert certificate.unique is True
|
|
107
|
+
assert certificate.headroom == rns.UNIQUE_SIGNED_MAX - 82_580_480
|
|
108
|
+
assert certificate.require_unique() is certificate
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_dot_bound_accounts_for_an_existing_addend():
|
|
112
|
+
certificate = rns.certify_signed_dot_bound(3, 5, 7, addend_abs_bound=11)
|
|
113
|
+
|
|
114
|
+
assert certificate.max_abs_bound == 116
|
|
115
|
+
assert certificate.minimum_required_modulus == 233
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_dot_bound_exposes_modular_only_outputs():
|
|
119
|
+
certificate = rns.certify_signed_dot_bound(rns.HALF_M, 1, 1)
|
|
120
|
+
|
|
121
|
+
assert certificate.max_abs_bound == rns.HALF_M
|
|
122
|
+
assert certificate.unique is False
|
|
123
|
+
with pytest.raises(OverflowError, match="modular-only"):
|
|
124
|
+
certificate.require_unique()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_dot_bound_validates_each_input():
|
|
128
|
+
with pytest.raises(ValueError, match="inner_dimension"):
|
|
129
|
+
rns.certify_signed_dot_bound(-1, 1, 1)
|
|
130
|
+
|
|
131
|
+
with pytest.raises(TypeError, match="left_abs_bound"):
|
|
132
|
+
rns.certify_signed_dot_bound(1, 1.5, 1)
|
|
133
|
+
|
|
134
|
+
with pytest.raises(ValueError, match="right_abs_bound"):
|
|
135
|
+
rns.certify_signed_dot_bound(1, 1, -1)
|
|
136
|
+
|
|
137
|
+
with pytest.raises(TypeError, match="addend_abs_bound"):
|
|
138
|
+
rns.certify_signed_dot_bound(1, 1, 1, addend_abs_bound="7")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|