-
Notifications
You must be signed in to change notification settings - Fork 197
Expand file tree
/
Copy pathformat_parameters.py
More file actions
416 lines (362 loc) · 15.5 KB
/
Copy pathformat_parameters.py
File metadata and controls
416 lines (362 loc) · 15.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
#
# Checks the binary_format<T> constants in include/fast_float/float_common.h
# with exact integer arithmetic, for double, float, std::float16_t and
# std::bfloat16_t. Every string is parsed as w * 10^q with w < 10^19, so each
# constant has a condition on (w, q) that it must satisfy.
#
# References:
# Daniel Lemire, Number Parsing at a Gigabyte per Second,
# Software: Practice and Experience 51 (8), 2021 https://arxiv.org/abs/2101.11408
# Noble Mushtak and Daniel Lemire, Fast Number Parsing Without Fallback,
# Software: Practice and Experience 53 (6), 2023 https://arxiv.org/abs/2212.06644
#
# Usage: python3 script/format_parameters.py [--types float16,bfloat16]
# [--sweep-width 3] [--jobs N]
#
import argparse
import multiprocessing
import os
import re
import sys
HEADER = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..",
"include", "fast_float", "float_common.h")
MASK64 = (1 << 64) - 1
MAX_W = 10**19
CXX_TYPES = {"double": "double", "float": "float",
"float16": "std::float16_t", "bfloat16": "std::bfloat16_t"}
class Format:
def __init__(self, name):
self.name = name
cxx = CXX_TYPES[name]
with open(HEADER) as f:
text = f.read()
pattern = r"binary_format<" + re.escape(cxx) + r">::(\w+)\(\)\s*\{(.*?)\n\}"
for fn, body in re.findall(pattern, text, re.S):
returns = re.findall(r"return\s+([^;]+);", body)
# the last return is the FLT_EVAL_METHOD 0/1 branch
if returns and re.fullmatch(r"-?(0x[0-9A-Fa-f]+|\d+)", returns[-1].strip()):
setattr(self, fn, int(returns[-1], 0))
tables = re.search(r"binary_format_lookup_tables<" + re.escape(cxx) +
r", U>\s*\{(.*?)\n\};", text, re.S).group(1)
powers = re.search(r"powers_of_ten\[\]\s*=\s*\{(.*?)\}", tables, re.S).group(1)
self.powers_of_ten = [float(x) for x in
re.sub(r"(bf16|f16|f)\b", "", powers).split(",") if x.strip()]
mant = re.search(r"max_mantissa\[\]\s*=\s*\{(.*?)\}", tables, re.S).group(1)
mant = re.sub(r"//[^\n]*", "", mant).replace("/", "//").replace("\n", " ")
self.max_mantissa = [eval(x.strip(), {"constant_55555": 5**5})
for x in mant.split(",") if x.strip()]
self.p = self.mantissa_explicit_bits
self.bias = self.p - self.minimum_exponent
self.denorm_min_exp = 1 - self.bias # smallest subnormal is 2^denorm_min_exp
self.emin = self.minimum_exponent + 1 # leading bit of the smallest normal
self.emax = self.infinite_power - 1 + self.minimum_exponent
def grid(self):
"""Every finite value and every midpoint as (num, exp2, kind) with
kind in value, tie_down (even lower neighbour), tie_up, overflow."""
out = []
two_p = 1 << self.p
d = self.denorm_min_exp
for m in range(1, two_p):
out.append((m, d, "value"))
for m in range(two_p):
out.append((2 * m + 1, d - 1, "tie_down" if m % 2 == 0 else "tie_up"))
for e in range(self.emin, self.emax + 1):
for f in range(two_p):
out.append((two_p + f, e - self.p, "value"))
kind = "tie_down" if f % 2 == 0 else "tie_up"
if e == self.emax and f == two_p - 1:
kind = "overflow"
out.append((2 * (two_p + f) + 1, e - self.p - 1, kind))
return out
def ratio(num, exp2):
return (num << exp2, 1) if exp2 >= 0 else (num, 1 << -exp2)
def ratio10(w, q):
return (w * 10**q, 1) if q >= 0 else (w, 10**-q)
def le(n1, d1, n2, d2):
return n1 * d2 <= n2 * d1
def floor_log2(n, d):
e = n.bit_length() - d.bit_length()
if (d << e if e >= 0 else d) > (n if e >= 0 else n << -e):
e -= 1
return e
def round_ties_even(n, d):
m, r = divmod(n, d)
if 2 * r > d or (2 * r == d and m & 1):
m += 1
return m
def reference_bits(fmt, n, d):
"""Correctly rounded bit pattern of n/d >= 0."""
if n == 0:
return 0
e = floor_log2(n, d)
s = fmt.denorm_min_exp if e < fmt.emin else e - fmt.p
m = round_ties_even(n, d << s) if s >= 0 else round_ties_even(n << -s, d)
if e < fmt.emin:
return m
if m == 2 << fmt.p:
m >>= 1
e += 1
if e > fmt.emax:
return fmt.infinite_power << fmt.p
return ((e - fmt.minimum_exponent) << fmt.p) | (m - (1 << fmt.p))
def ceil_log2(x):
z = 0
while (1 << z) < x:
z += 1
return z
def table_entry(q):
"""T[q] as stored in fast_table.h (see table_generation.py)."""
if q < 0:
power5 = 5**-q
z = ceil_log2(power5)
b = z + 127 if q >= -27 else 2 * z + 128
c = 2**b // power5 + 1
while c >= 1 << 128:
c //= 2
return c
power5 = 5**q
while power5 < 1 << 127:
power5 *= 2
while power5 >= 1 << 128:
power5 //= 2
return power5
TABLE = {q: table_entry(q) for q in range(-342, 309)}
def compute_product_approximation(q, w, bit_precision):
t = TABLE[q]
first = w * (t >> 64)
high, low = first >> 64, first & MASK64
if (high & (MASK64 >> bit_precision)) == MASK64 >> bit_precision:
second_high = (w * (t & MASK64)) >> 64
low = (low + second_high) & MASK64
if second_high > low:
high += 1
return high, low
def compute_float(fmt, q, w, subnormal_tie_test=True):
"""Model of compute_float in decimal_to_binary.h. Returns
(mantissa, power2, truncated mantissa, k) with truncated == floor(w 10^q 2^k)."""
if w == 0 or q < fmt.smallest_power_of_ten:
return 0, 0, None, None
if q > fmt.largest_power_of_ten:
return 0, fmt.infinite_power, None, None
lz = 64 - w.bit_length()
w <<= lz
high, low = compute_product_approximation(q, w, fmt.p + 3)
upperbit = high >> 63
shift = upperbit + 64 - fmt.p - 3
mantissa = high >> shift
power2 = ((152170 + 65536) * q >> 16) + 63 + upperbit - lz - fmt.minimum_exponent
tie_q = fmt.min_exponent_round_to_even <= q <= fmt.max_exponent_round_to_even
if power2 <= 0:
if -power2 + 1 >= 64:
return 0, 0, None, None
extra = -power2 + 1
mantissa >>= extra
truncated, k = mantissa, fmt.bias
if (subnormal_tie_test and low <= 1 and tie_q and (mantissa & 3) == 1
and ((mantissa << extra) << shift) == high):
mantissa &= ~1
mantissa += mantissa & 1
mantissa >>= 1
return mantissa, int(mantissa >= 1 << fmt.p), truncated, k
truncated, k = mantissa, 1 + fmt.bias - power2
if low <= 1 and tie_q and (mantissa & 3) == 1 and (mantissa << shift) == high:
mantissa &= ~1
mantissa += mantissa & 1
mantissa >>= 1
if mantissa >= 2 << fmt.p:
mantissa = 1 << fmt.p
power2 += 1
mantissa &= ~(1 << fmt.p)
if power2 >= fmt.infinite_power:
return 0, fmt.infinite_power, truncated, k
return mantissa, power2, truncated, k
def parsed_bits(fmt, w, q, subnormal_tie_test=True):
"""from_chars on the digits w * 10^q. Clinger's fast path is one correctly
rounded operation on exact operands, so it equals the reference."""
if fmt.min_exponent_fast_path <= q <= fmt.max_exponent_fast_path and w <= 2 << fmt.p:
return reference_bits(fmt, *ratio10(w, q)), None, None
mantissa, power2, truncated, k = compute_float(fmt, q, w, subnormal_tie_test)
return mantissa | (power2 << fmt.p), truncated, k
failures = 0
def check(ok, text):
global failures
print((" ok " if ok else " FAIL ") + text)
failures += not ok
def note(text):
print(" note " + text)
def continued_fraction(numer, denom):
cf = []
while denom:
quot, rem = divmod(numer, denom)
cf.append(quot)
numer, denom = denom, rem
return cf
def convergents(cf):
p2, q2, p1, q1 = 0, 1, 1, 0
for a in cf:
p1, p2, q1, q2 = a * p1 + p2, p1, a * q1 + q2, q1
yield p1, q1
def check_constants(fmt):
half_denorm = ratio(1, fmt.denorm_min_exp - 1)
q = fmt.smallest_power_of_ten
check(le(*ratio10(MAX_W - 1, q - 1), *half_denorm),
"smallest_power_of_ten=%d: q < %d rounds to zero" % (q, q))
check(not le(*ratio10(MAX_W - 1, q), *half_denorm),
"smallest_power_of_ten is tight")
threshold = ratio((4 << fmt.p) - 1, fmt.emax - fmt.p - 1) # max + ulp/2
q = fmt.largest_power_of_ten
check(le(*threshold, *ratio10(1, q + 1)),
"largest_power_of_ten=%d: q > %d overflows" % (q, q))
check(not le(*threshold, *ratio10(1, q)), "largest_power_of_ten is tight")
two_p1 = 2 << fmt.p
q = fmt.max_exponent_fast_path
check(5**q <= two_p1 < 5 ** (q + 1),
"max_exponent_fast_path=%d: 10^q exact in T iff q <= %d" % (q, q))
check(fmt.powers_of_ten[: q + 1] == [10.0**i for i in range(q + 1)], "powers_of_ten")
check(fmt.max_mantissa[: q + 1] == [two_p1 // 5**i for i in range(q + 1)],
"max_mantissa[q] == floor(2^%d / 5^q)" % (fmt.p + 1))
overflow = not le(two_p1 * 10**q, 1, *ratio(two_p1 - 1, fmt.emax - fmt.p))
note("fast path product 2^%d * 10^%d %s the largest finite value"
% (fmt.p + 1, q, "exceeds" if overflow else "is below"))
k = -fmt.min_exponent_fast_path
check(0 <= k <= fmt.max_exponent_fast_path, "min_exponent_fast_path=%d" % -k)
if fmt.p <= 10:
# double rounding: w / 10^k rounded to `wide` bits, then to T
for wide in (24, 53, 64):
bad = 0
for kk in range(1, fmt.max_exponent_fast_path + 1):
for w in range(1, two_p1 + 1):
e = floor_log2(w, 10**kk)
s = e - wide + 1
m = round_ties_even(w, 10**kk << s) if s >= 0 else round_ties_even(w << -s, 10**kk)
if reference_bits(fmt, *ratio(m, s)) != reference_bits(fmt, w, 10**kk):
bad += 1
check(bad == 0, "w / 10^k, k <= %d, via %d-bit intermediate never double-rounds"
% (fmt.max_exponent_fast_path, wide))
def check_ties(fmt):
"""A tie is m * 2^e with m odd. It is w * 10^q, w < 10^19, iff q <= e and
w = m 5^-q 2^(e-q) (q <= 0) or 5^q | m (q > 0). Ties whose lower neighbour
is even (m == 1 mod 4) must fall in the round-to-even range."""
p = fmt.p
lo, hi = fmt.min_exponent_round_to_even, fmt.max_exponent_round_to_even
q = 0
while ((1 << (p + 1)) + 1) * 5 ** (q + 1) < MAX_W:
q += 1
normal_lo = -q
q = 0
while 5 ** (q + 1) < 1 << (p + 2):
q += 1
while True:
c = 1
while 5**q * c <= 1 << (p + 1):
c += 4
if 5**q * c < 1 << (p + 2) and q <= fmt.emax - p - 1:
break
q -= 1
normal_hi = q
e = fmt.denorm_min_exp - 1
sub = []
q = e
while 5**-q << (e - q) < MAX_W:
sub.append(q)
q -= 1
check(lo <= normal_lo and hi >= normal_hi,
"round_to_even range [%d, %d] covers normal ties [%d, %d]" % (lo, hi, normal_lo, normal_hi))
if sub:
check(lo <= min(sub), "and the subnormal ties at q in [%d, %d]" % (min(sub), max(sub)))
else:
note("no subnormal tie has fewer than 20 digits")
# subnormal_ties_possible() in float_common.h: an upper bound on the
# largest q that can give a subnormal, compared with the tie range
q_sub = ((fmt.minimum_exponent + 1) * 1233) >> 12
check(10**q_sub < 2**fmt.emin < 10 ** (q_sub + 2), "q <= %d bounds the subnormal exponents" % q_sub)
check((lo <= q_sub) == bool(sub), "subnormal_ties_possible() == %s" % bool(sub))
longest = len(str(((1 << (p + 2)) - 1) * 5**fmt.bias))
check(fmt.max_digits >= longest, "max_digits=%d >= %d, the longest tie" % (fmt.max_digits, longest))
def check_table(fmt):
"""Facts behind the tie test (product.low <= 1 and dropped bits zero)."""
q_lo, q_hi = max(fmt.smallest_power_of_ten, -27), -1
check(fmt.min_exponent_round_to_even >= -27, "tie test stays where 5^-q < 2^64")
check(all(TABLE[q] & MASK64 for q in range(q_lo, q_hi + 1)),
"T[q] low word nonzero for %d <= q <= %d" % (q_lo, q_hi))
fake = False
for q in range(q_lo, q_hi + 1):
t_hi = TABLE[q] >> 64
v2 = (t_hi & -t_hi).bit_length() - 1
z = ceil_log2(5**-q)
for shift in (61 - fmt.p, 62 - fmt.p):
# skipped second product: w * t_hi == 0 or 1 mod 2^(shift+64)
if shift + 64 - v2 <= 63:
fake = True
if v2 == 0 and (1 << 63) <= pow(t_hi, -1, 1 << (shift + 64)) < 1 << 64:
fake = True
# computed second product: a non-tie within 2^-62 of a rational
# with denominator 2^(z-1-shift) is impossible
if z - 1 - shift > 62:
fake = True
check(not fake, "no false tie for %d <= q <= %d" % (q_lo, q_hi))
bad = False
for q in range(fmt.smallest_power_of_ten, fmt.largest_power_of_ten + 1):
for _, w in convergents(continued_fraction(TABLE[q], 2**137)):
if w >= 2**64:
break
if (TABLE[q] * w) % 2**137 > 2**137 - 2**64:
bad = True
check(not bad, "Mushtak-Lemire condition for %d <= q <= %d"
% (fmt.smallest_power_of_ten, fmt.largest_power_of_ten))
def sweep_q(args):
fmt, grid, q, width = args
cands = set()
for num, exp2, _ in grid:
n, d = ratio(num, exp2)
if q >= 0:
d *= 10**q
else:
n *= 10**-q
cands.update(w for w in range(n // d - width, n // d + width + 2) if 1 <= w < MAX_W)
wrong, wrong_without, bad_trunc = [], 0, 0
for w in cands:
n, d = ratio10(w, q)
want = reference_bits(fmt, n, d)
got, truncated, k = parsed_bits(fmt, w, q)
if got != want:
wrong.append((w, q, got, want))
if truncated is not None:
exact = (n << k) // d if k >= 0 else n // (d << -k)
bad_trunc += truncated != exact
if parsed_bits(fmt, w, q, False)[0] != want:
wrong_without += 1
return len(cands), wrong, wrong_without, bad_trunc
def sweep(fmt, width, jobs):
"""Compare the model with the reference on every w within `width` of
G / 10^q for every value, tie and the overflow threshold G."""
grid = fmt.grid()
qs = range(fmt.smallest_power_of_ten - 1, fmt.largest_power_of_ten + 2)
tasks = [(fmt, grid, q, width) for q in qs]
with multiprocessing.Pool(jobs) as pool:
results = pool.map(sweep_q, tasks)
wrong = [x for r in results for x in r[1]]
note("swept %d pairs (w, q)" % sum(r[0] for r in results))
check(sum(r[3] for r in results) == 0, "truncated mantissa is exact")
check(not wrong, "compute_float matches the reference")
for w, q, got, want in sorted(wrong)[:20]:
print(" w=%d q=%d got 0x%04x want 0x%04x" % (w, q, got, want))
note("without the subnormal tie test: %d wrong" % sum(r[2] for r in results))
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--types", default="double,float,float16,bfloat16")
ap.add_argument("--sweep-width", type=int, default=3)
ap.add_argument("--jobs", type=int, default=os.cpu_count() or 1)
args = ap.parse_args()
for name in args.types.split(","):
fmt = Format(name.strip())
print("== %s" % fmt.name)
check_constants(fmt)
check_table(fmt)
check_ties(fmt)
if fmt.p <= 10 and args.sweep_width > 0:
sweep(fmt, args.sweep_width, args.jobs)
print("%d failure(s)" % failures)
return 1 if failures else 0
if __name__ == "__main__":
sys.exit(main())