rns-engine 0.7.0__tar.gz → 0.8.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.7.0/src/rns_engine.egg-info → rns_engine-0.8.0}/PKG-INFO +60 -10
- {rns_engine-0.7.0 → rns_engine-0.8.0}/README.md +59 -9
- {rns_engine-0.7.0 → rns_engine-0.8.0}/pyproject.toml +1 -1
- {rns_engine-0.7.0 → rns_engine-0.8.0}/src/rns_engine/__init__.py +16 -0
- rns_engine-0.8.0/src/rns_engine/capacity.py +390 -0
- rns_engine-0.8.0/src/rns_engine/coefficients.py +179 -0
- {rns_engine-0.7.0 → rns_engine-0.8.0/src/rns_engine.egg-info}/PKG-INFO +60 -10
- {rns_engine-0.7.0 → rns_engine-0.8.0}/src/rns_engine.egg-info/SOURCES.txt +4 -0
- rns_engine-0.8.0/tests/test_capacity.py +161 -0
- rns_engine-0.8.0/tests/test_coefficients.py +110 -0
- {rns_engine-0.7.0 → rns_engine-0.8.0}/LICENSE +0 -0
- {rns_engine-0.7.0 → rns_engine-0.8.0}/setup.cfg +0 -0
- {rns_engine-0.7.0 → rns_engine-0.8.0}/setup.py +0 -0
- {rns_engine-0.7.0 → rns_engine-0.8.0}/src/rns_engine/_core.cpp +0 -0
- {rns_engine-0.7.0 → rns_engine-0.8.0}/src/rns_engine/_weighted.cpp +0 -0
- {rns_engine-0.7.0 → rns_engine-0.8.0}/src/rns_engine/engine.py +0 -0
- {rns_engine-0.7.0 → rns_engine-0.8.0}/src/rns_engine/signed.py +0 -0
- {rns_engine-0.7.0 → rns_engine-0.8.0}/src/rns_engine/weighted.py +0 -0
- {rns_engine-0.7.0 → rns_engine-0.8.0}/src/rns_engine.egg-info/dependency_links.txt +0 -0
- {rns_engine-0.7.0 → rns_engine-0.8.0}/src/rns_engine.egg-info/not-zip-safe +0 -0
- {rns_engine-0.7.0 → rns_engine-0.8.0}/src/rns_engine.egg-info/requires.txt +0 -0
- {rns_engine-0.7.0 → rns_engine-0.8.0}/src/rns_engine.egg-info/top_level.txt +0 -0
- {rns_engine-0.7.0 → rns_engine-0.8.0}/tests/test_packaging.py +0 -0
- {rns_engine-0.7.0 → rns_engine-0.8.0}/tests/test_rns.py +0 -0
- {rns_engine-0.7.0 → rns_engine-0.8.0}/tests/test_session.py +0 -0
- {rns_engine-0.7.0 → rns_engine-0.8.0}/tests/test_signed.py +0 -0
- {rns_engine-0.7.0 → rns_engine-0.8.0}/tests/test_weighted.py +0 -0
- {rns_engine-0.7.0 → rns_engine-0.8.0}/tests/test_weighted_benchmark.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rns_engine
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Exact RNS engine with session/cache API.
|
|
5
5
|
Author: Evan Wesley
|
|
6
6
|
License-Expression: AGPL-3.0-only
|
|
@@ -194,7 +194,7 @@ out = session.decode_signed(session.add(a, b))
|
|
|
194
194
|
|
|
195
195
|
## Weighted INT32 partial accumulation
|
|
196
196
|
|
|
197
|
-
Version 0.7
|
|
197
|
+
Version 0.7 introduced the fused direct bridge for digit-plane, chunked-GEMM, and Tensor Core pipelines.
|
|
198
198
|
|
|
199
199
|
Give the engine signed INT32 partial outputs with shape:
|
|
200
200
|
|
|
@@ -246,7 +246,7 @@ Important details:
|
|
|
246
246
|
- `decode_modular()` always returns the canonical residue in `[0, M)`.
|
|
247
247
|
- Empty term sets produce an exact zero result.
|
|
248
248
|
|
|
249
|
-
The
|
|
249
|
+
The implementation uses one fused native `_weighted` call to read signed INT32 partials, collect exact per-term magnitude bounds, apply positional weights modulo `M`, and accumulate all four rails. The pre-fusion staged encode → scale → add body remains available internally as an exact reference for A/B tests. Local AVX2 diagnostics measured roughly 2.1×–2.9× speedup at one million outputs and larger gains on tiny many-term cases; those figures are hardware-specific CPU evidence, not CUDA, Tensor Core, or universal performance claims.
|
|
250
250
|
|
|
251
251
|
A precomputed receipt can also be created without materializing partial arrays:
|
|
252
252
|
|
|
@@ -260,6 +260,45 @@ certificate.require_unique()
|
|
|
260
260
|
|
|
261
261
|
---
|
|
262
262
|
|
|
263
|
+
## Exact GEMM capacity planning
|
|
264
|
+
|
|
265
|
+
Version 0.8 separates two different safety questions that must not be conflated:
|
|
266
|
+
|
|
267
|
+
1. Does each grouped raw digit-plane coefficient fit the native signed accumulator?
|
|
268
|
+
2. Does the final radix-weighted result fit uniquely inside the global RNS modulus product?
|
|
269
|
+
|
|
270
|
+
```python
|
|
271
|
+
import rns_engine as rns
|
|
272
|
+
|
|
273
|
+
local = rns.plan_grouped_coefficient_capacity(
|
|
274
|
+
inner_dimension=5120,
|
|
275
|
+
left_digit_abs_bounds=[127] * 8,
|
|
276
|
+
right_digit_abs_bounds=[127] * 8,
|
|
277
|
+
accumulator_bits=32,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
global_plan = rns.plan_digit_plane_gemm_capacity(
|
|
281
|
+
inner_dimension=5120,
|
|
282
|
+
radix=128,
|
|
283
|
+
left_digit_abs_bounds=[127] * 8,
|
|
284
|
+
right_digit_abs_bounds=[127] * 8,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
assert local.max_abs_bound == 660_643_840
|
|
288
|
+
assert local.safe
|
|
289
|
+
assert local.minimum_signed_accumulator_bits == 31
|
|
290
|
+
|
|
291
|
+
assert global_plan.capacity.required_modulus_bits == 126
|
|
292
|
+
assert global_plan.capacity.additional_bits_required == 71
|
|
293
|
+
assert not global_plan.current_unique
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
That workload is locally safe in INT32 but globally modular-only in the current four-rail range. The planner can also validate explicit proposed CRT moduli and rejects candidates that are not pairwise coprime with the existing product.
|
|
297
|
+
|
|
298
|
+
See [`CAPACITY_PLANNING.md`](CAPACITY_PLANNING.md) for the exact laws, examples, and limits of what the receipts prove.
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
263
302
|
## Core API
|
|
264
303
|
|
|
265
304
|
### Rail operations
|
|
@@ -285,6 +324,16 @@ certificate.require_unique()
|
|
|
285
324
|
- `WeightedInt32Result.decode_signed(require_unique=True)`
|
|
286
325
|
- `WeightedInt32Result.decode_modular()`
|
|
287
326
|
|
|
327
|
+
### Capacity planning
|
|
328
|
+
|
|
329
|
+
- `plan_signed_capacity(max_abs_bound, additional_moduli=())`
|
|
330
|
+
- `plan_weighted_sum_capacity(weights, term_abs_bounds, addend_abs_bound=0, additional_moduli=())`
|
|
331
|
+
- `plan_digit_plane_gemm_capacity(inner_dimension, radix, left_digit_abs_bounds, right_digit_abs_bounds, ...)`
|
|
332
|
+
- `plan_grouped_coefficient_capacity(inner_dimension, left_digit_abs_bounds, right_digit_abs_bounds, accumulator_bits=32)`
|
|
333
|
+
- `SignedCapacityPlan`
|
|
334
|
+
- `DigitPlaneGemmCapacityPlan`
|
|
335
|
+
- `GroupedCoefficientCapacityPlan`
|
|
336
|
+
|
|
288
337
|
### Scalar-broadcast encoded operations
|
|
289
338
|
|
|
290
339
|
- `mul_u64(...)`
|
|
@@ -359,7 +408,7 @@ Workload: `affine_repeat_u64_io(x, 1_000_003, 7, iterations=1000)`
|
|
|
359
408
|
- OpenMP with one thread: **82.80 billion ops/sec**
|
|
360
409
|
- OpenMP with two threads: **94.86 billion ops/sec**
|
|
361
410
|
|
|
362
|
-
These measurements cover the existing affine kernels, not
|
|
411
|
+
These measurements cover the existing affine kernels, not weighted INT32 reconstruction or the pure-Python capacity planners.
|
|
363
412
|
|
|
364
413
|
---
|
|
365
414
|
|
|
@@ -404,14 +453,15 @@ rns.HAS_AVX2
|
|
|
404
453
|
|
|
405
454
|
## Current release
|
|
406
455
|
|
|
407
|
-
### v0.
|
|
456
|
+
### v0.8.0
|
|
408
457
|
|
|
458
|
+
- exact local grouped-coefficient accumulator planning
|
|
459
|
+
- exact global digit-plane GEMM RNS-capacity planning
|
|
460
|
+
- strict separation between local INT32 safety and global signed uniqueness
|
|
461
|
+
- candidate CRT-modulus validation with pairwise-coprime enforcement
|
|
462
|
+
- exact modulus shortfall and additional-bit receipts
|
|
409
463
|
- fused native weighted signed INT32 partial accumulation
|
|
410
|
-
-
|
|
411
|
-
- staged reference body retained for exact fused-versus-staged A/B benchmarking
|
|
412
|
-
- exact arbitrary-precision positional weights
|
|
413
|
-
- per-term magnitude receipts and strict no-wrap certification
|
|
414
|
-
- canonical modular and uniqueness-gated signed decode paths
|
|
464
|
+
- arbitrary-precision positional weights and no-wrap certificates
|
|
415
465
|
- canonical centered signed encoding and decoding
|
|
416
466
|
- exact dot-product and GEMM output-bound receipts
|
|
417
467
|
- `SignedSession` high-level API
|
|
@@ -171,7 +171,7 @@ out = session.decode_signed(session.add(a, b))
|
|
|
171
171
|
|
|
172
172
|
## Weighted INT32 partial accumulation
|
|
173
173
|
|
|
174
|
-
Version 0.7
|
|
174
|
+
Version 0.7 introduced the fused direct bridge for digit-plane, chunked-GEMM, and Tensor Core pipelines.
|
|
175
175
|
|
|
176
176
|
Give the engine signed INT32 partial outputs with shape:
|
|
177
177
|
|
|
@@ -223,7 +223,7 @@ Important details:
|
|
|
223
223
|
- `decode_modular()` always returns the canonical residue in `[0, M)`.
|
|
224
224
|
- Empty term sets produce an exact zero result.
|
|
225
225
|
|
|
226
|
-
The
|
|
226
|
+
The implementation uses one fused native `_weighted` call to read signed INT32 partials, collect exact per-term magnitude bounds, apply positional weights modulo `M`, and accumulate all four rails. The pre-fusion staged encode → scale → add body remains available internally as an exact reference for A/B tests. Local AVX2 diagnostics measured roughly 2.1×–2.9× speedup at one million outputs and larger gains on tiny many-term cases; those figures are hardware-specific CPU evidence, not CUDA, Tensor Core, or universal performance claims.
|
|
227
227
|
|
|
228
228
|
A precomputed receipt can also be created without materializing partial arrays:
|
|
229
229
|
|
|
@@ -237,6 +237,45 @@ certificate.require_unique()
|
|
|
237
237
|
|
|
238
238
|
---
|
|
239
239
|
|
|
240
|
+
## Exact GEMM capacity planning
|
|
241
|
+
|
|
242
|
+
Version 0.8 separates two different safety questions that must not be conflated:
|
|
243
|
+
|
|
244
|
+
1. Does each grouped raw digit-plane coefficient fit the native signed accumulator?
|
|
245
|
+
2. Does the final radix-weighted result fit uniquely inside the global RNS modulus product?
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
import rns_engine as rns
|
|
249
|
+
|
|
250
|
+
local = rns.plan_grouped_coefficient_capacity(
|
|
251
|
+
inner_dimension=5120,
|
|
252
|
+
left_digit_abs_bounds=[127] * 8,
|
|
253
|
+
right_digit_abs_bounds=[127] * 8,
|
|
254
|
+
accumulator_bits=32,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
global_plan = rns.plan_digit_plane_gemm_capacity(
|
|
258
|
+
inner_dimension=5120,
|
|
259
|
+
radix=128,
|
|
260
|
+
left_digit_abs_bounds=[127] * 8,
|
|
261
|
+
right_digit_abs_bounds=[127] * 8,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
assert local.max_abs_bound == 660_643_840
|
|
265
|
+
assert local.safe
|
|
266
|
+
assert local.minimum_signed_accumulator_bits == 31
|
|
267
|
+
|
|
268
|
+
assert global_plan.capacity.required_modulus_bits == 126
|
|
269
|
+
assert global_plan.capacity.additional_bits_required == 71
|
|
270
|
+
assert not global_plan.current_unique
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
That workload is locally safe in INT32 but globally modular-only in the current four-rail range. The planner can also validate explicit proposed CRT moduli and rejects candidates that are not pairwise coprime with the existing product.
|
|
274
|
+
|
|
275
|
+
See [`CAPACITY_PLANNING.md`](CAPACITY_PLANNING.md) for the exact laws, examples, and limits of what the receipts prove.
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
240
279
|
## Core API
|
|
241
280
|
|
|
242
281
|
### Rail operations
|
|
@@ -262,6 +301,16 @@ certificate.require_unique()
|
|
|
262
301
|
- `WeightedInt32Result.decode_signed(require_unique=True)`
|
|
263
302
|
- `WeightedInt32Result.decode_modular()`
|
|
264
303
|
|
|
304
|
+
### Capacity planning
|
|
305
|
+
|
|
306
|
+
- `plan_signed_capacity(max_abs_bound, additional_moduli=())`
|
|
307
|
+
- `plan_weighted_sum_capacity(weights, term_abs_bounds, addend_abs_bound=0, additional_moduli=())`
|
|
308
|
+
- `plan_digit_plane_gemm_capacity(inner_dimension, radix, left_digit_abs_bounds, right_digit_abs_bounds, ...)`
|
|
309
|
+
- `plan_grouped_coefficient_capacity(inner_dimension, left_digit_abs_bounds, right_digit_abs_bounds, accumulator_bits=32)`
|
|
310
|
+
- `SignedCapacityPlan`
|
|
311
|
+
- `DigitPlaneGemmCapacityPlan`
|
|
312
|
+
- `GroupedCoefficientCapacityPlan`
|
|
313
|
+
|
|
265
314
|
### Scalar-broadcast encoded operations
|
|
266
315
|
|
|
267
316
|
- `mul_u64(...)`
|
|
@@ -336,7 +385,7 @@ Workload: `affine_repeat_u64_io(x, 1_000_003, 7, iterations=1000)`
|
|
|
336
385
|
- OpenMP with one thread: **82.80 billion ops/sec**
|
|
337
386
|
- OpenMP with two threads: **94.86 billion ops/sec**
|
|
338
387
|
|
|
339
|
-
These measurements cover the existing affine kernels, not
|
|
388
|
+
These measurements cover the existing affine kernels, not weighted INT32 reconstruction or the pure-Python capacity planners.
|
|
340
389
|
|
|
341
390
|
---
|
|
342
391
|
|
|
@@ -381,14 +430,15 @@ rns.HAS_AVX2
|
|
|
381
430
|
|
|
382
431
|
## Current release
|
|
383
432
|
|
|
384
|
-
### v0.
|
|
433
|
+
### v0.8.0
|
|
385
434
|
|
|
435
|
+
- exact local grouped-coefficient accumulator planning
|
|
436
|
+
- exact global digit-plane GEMM RNS-capacity planning
|
|
437
|
+
- strict separation between local INT32 safety and global signed uniqueness
|
|
438
|
+
- candidate CRT-modulus validation with pairwise-coprime enforcement
|
|
439
|
+
- exact modulus shortfall and additional-bit receipts
|
|
386
440
|
- fused native weighted signed INT32 partial accumulation
|
|
387
|
-
-
|
|
388
|
-
- staged reference body retained for exact fused-versus-staged A/B benchmarking
|
|
389
|
-
- exact arbitrary-precision positional weights
|
|
390
|
-
- per-term magnitude receipts and strict no-wrap certification
|
|
391
|
-
- canonical modular and uniqueness-gated signed decode paths
|
|
441
|
+
- arbitrary-precision positional weights and no-wrap certificates
|
|
392
442
|
- canonical centered signed encoding and decoding
|
|
393
443
|
- exact dot-product and GEMM output-bound receipts
|
|
394
444
|
- `SignedSession` high-level API
|
|
@@ -57,6 +57,17 @@ from .weighted import (
|
|
|
57
57
|
accumulate_weighted_int32,
|
|
58
58
|
certify_weighted_sum_bound,
|
|
59
59
|
)
|
|
60
|
+
from .capacity import (
|
|
61
|
+
DigitPlaneGemmCapacityPlan,
|
|
62
|
+
SignedCapacityPlan,
|
|
63
|
+
plan_digit_plane_gemm_capacity,
|
|
64
|
+
plan_signed_capacity,
|
|
65
|
+
plan_weighted_sum_capacity,
|
|
66
|
+
)
|
|
67
|
+
from .coefficients import (
|
|
68
|
+
GroupedCoefficientCapacityPlan,
|
|
69
|
+
plan_grouped_coefficient_capacity,
|
|
70
|
+
)
|
|
60
71
|
|
|
61
72
|
try:
|
|
62
73
|
__version__ = _distribution_version("rns_engine")
|
|
@@ -83,6 +94,10 @@ __all__ = [
|
|
|
83
94
|
"EncodedArray", "SessionCache", "Session", "SignedSession",
|
|
84
95
|
"SignedRangeCertificate", "certify_signed_bound", "certify_signed_dot_bound",
|
|
85
96
|
"WeightedInt32Result", "accumulate_weighted_int32", "certify_weighted_sum_bound",
|
|
97
|
+
"SignedCapacityPlan", "DigitPlaneGemmCapacityPlan",
|
|
98
|
+
"GroupedCoefficientCapacityPlan",
|
|
99
|
+
"plan_signed_capacity", "plan_weighted_sum_capacity",
|
|
100
|
+
"plan_digit_plane_gemm_capacity", "plan_grouped_coefficient_capacity",
|
|
86
101
|
"info",
|
|
87
102
|
]
|
|
88
103
|
|
|
@@ -96,3 +111,4 @@ def info():
|
|
|
96
111
|
print(f" AVX2 : {'yes' if HAS_AVX2 else 'no'}")
|
|
97
112
|
print(" Core APIs : add/sub/mul/fma + raw/omp/auto scalar-broadcast family")
|
|
98
113
|
print(" Bridge APIs : weighted signed INT32 accumulation with range receipts")
|
|
114
|
+
print(" Planning APIs : local coefficient safety + global digit-plane RNS capacity")
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from math import gcd, prod
|
|
5
|
+
from operator import index
|
|
6
|
+
from typing import Any, Iterable, Sequence
|
|
7
|
+
|
|
8
|
+
from ._core import M
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _require_integer(value: Any, *, name: str) -> int:
|
|
12
|
+
try:
|
|
13
|
+
return index(value)
|
|
14
|
+
except TypeError as exc:
|
|
15
|
+
raise TypeError(f"{name} must be an integer") from exc
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _require_nonnegative_integer(value: Any, *, name: str) -> int:
|
|
19
|
+
integer = _require_integer(value, name=name)
|
|
20
|
+
if integer < 0:
|
|
21
|
+
raise ValueError(f"{name} must be >= 0")
|
|
22
|
+
return integer
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _require_radix(value: Any) -> int:
|
|
26
|
+
radix = _require_integer(value, name="radix")
|
|
27
|
+
if radix < 2:
|
|
28
|
+
raise ValueError("radix must be >= 2")
|
|
29
|
+
return radix
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _nonnegative_tuple(values: Iterable[Any], *, name: str) -> tuple[int, ...]:
|
|
33
|
+
try:
|
|
34
|
+
raw = tuple(values)
|
|
35
|
+
except TypeError as exc:
|
|
36
|
+
raise TypeError(f"{name} must be an iterable of nonnegative integers") from exc
|
|
37
|
+
|
|
38
|
+
return tuple(
|
|
39
|
+
_require_nonnegative_integer(value, name=f"{name}[{position}]")
|
|
40
|
+
for position, value in enumerate(raw)
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _integer_tuple(values: Iterable[Any], *, name: str) -> tuple[int, ...]:
|
|
45
|
+
try:
|
|
46
|
+
raw = tuple(values)
|
|
47
|
+
except TypeError as exc:
|
|
48
|
+
raise TypeError(f"{name} must be an iterable of integers") from exc
|
|
49
|
+
|
|
50
|
+
return tuple(
|
|
51
|
+
_require_integer(value, name=f"{name}[{position}]")
|
|
52
|
+
for position, value in enumerate(raw)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _validate_additional_moduli(values: Iterable[Any]) -> tuple[int, ...]:
|
|
57
|
+
moduli = _integer_tuple(values, name="additional_moduli")
|
|
58
|
+
running_product = int(M)
|
|
59
|
+
|
|
60
|
+
for position, modulus in enumerate(moduli):
|
|
61
|
+
if modulus <= 1:
|
|
62
|
+
raise ValueError(f"additional_moduli[{position}] must be > 1")
|
|
63
|
+
|
|
64
|
+
common_factor = gcd(running_product, modulus)
|
|
65
|
+
if common_factor != 1:
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"additional_moduli[{position}]={modulus} must be coprime with "
|
|
68
|
+
f"the existing modulus product; gcd={common_factor}"
|
|
69
|
+
)
|
|
70
|
+
running_product *= modulus
|
|
71
|
+
|
|
72
|
+
return moduli
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _ceil_div(numerator: int, denominator: int) -> int:
|
|
76
|
+
return (numerator + denominator - 1) // denominator
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True, slots=True)
|
|
80
|
+
class SignedCapacityPlan:
|
|
81
|
+
"""Exact modulus-capacity receipt for a symmetric signed result bound.
|
|
82
|
+
|
|
83
|
+
The current engine uniquely represents every integer in ``[-bound, bound]``
|
|
84
|
+
only when its modulus is at least ``2 * bound + 1``. Optional additional
|
|
85
|
+
moduli are treated as proposed CRT rails and must be pairwise coprime with
|
|
86
|
+
the current four-rail modulus product and with one another.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
max_abs_bound: int
|
|
90
|
+
additional_moduli: tuple[int, ...] = ()
|
|
91
|
+
|
|
92
|
+
def __post_init__(self) -> None:
|
|
93
|
+
bound = _require_nonnegative_integer(
|
|
94
|
+
self.max_abs_bound,
|
|
95
|
+
name="max_abs_bound",
|
|
96
|
+
)
|
|
97
|
+
moduli = _validate_additional_moduli(self.additional_moduli)
|
|
98
|
+
object.__setattr__(self, "max_abs_bound", bound)
|
|
99
|
+
object.__setattr__(self, "additional_moduli", moduli)
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def current_modulus(self) -> int:
|
|
103
|
+
return int(M)
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def additional_modulus_product(self) -> int:
|
|
107
|
+
return prod(self.additional_moduli, start=1)
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def available_modulus(self) -> int:
|
|
111
|
+
return self.current_modulus * self.additional_modulus_product
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def minimum_required_modulus(self) -> int:
|
|
115
|
+
return 2 * self.max_abs_bound + 1
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def current_unique(self) -> bool:
|
|
119
|
+
return self.current_modulus >= self.minimum_required_modulus
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def unique(self) -> bool:
|
|
123
|
+
return self.available_modulus >= self.minimum_required_modulus
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def current_headroom(self) -> int:
|
|
127
|
+
return (self.current_modulus - 1) // 2 - self.max_abs_bound
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def available_headroom(self) -> int:
|
|
131
|
+
return (self.available_modulus - 1) // 2 - self.max_abs_bound
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def modulus_shortfall(self) -> int:
|
|
135
|
+
return max(0, self.minimum_required_modulus - self.available_modulus)
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def minimum_additional_product_factor(self) -> int:
|
|
139
|
+
"""Smallest exact product factor needed beyond the current modulus."""
|
|
140
|
+
|
|
141
|
+
return max(
|
|
142
|
+
1,
|
|
143
|
+
_ceil_div(self.minimum_required_modulus, self.current_modulus),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def minimum_single_coprime_modulus(self) -> int:
|
|
148
|
+
"""Smallest one-rail modulus that closes the current capacity deficit.
|
|
149
|
+
|
|
150
|
+
This is a mathematical CRT-capacity result, not a claim that the
|
|
151
|
+
returned modulus has efficient reduction or hardware properties.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
candidate = self.minimum_additional_product_factor
|
|
155
|
+
if candidate <= 1:
|
|
156
|
+
return 1
|
|
157
|
+
while gcd(candidate, self.current_modulus) != 1:
|
|
158
|
+
candidate += 1
|
|
159
|
+
return candidate
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def additional_bits_required(self) -> int:
|
|
163
|
+
"""Power-of-two bit budget needed beyond the current modulus product."""
|
|
164
|
+
|
|
165
|
+
factor = self.minimum_additional_product_factor
|
|
166
|
+
return 0 if factor <= 1 else (factor - 1).bit_length()
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def required_modulus_bits(self) -> int:
|
|
170
|
+
return self.minimum_required_modulus.bit_length()
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def available_modulus_bits(self) -> int:
|
|
174
|
+
return self.available_modulus.bit_length()
|
|
175
|
+
|
|
176
|
+
def require_unique(self) -> SignedCapacityPlan:
|
|
177
|
+
if not self.unique:
|
|
178
|
+
raise OverflowError(
|
|
179
|
+
"signed result is modular-only: available modulus "
|
|
180
|
+
f"{self.available_modulus} is smaller than the required modulus "
|
|
181
|
+
f"{self.minimum_required_modulus} for absolute bound "
|
|
182
|
+
f"{self.max_abs_bound}"
|
|
183
|
+
)
|
|
184
|
+
return self
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def plan_signed_capacity(
|
|
188
|
+
max_abs_bound: int,
|
|
189
|
+
additional_moduli: Sequence[int] | Iterable[int] = (),
|
|
190
|
+
) -> SignedCapacityPlan:
|
|
191
|
+
"""Plan exact signed capacity for a known absolute result bound."""
|
|
192
|
+
|
|
193
|
+
return SignedCapacityPlan(
|
|
194
|
+
max_abs_bound=max_abs_bound,
|
|
195
|
+
additional_moduli=tuple(additional_moduli),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def plan_weighted_sum_capacity(
|
|
200
|
+
weights: Sequence[int] | Iterable[int],
|
|
201
|
+
term_abs_bounds: Sequence[int] | Iterable[int],
|
|
202
|
+
addend_abs_bound: int = 0,
|
|
203
|
+
additional_moduli: Sequence[int] | Iterable[int] = (),
|
|
204
|
+
) -> SignedCapacityPlan:
|
|
205
|
+
"""Plan capacity for ``sum(weight[t] * term[t]) + addend`` exactly."""
|
|
206
|
+
|
|
207
|
+
exact_weights = _integer_tuple(weights, name="weights")
|
|
208
|
+
exact_bounds = _nonnegative_tuple(term_abs_bounds, name="term_abs_bounds")
|
|
209
|
+
if len(exact_weights) != len(exact_bounds):
|
|
210
|
+
raise ValueError(
|
|
211
|
+
"weights and term_abs_bounds must have the same length "
|
|
212
|
+
f"({len(exact_weights)} != {len(exact_bounds)})"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
addend = _require_nonnegative_integer(
|
|
216
|
+
addend_abs_bound,
|
|
217
|
+
name="addend_abs_bound",
|
|
218
|
+
)
|
|
219
|
+
bound = addend + sum(
|
|
220
|
+
abs(weight) * term_bound
|
|
221
|
+
for weight, term_bound in zip(exact_weights, exact_bounds)
|
|
222
|
+
)
|
|
223
|
+
return plan_signed_capacity(bound, additional_moduli)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@dataclass(frozen=True, slots=True)
|
|
227
|
+
class DigitPlaneGemmCapacityPlan:
|
|
228
|
+
"""Exact worst-case capacity plan for positional digit-plane GEMM.
|
|
229
|
+
|
|
230
|
+
If ``A = sum(A_i * radix**i)`` and ``B = sum(B_j * radix**j)``, with
|
|
231
|
+
per-plane entry magnitude bounds ``left_digit_abs_bounds[i]`` and
|
|
232
|
+
``right_digit_abs_bounds[j]``, then one output entry obeys::
|
|
233
|
+
|
|
234
|
+
|C| <= K * left_value_abs_bound * right_value_abs_bound + addend
|
|
235
|
+
|
|
236
|
+
This is algebraically identical to summing every plane-pair GEMM with its
|
|
237
|
+
positional weight ``radix**(i + j)``. No floating point is used.
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
inner_dimension: int
|
|
241
|
+
radix: int
|
|
242
|
+
left_digit_abs_bounds: tuple[int, ...]
|
|
243
|
+
right_digit_abs_bounds: tuple[int, ...]
|
|
244
|
+
addend_abs_bound: int = 0
|
|
245
|
+
additional_moduli: tuple[int, ...] = ()
|
|
246
|
+
|
|
247
|
+
def __post_init__(self) -> None:
|
|
248
|
+
object.__setattr__(
|
|
249
|
+
self,
|
|
250
|
+
"inner_dimension",
|
|
251
|
+
_require_nonnegative_integer(
|
|
252
|
+
self.inner_dimension,
|
|
253
|
+
name="inner_dimension",
|
|
254
|
+
),
|
|
255
|
+
)
|
|
256
|
+
object.__setattr__(self, "radix", _require_radix(self.radix))
|
|
257
|
+
object.__setattr__(
|
|
258
|
+
self,
|
|
259
|
+
"left_digit_abs_bounds",
|
|
260
|
+
_nonnegative_tuple(
|
|
261
|
+
self.left_digit_abs_bounds,
|
|
262
|
+
name="left_digit_abs_bounds",
|
|
263
|
+
),
|
|
264
|
+
)
|
|
265
|
+
object.__setattr__(
|
|
266
|
+
self,
|
|
267
|
+
"right_digit_abs_bounds",
|
|
268
|
+
_nonnegative_tuple(
|
|
269
|
+
self.right_digit_abs_bounds,
|
|
270
|
+
name="right_digit_abs_bounds",
|
|
271
|
+
),
|
|
272
|
+
)
|
|
273
|
+
object.__setattr__(
|
|
274
|
+
self,
|
|
275
|
+
"addend_abs_bound",
|
|
276
|
+
_require_nonnegative_integer(
|
|
277
|
+
self.addend_abs_bound,
|
|
278
|
+
name="addend_abs_bound",
|
|
279
|
+
),
|
|
280
|
+
)
|
|
281
|
+
object.__setattr__(
|
|
282
|
+
self,
|
|
283
|
+
"additional_moduli",
|
|
284
|
+
_validate_additional_moduli(self.additional_moduli),
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
@property
|
|
288
|
+
def left_plane_count(self) -> int:
|
|
289
|
+
return len(self.left_digit_abs_bounds)
|
|
290
|
+
|
|
291
|
+
@property
|
|
292
|
+
def right_plane_count(self) -> int:
|
|
293
|
+
return len(self.right_digit_abs_bounds)
|
|
294
|
+
|
|
295
|
+
@property
|
|
296
|
+
def plane_pair_count(self) -> int:
|
|
297
|
+
return self.left_plane_count * self.right_plane_count
|
|
298
|
+
|
|
299
|
+
@property
|
|
300
|
+
def grouped_coefficient_count(self) -> int:
|
|
301
|
+
if self.left_plane_count == 0 or self.right_plane_count == 0:
|
|
302
|
+
return 0
|
|
303
|
+
return self.left_plane_count + self.right_plane_count - 1
|
|
304
|
+
|
|
305
|
+
@property
|
|
306
|
+
def left_value_abs_bound(self) -> int:
|
|
307
|
+
return sum(
|
|
308
|
+
digit_bound * self.radix**position
|
|
309
|
+
for position, digit_bound in enumerate(self.left_digit_abs_bounds)
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
@property
|
|
313
|
+
def right_value_abs_bound(self) -> int:
|
|
314
|
+
return sum(
|
|
315
|
+
digit_bound * self.radix**position
|
|
316
|
+
for position, digit_bound in enumerate(self.right_digit_abs_bounds)
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
@property
|
|
320
|
+
def plane_pair_abs_bounds(self) -> tuple[int, ...]:
|
|
321
|
+
return tuple(
|
|
322
|
+
self.inner_dimension * left_bound * right_bound
|
|
323
|
+
for left_bound in self.left_digit_abs_bounds
|
|
324
|
+
for right_bound in self.right_digit_abs_bounds
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
@property
|
|
328
|
+
def plane_pair_weights(self) -> tuple[int, ...]:
|
|
329
|
+
return tuple(
|
|
330
|
+
self.radix ** (left_position + right_position)
|
|
331
|
+
for left_position in range(self.left_plane_count)
|
|
332
|
+
for right_position in range(self.right_plane_count)
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
@property
|
|
336
|
+
def max_abs_bound(self) -> int:
|
|
337
|
+
return (
|
|
338
|
+
self.inner_dimension
|
|
339
|
+
* self.left_value_abs_bound
|
|
340
|
+
* self.right_value_abs_bound
|
|
341
|
+
+ self.addend_abs_bound
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
@property
|
|
345
|
+
def capacity(self) -> SignedCapacityPlan:
|
|
346
|
+
return plan_signed_capacity(
|
|
347
|
+
self.max_abs_bound,
|
|
348
|
+
self.additional_moduli,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
@property
|
|
352
|
+
def current_unique(self) -> bool:
|
|
353
|
+
return self.capacity.current_unique
|
|
354
|
+
|
|
355
|
+
@property
|
|
356
|
+
def unique(self) -> bool:
|
|
357
|
+
return self.capacity.unique
|
|
358
|
+
|
|
359
|
+
def require_unique(self) -> DigitPlaneGemmCapacityPlan:
|
|
360
|
+
self.capacity.require_unique()
|
|
361
|
+
return self
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def plan_digit_plane_gemm_capacity(
|
|
365
|
+
inner_dimension: int,
|
|
366
|
+
radix: int,
|
|
367
|
+
left_digit_abs_bounds: Sequence[int] | Iterable[int],
|
|
368
|
+
right_digit_abs_bounds: Sequence[int] | Iterable[int],
|
|
369
|
+
addend_abs_bound: int = 0,
|
|
370
|
+
additional_moduli: Sequence[int] | Iterable[int] = (),
|
|
371
|
+
) -> DigitPlaneGemmCapacityPlan:
|
|
372
|
+
"""Build an exact positional digit-plane GEMM capacity receipt."""
|
|
373
|
+
|
|
374
|
+
return DigitPlaneGemmCapacityPlan(
|
|
375
|
+
inner_dimension=inner_dimension,
|
|
376
|
+
radix=radix,
|
|
377
|
+
left_digit_abs_bounds=tuple(left_digit_abs_bounds),
|
|
378
|
+
right_digit_abs_bounds=tuple(right_digit_abs_bounds),
|
|
379
|
+
addend_abs_bound=addend_abs_bound,
|
|
380
|
+
additional_moduli=tuple(additional_moduli),
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
__all__ = [
|
|
385
|
+
"DigitPlaneGemmCapacityPlan",
|
|
386
|
+
"SignedCapacityPlan",
|
|
387
|
+
"plan_digit_plane_gemm_capacity",
|
|
388
|
+
"plan_signed_capacity",
|
|
389
|
+
"plan_weighted_sum_capacity",
|
|
390
|
+
]
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from operator import index
|
|
5
|
+
from typing import Any, Iterable, Sequence
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _require_integer(value: Any, *, name: str) -> int:
|
|
9
|
+
try:
|
|
10
|
+
return index(value)
|
|
11
|
+
except TypeError as exc:
|
|
12
|
+
raise TypeError(f"{name} must be an integer") from exc
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _require_nonnegative_integer(value: Any, *, name: str) -> int:
|
|
16
|
+
integer = _require_integer(value, name=name)
|
|
17
|
+
if integer < 0:
|
|
18
|
+
raise ValueError(f"{name} must be >= 0")
|
|
19
|
+
return integer
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _nonnegative_tuple(values: Iterable[Any], *, name: str) -> tuple[int, ...]:
|
|
23
|
+
try:
|
|
24
|
+
raw = tuple(values)
|
|
25
|
+
except TypeError as exc:
|
|
26
|
+
raise TypeError(f"{name} must be an iterable of nonnegative integers") from exc
|
|
27
|
+
|
|
28
|
+
return tuple(
|
|
29
|
+
_require_nonnegative_integer(value, name=f"{name}[{position}]")
|
|
30
|
+
for position, value in enumerate(raw)
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _ceil_log2(value: int) -> int:
|
|
35
|
+
if value <= 1:
|
|
36
|
+
return 0
|
|
37
|
+
return (value - 1).bit_length()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True, slots=True)
|
|
41
|
+
class GroupedCoefficientCapacityPlan:
|
|
42
|
+
"""Exact local-accumulator receipt for grouped digit-plane products.
|
|
43
|
+
|
|
44
|
+
For raw positional coefficients
|
|
45
|
+
|
|
46
|
+
``R[k] = sum_{i+j=k} GEMM(A_i, B_j)``,
|
|
47
|
+
|
|
48
|
+
each contributing plane-pair GEMM has magnitude bound
|
|
49
|
+
|
|
50
|
+
``inner_dimension * left_bound[i] * right_bound[j]``.
|
|
51
|
+
|
|
52
|
+
This receipt answers whether those grouped coefficients fit a signed native
|
|
53
|
+
accumulator such as INT32. It deliberately does not answer whether the
|
|
54
|
+
later radix-weighted wide reconstruction fits the global RNS modulus; that
|
|
55
|
+
is handled by :class:`DigitPlaneGemmCapacityPlan`.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
inner_dimension: int
|
|
59
|
+
left_digit_abs_bounds: tuple[int, ...]
|
|
60
|
+
right_digit_abs_bounds: tuple[int, ...]
|
|
61
|
+
accumulator_bits: int = 32
|
|
62
|
+
|
|
63
|
+
def __post_init__(self) -> None:
|
|
64
|
+
object.__setattr__(
|
|
65
|
+
self,
|
|
66
|
+
"inner_dimension",
|
|
67
|
+
_require_nonnegative_integer(
|
|
68
|
+
self.inner_dimension,
|
|
69
|
+
name="inner_dimension",
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
object.__setattr__(
|
|
73
|
+
self,
|
|
74
|
+
"left_digit_abs_bounds",
|
|
75
|
+
_nonnegative_tuple(
|
|
76
|
+
self.left_digit_abs_bounds,
|
|
77
|
+
name="left_digit_abs_bounds",
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
object.__setattr__(
|
|
81
|
+
self,
|
|
82
|
+
"right_digit_abs_bounds",
|
|
83
|
+
_nonnegative_tuple(
|
|
84
|
+
self.right_digit_abs_bounds,
|
|
85
|
+
name="right_digit_abs_bounds",
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
bits = _require_integer(self.accumulator_bits, name="accumulator_bits")
|
|
89
|
+
if bits < 1:
|
|
90
|
+
raise ValueError("accumulator_bits must be >= 1")
|
|
91
|
+
object.__setattr__(self, "accumulator_bits", bits)
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def coefficient_count(self) -> int:
|
|
95
|
+
if not self.left_digit_abs_bounds or not self.right_digit_abs_bounds:
|
|
96
|
+
return 0
|
|
97
|
+
return len(self.left_digit_abs_bounds) + len(self.right_digit_abs_bounds) - 1
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def plane_pair_count(self) -> int:
|
|
101
|
+
return len(self.left_digit_abs_bounds) * len(self.right_digit_abs_bounds)
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def coefficient_pair_counts(self) -> tuple[int, ...]:
|
|
105
|
+
return tuple(
|
|
106
|
+
sum(
|
|
107
|
+
1
|
|
108
|
+
for left_position in range(len(self.left_digit_abs_bounds))
|
|
109
|
+
for right_position in range(len(self.right_digit_abs_bounds))
|
|
110
|
+
if left_position + right_position == coefficient_position
|
|
111
|
+
)
|
|
112
|
+
for coefficient_position in range(self.coefficient_count)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def coefficient_abs_bounds(self) -> tuple[int, ...]:
|
|
117
|
+
return tuple(
|
|
118
|
+
sum(
|
|
119
|
+
self.inner_dimension
|
|
120
|
+
* self.left_digit_abs_bounds[left_position]
|
|
121
|
+
* self.right_digit_abs_bounds[right_position]
|
|
122
|
+
for left_position in range(len(self.left_digit_abs_bounds))
|
|
123
|
+
for right_position in range(len(self.right_digit_abs_bounds))
|
|
124
|
+
if left_position + right_position == coefficient_position
|
|
125
|
+
)
|
|
126
|
+
for coefficient_position in range(self.coefficient_count)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def max_abs_bound(self) -> int:
|
|
131
|
+
return max(self.coefficient_abs_bounds, default=0)
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def signed_accumulator_limit(self) -> int:
|
|
135
|
+
return (1 << (self.accumulator_bits - 1)) - 1
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def safe(self) -> bool:
|
|
139
|
+
return self.max_abs_bound <= self.signed_accumulator_limit
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def headroom(self) -> int:
|
|
143
|
+
return self.signed_accumulator_limit - self.max_abs_bound
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def minimum_signed_accumulator_bits(self) -> int:
|
|
147
|
+
return 1 + _ceil_log2(self.max_abs_bound + 1)
|
|
148
|
+
|
|
149
|
+
def require_safe(self) -> GroupedCoefficientCapacityPlan:
|
|
150
|
+
if not self.safe:
|
|
151
|
+
raise OverflowError(
|
|
152
|
+
"grouped coefficient exceeds the signed accumulator: bound "
|
|
153
|
+
f"{self.max_abs_bound} is greater than the "
|
|
154
|
+
f"{self.accumulator_bits}-bit limit "
|
|
155
|
+
f"{self.signed_accumulator_limit}"
|
|
156
|
+
)
|
|
157
|
+
return self
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def plan_grouped_coefficient_capacity(
|
|
161
|
+
inner_dimension: int,
|
|
162
|
+
left_digit_abs_bounds: Sequence[int] | Iterable[int],
|
|
163
|
+
right_digit_abs_bounds: Sequence[int] | Iterable[int],
|
|
164
|
+
accumulator_bits: int = 32,
|
|
165
|
+
) -> GroupedCoefficientCapacityPlan:
|
|
166
|
+
"""Build an exact local grouped-coefficient accumulator receipt."""
|
|
167
|
+
|
|
168
|
+
return GroupedCoefficientCapacityPlan(
|
|
169
|
+
inner_dimension=inner_dimension,
|
|
170
|
+
left_digit_abs_bounds=tuple(left_digit_abs_bounds),
|
|
171
|
+
right_digit_abs_bounds=tuple(right_digit_abs_bounds),
|
|
172
|
+
accumulator_bits=accumulator_bits,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
__all__ = [
|
|
177
|
+
"GroupedCoefficientCapacityPlan",
|
|
178
|
+
"plan_grouped_coefficient_capacity",
|
|
179
|
+
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rns_engine
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Exact RNS engine with session/cache API.
|
|
5
5
|
Author: Evan Wesley
|
|
6
6
|
License-Expression: AGPL-3.0-only
|
|
@@ -194,7 +194,7 @@ out = session.decode_signed(session.add(a, b))
|
|
|
194
194
|
|
|
195
195
|
## Weighted INT32 partial accumulation
|
|
196
196
|
|
|
197
|
-
Version 0.7
|
|
197
|
+
Version 0.7 introduced the fused direct bridge for digit-plane, chunked-GEMM, and Tensor Core pipelines.
|
|
198
198
|
|
|
199
199
|
Give the engine signed INT32 partial outputs with shape:
|
|
200
200
|
|
|
@@ -246,7 +246,7 @@ Important details:
|
|
|
246
246
|
- `decode_modular()` always returns the canonical residue in `[0, M)`.
|
|
247
247
|
- Empty term sets produce an exact zero result.
|
|
248
248
|
|
|
249
|
-
The
|
|
249
|
+
The implementation uses one fused native `_weighted` call to read signed INT32 partials, collect exact per-term magnitude bounds, apply positional weights modulo `M`, and accumulate all four rails. The pre-fusion staged encode → scale → add body remains available internally as an exact reference for A/B tests. Local AVX2 diagnostics measured roughly 2.1×–2.9× speedup at one million outputs and larger gains on tiny many-term cases; those figures are hardware-specific CPU evidence, not CUDA, Tensor Core, or universal performance claims.
|
|
250
250
|
|
|
251
251
|
A precomputed receipt can also be created without materializing partial arrays:
|
|
252
252
|
|
|
@@ -260,6 +260,45 @@ certificate.require_unique()
|
|
|
260
260
|
|
|
261
261
|
---
|
|
262
262
|
|
|
263
|
+
## Exact GEMM capacity planning
|
|
264
|
+
|
|
265
|
+
Version 0.8 separates two different safety questions that must not be conflated:
|
|
266
|
+
|
|
267
|
+
1. Does each grouped raw digit-plane coefficient fit the native signed accumulator?
|
|
268
|
+
2. Does the final radix-weighted result fit uniquely inside the global RNS modulus product?
|
|
269
|
+
|
|
270
|
+
```python
|
|
271
|
+
import rns_engine as rns
|
|
272
|
+
|
|
273
|
+
local = rns.plan_grouped_coefficient_capacity(
|
|
274
|
+
inner_dimension=5120,
|
|
275
|
+
left_digit_abs_bounds=[127] * 8,
|
|
276
|
+
right_digit_abs_bounds=[127] * 8,
|
|
277
|
+
accumulator_bits=32,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
global_plan = rns.plan_digit_plane_gemm_capacity(
|
|
281
|
+
inner_dimension=5120,
|
|
282
|
+
radix=128,
|
|
283
|
+
left_digit_abs_bounds=[127] * 8,
|
|
284
|
+
right_digit_abs_bounds=[127] * 8,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
assert local.max_abs_bound == 660_643_840
|
|
288
|
+
assert local.safe
|
|
289
|
+
assert local.minimum_signed_accumulator_bits == 31
|
|
290
|
+
|
|
291
|
+
assert global_plan.capacity.required_modulus_bits == 126
|
|
292
|
+
assert global_plan.capacity.additional_bits_required == 71
|
|
293
|
+
assert not global_plan.current_unique
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
That workload is locally safe in INT32 but globally modular-only in the current four-rail range. The planner can also validate explicit proposed CRT moduli and rejects candidates that are not pairwise coprime with the existing product.
|
|
297
|
+
|
|
298
|
+
See [`CAPACITY_PLANNING.md`](CAPACITY_PLANNING.md) for the exact laws, examples, and limits of what the receipts prove.
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
263
302
|
## Core API
|
|
264
303
|
|
|
265
304
|
### Rail operations
|
|
@@ -285,6 +324,16 @@ certificate.require_unique()
|
|
|
285
324
|
- `WeightedInt32Result.decode_signed(require_unique=True)`
|
|
286
325
|
- `WeightedInt32Result.decode_modular()`
|
|
287
326
|
|
|
327
|
+
### Capacity planning
|
|
328
|
+
|
|
329
|
+
- `plan_signed_capacity(max_abs_bound, additional_moduli=())`
|
|
330
|
+
- `plan_weighted_sum_capacity(weights, term_abs_bounds, addend_abs_bound=0, additional_moduli=())`
|
|
331
|
+
- `plan_digit_plane_gemm_capacity(inner_dimension, radix, left_digit_abs_bounds, right_digit_abs_bounds, ...)`
|
|
332
|
+
- `plan_grouped_coefficient_capacity(inner_dimension, left_digit_abs_bounds, right_digit_abs_bounds, accumulator_bits=32)`
|
|
333
|
+
- `SignedCapacityPlan`
|
|
334
|
+
- `DigitPlaneGemmCapacityPlan`
|
|
335
|
+
- `GroupedCoefficientCapacityPlan`
|
|
336
|
+
|
|
288
337
|
### Scalar-broadcast encoded operations
|
|
289
338
|
|
|
290
339
|
- `mul_u64(...)`
|
|
@@ -359,7 +408,7 @@ Workload: `affine_repeat_u64_io(x, 1_000_003, 7, iterations=1000)`
|
|
|
359
408
|
- OpenMP with one thread: **82.80 billion ops/sec**
|
|
360
409
|
- OpenMP with two threads: **94.86 billion ops/sec**
|
|
361
410
|
|
|
362
|
-
These measurements cover the existing affine kernels, not
|
|
411
|
+
These measurements cover the existing affine kernels, not weighted INT32 reconstruction or the pure-Python capacity planners.
|
|
363
412
|
|
|
364
413
|
---
|
|
365
414
|
|
|
@@ -404,14 +453,15 @@ rns.HAS_AVX2
|
|
|
404
453
|
|
|
405
454
|
## Current release
|
|
406
455
|
|
|
407
|
-
### v0.
|
|
456
|
+
### v0.8.0
|
|
408
457
|
|
|
458
|
+
- exact local grouped-coefficient accumulator planning
|
|
459
|
+
- exact global digit-plane GEMM RNS-capacity planning
|
|
460
|
+
- strict separation between local INT32 safety and global signed uniqueness
|
|
461
|
+
- candidate CRT-modulus validation with pairwise-coprime enforcement
|
|
462
|
+
- exact modulus shortfall and additional-bit receipts
|
|
409
463
|
- fused native weighted signed INT32 partial accumulation
|
|
410
|
-
-
|
|
411
|
-
- staged reference body retained for exact fused-versus-staged A/B benchmarking
|
|
412
|
-
- exact arbitrary-precision positional weights
|
|
413
|
-
- per-term magnitude receipts and strict no-wrap certification
|
|
414
|
-
- canonical modular and uniqueness-gated signed decode paths
|
|
464
|
+
- arbitrary-precision positional weights and no-wrap certificates
|
|
415
465
|
- canonical centered signed encoding and decoding
|
|
416
466
|
- exact dot-product and GEMM output-bound receipts
|
|
417
467
|
- `SignedSession` high-level API
|
|
@@ -5,6 +5,8 @@ setup.py
|
|
|
5
5
|
src/rns_engine/__init__.py
|
|
6
6
|
src/rns_engine/_core.cpp
|
|
7
7
|
src/rns_engine/_weighted.cpp
|
|
8
|
+
src/rns_engine/capacity.py
|
|
9
|
+
src/rns_engine/coefficients.py
|
|
8
10
|
src/rns_engine/engine.py
|
|
9
11
|
src/rns_engine/signed.py
|
|
10
12
|
src/rns_engine/weighted.py
|
|
@@ -14,6 +16,8 @@ src/rns_engine.egg-info/dependency_links.txt
|
|
|
14
16
|
src/rns_engine.egg-info/not-zip-safe
|
|
15
17
|
src/rns_engine.egg-info/requires.txt
|
|
16
18
|
src/rns_engine.egg-info/top_level.txt
|
|
19
|
+
tests/test_capacity.py
|
|
20
|
+
tests/test_coefficients.py
|
|
17
21
|
tests/test_packaging.py
|
|
18
22
|
tests/test_rns.py
|
|
19
23
|
tests/test_session.py
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
import rns_engine as rns
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_single_plane_int8_gemm_fits_current_four_rail_signed_range():
|
|
9
|
+
plan = rns.plan_digit_plane_gemm_capacity(
|
|
10
|
+
inner_dimension=5120,
|
|
11
|
+
radix=128,
|
|
12
|
+
left_digit_abs_bounds=[127],
|
|
13
|
+
right_digit_abs_bounds=[127],
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
assert plan.plane_pair_count == 1
|
|
17
|
+
assert plan.grouped_coefficient_count == 1
|
|
18
|
+
assert plan.left_value_abs_bound == 127
|
|
19
|
+
assert plan.right_value_abs_bound == 127
|
|
20
|
+
assert plan.max_abs_bound == 82_580_480
|
|
21
|
+
assert plan.current_unique is True
|
|
22
|
+
assert plan.unique is True
|
|
23
|
+
assert plan.capacity.additional_bits_required == 0
|
|
24
|
+
assert plan.require_unique() is plan
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_radix128_plane_count_crosses_current_capacity_at_three_planes():
|
|
28
|
+
two_planes = rns.plan_digit_plane_gemm_capacity(
|
|
29
|
+
5120,
|
|
30
|
+
128,
|
|
31
|
+
[127, 127],
|
|
32
|
+
[127, 127],
|
|
33
|
+
)
|
|
34
|
+
three_planes = rns.plan_digit_plane_gemm_capacity(
|
|
35
|
+
5120,
|
|
36
|
+
128,
|
|
37
|
+
[127, 127, 127],
|
|
38
|
+
[127, 127, 127],
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
assert two_planes.left_value_abs_bound == 128**2 - 1
|
|
42
|
+
assert two_planes.current_unique is True
|
|
43
|
+
assert three_planes.left_value_abs_bound == 128**3 - 1
|
|
44
|
+
assert three_planes.current_unique is False
|
|
45
|
+
with pytest.raises(OverflowError, match="modular-only"):
|
|
46
|
+
three_planes.require_unique()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_eight_full_radix128_planes_need_126_bit_modulus():
|
|
50
|
+
plan = rns.plan_digit_plane_gemm_capacity(
|
|
51
|
+
inner_dimension=5120,
|
|
52
|
+
radix=128,
|
|
53
|
+
left_digit_abs_bounds=[127] * 8,
|
|
54
|
+
right_digit_abs_bounds=[127] * 8,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
full_value_bound = 128**8 - 1
|
|
58
|
+
expected_output_bound = 5120 * full_value_bound**2
|
|
59
|
+
|
|
60
|
+
assert plan.left_value_abs_bound == full_value_bound
|
|
61
|
+
assert plan.right_value_abs_bound == full_value_bound
|
|
62
|
+
assert plan.plane_pair_count == 64
|
|
63
|
+
assert plan.grouped_coefficient_count == 15
|
|
64
|
+
assert plan.max_abs_bound == expected_output_bound
|
|
65
|
+
assert plan.capacity.minimum_required_modulus == 2 * expected_output_bound + 1
|
|
66
|
+
assert plan.capacity.required_modulus_bits == 126
|
|
67
|
+
assert plan.capacity.additional_bits_required == 71
|
|
68
|
+
assert plan.current_unique is False
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_plane_pair_ledger_matches_factored_value_bound_exactly():
|
|
72
|
+
plan = rns.plan_digit_plane_gemm_capacity(
|
|
73
|
+
inner_dimension=37,
|
|
74
|
+
radix=16,
|
|
75
|
+
left_digit_abs_bounds=[7, 3, 1],
|
|
76
|
+
right_digit_abs_bounds=[5, 2],
|
|
77
|
+
addend_abs_bound=11,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
expanded = rns.plan_weighted_sum_capacity(
|
|
81
|
+
plan.plane_pair_weights,
|
|
82
|
+
plan.plane_pair_abs_bounds,
|
|
83
|
+
addend_abs_bound=11,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
assert expanded.max_abs_bound == plan.max_abs_bound
|
|
87
|
+
assert plan.plane_pair_weights == (1, 16, 16, 256, 256, 4096)
|
|
88
|
+
assert plan.plane_pair_abs_bounds == (
|
|
89
|
+
37 * 7 * 5,
|
|
90
|
+
37 * 7 * 2,
|
|
91
|
+
37 * 3 * 5,
|
|
92
|
+
37 * 3 * 2,
|
|
93
|
+
37 * 1 * 5,
|
|
94
|
+
37 * 1 * 2,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_candidate_extra_rail_is_checked_by_exact_coprime_product():
|
|
99
|
+
baseline = rns.plan_digit_plane_gemm_capacity(
|
|
100
|
+
5120,
|
|
101
|
+
128,
|
|
102
|
+
[127] * 8,
|
|
103
|
+
[127] * 8,
|
|
104
|
+
)
|
|
105
|
+
extra_modulus = baseline.capacity.minimum_single_coprime_modulus
|
|
106
|
+
|
|
107
|
+
expanded = rns.plan_digit_plane_gemm_capacity(
|
|
108
|
+
5120,
|
|
109
|
+
128,
|
|
110
|
+
[127] * 8,
|
|
111
|
+
[127] * 8,
|
|
112
|
+
additional_moduli=[extra_modulus],
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
assert math.gcd(extra_modulus, rns.M) == 1
|
|
116
|
+
assert expanded.capacity.available_modulus >= expanded.capacity.minimum_required_modulus
|
|
117
|
+
assert expanded.unique is True
|
|
118
|
+
assert expanded.require_unique() is expanded
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_capacity_planner_rejects_non_coprime_or_invalid_candidate_rails():
|
|
122
|
+
with pytest.raises(ValueError, match="coprime"):
|
|
123
|
+
rns.plan_signed_capacity(1, additional_moduli=[127])
|
|
124
|
+
|
|
125
|
+
with pytest.raises(ValueError, match="coprime"):
|
|
126
|
+
rns.plan_signed_capacity(1, additional_moduli=[3, 3])
|
|
127
|
+
|
|
128
|
+
with pytest.raises(ValueError, match="> 1"):
|
|
129
|
+
rns.plan_signed_capacity(1, additional_moduli=[1])
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_capacity_planner_uses_exact_integer_inputs_only():
|
|
133
|
+
with pytest.raises(TypeError, match="max_abs_bound"):
|
|
134
|
+
rns.plan_signed_capacity(1.5)
|
|
135
|
+
|
|
136
|
+
with pytest.raises(ValueError, match="max_abs_bound"):
|
|
137
|
+
rns.plan_signed_capacity(-1)
|
|
138
|
+
|
|
139
|
+
with pytest.raises(ValueError, match="radix"):
|
|
140
|
+
rns.plan_digit_plane_gemm_capacity(1, 1, [1], [1])
|
|
141
|
+
|
|
142
|
+
with pytest.raises(ValueError, match=r"left_digit_abs_bounds\[0\]"):
|
|
143
|
+
rns.plan_digit_plane_gemm_capacity(1, 2, [-1], [1])
|
|
144
|
+
|
|
145
|
+
with pytest.raises(ValueError, match="same length"):
|
|
146
|
+
rns.plan_weighted_sum_capacity([1, 2], [3])
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_empty_digit_plane_body_reduces_to_addend_capacity():
|
|
150
|
+
plan = rns.plan_digit_plane_gemm_capacity(
|
|
151
|
+
inner_dimension=5120,
|
|
152
|
+
radix=128,
|
|
153
|
+
left_digit_abs_bounds=[],
|
|
154
|
+
right_digit_abs_bounds=[127],
|
|
155
|
+
addend_abs_bound=9,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
assert plan.plane_pair_count == 0
|
|
159
|
+
assert plan.grouped_coefficient_count == 0
|
|
160
|
+
assert plan.max_abs_bound == 9
|
|
161
|
+
assert plan.current_unique is True
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
import rns_engine as rns
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_single_int8_plane_has_expected_int32_bound():
|
|
7
|
+
plan = rns.plan_grouped_coefficient_capacity(
|
|
8
|
+
inner_dimension=5120,
|
|
9
|
+
left_digit_abs_bounds=[127],
|
|
10
|
+
right_digit_abs_bounds=[127],
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
assert plan.coefficient_count == 1
|
|
14
|
+
assert plan.plane_pair_count == 1
|
|
15
|
+
assert plan.coefficient_pair_counts == (1,)
|
|
16
|
+
assert plan.coefficient_abs_bounds == (82_580_480,)
|
|
17
|
+
assert plan.max_abs_bound == 82_580_480
|
|
18
|
+
assert plan.safe is True
|
|
19
|
+
assert plan.minimum_signed_accumulator_bits == 28
|
|
20
|
+
assert plan.require_safe() is plan
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_eight_plane_grouped_coefficients_fit_int32_while_global_result_does_not():
|
|
24
|
+
local = rns.plan_grouped_coefficient_capacity(
|
|
25
|
+
inner_dimension=5120,
|
|
26
|
+
left_digit_abs_bounds=[127] * 8,
|
|
27
|
+
right_digit_abs_bounds=[127] * 8,
|
|
28
|
+
accumulator_bits=32,
|
|
29
|
+
)
|
|
30
|
+
global_plan = rns.plan_digit_plane_gemm_capacity(
|
|
31
|
+
inner_dimension=5120,
|
|
32
|
+
radix=128,
|
|
33
|
+
left_digit_abs_bounds=[127] * 8,
|
|
34
|
+
right_digit_abs_bounds=[127] * 8,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
expected_pair_counts = (1, 2, 3, 4, 5, 6, 7, 8, 7, 6, 5, 4, 3, 2, 1)
|
|
38
|
+
one_pair_bound = 82_580_480
|
|
39
|
+
|
|
40
|
+
assert local.coefficient_pair_counts == expected_pair_counts
|
|
41
|
+
assert local.coefficient_abs_bounds == tuple(
|
|
42
|
+
count * one_pair_bound for count in expected_pair_counts
|
|
43
|
+
)
|
|
44
|
+
assert local.max_abs_bound == 660_643_840
|
|
45
|
+
assert local.minimum_signed_accumulator_bits == 31
|
|
46
|
+
assert local.headroom == 1_486_839_807
|
|
47
|
+
assert local.safe is True
|
|
48
|
+
assert global_plan.current_unique is False
|
|
49
|
+
assert global_plan.capacity.required_modulus_bits == 126
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_grouped_coefficient_plan_detects_int32_overflow():
|
|
53
|
+
plan = rns.plan_grouped_coefficient_capacity(
|
|
54
|
+
inner_dimension=20_000,
|
|
55
|
+
left_digit_abs_bounds=[127] * 8,
|
|
56
|
+
right_digit_abs_bounds=[127] * 8,
|
|
57
|
+
accumulator_bits=32,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
assert plan.max_abs_bound == 20_000 * 8 * 127 * 127
|
|
61
|
+
assert plan.safe is False
|
|
62
|
+
assert plan.headroom < 0
|
|
63
|
+
assert plan.minimum_signed_accumulator_bits == 33
|
|
64
|
+
with pytest.raises(OverflowError, match="exceeds"):
|
|
65
|
+
plan.require_safe()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_grouped_bounds_match_manual_nonuniform_convolution():
|
|
69
|
+
plan = rns.plan_grouped_coefficient_capacity(
|
|
70
|
+
inner_dimension=11,
|
|
71
|
+
left_digit_abs_bounds=[7, 3, 1],
|
|
72
|
+
right_digit_abs_bounds=[5, 2],
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
assert plan.coefficient_pair_counts == (1, 2, 2, 1)
|
|
76
|
+
assert plan.coefficient_abs_bounds == (
|
|
77
|
+
11 * (7 * 5),
|
|
78
|
+
11 * (7 * 2 + 3 * 5),
|
|
79
|
+
11 * (3 * 2 + 1 * 5),
|
|
80
|
+
11 * (1 * 2),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_empty_plane_body_has_zero_local_bound():
|
|
85
|
+
plan = rns.plan_grouped_coefficient_capacity(
|
|
86
|
+
inner_dimension=5120,
|
|
87
|
+
left_digit_abs_bounds=[],
|
|
88
|
+
right_digit_abs_bounds=[127],
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
assert plan.coefficient_count == 0
|
|
92
|
+
assert plan.coefficient_pair_counts == ()
|
|
93
|
+
assert plan.coefficient_abs_bounds == ()
|
|
94
|
+
assert plan.max_abs_bound == 0
|
|
95
|
+
assert plan.minimum_signed_accumulator_bits == 1
|
|
96
|
+
assert plan.safe is True
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_grouped_coefficient_inputs_are_strict_integers():
|
|
100
|
+
with pytest.raises(TypeError, match="inner_dimension"):
|
|
101
|
+
rns.plan_grouped_coefficient_capacity(1.5, [1], [1])
|
|
102
|
+
|
|
103
|
+
with pytest.raises(ValueError, match="inner_dimension"):
|
|
104
|
+
rns.plan_grouped_coefficient_capacity(-1, [1], [1])
|
|
105
|
+
|
|
106
|
+
with pytest.raises(ValueError, match=r"left_digit_abs_bounds\[0\]"):
|
|
107
|
+
rns.plan_grouped_coefficient_capacity(1, [-1], [1])
|
|
108
|
+
|
|
109
|
+
with pytest.raises(ValueError, match="accumulator_bits"):
|
|
110
|
+
rns.plan_grouped_coefficient_capacity(1, [1], [1], accumulator_bits=0)
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|