Skip to content

Taylor series, remainder bounds, range reduction

ModuleM02.1 · build · Python · Pass 2 · 3 to 4 h
You buildpython/tinyllm/num/taylor.py: exp_taylor_coeffs, exp_range_reduced (exp as 2k2^k times a polynomial), erf_series (erf from a series with no cancellation)
Contractcourse/contracts/py/tinyllm/num/taylor.pyi
Testscourse/tests/M02.1/test_taylor.py (what they check: section 4)
NeedsM00.4 horner, which evaluates the polynomial (or --ref-deps). Reading: M01.1 (derivatives and their error terms)
Used byM01.3 computes the exact GELU with erf_series · later M09.6 ports exp_range_reduced to C as tl_expf for softmax and SiLU kernels, and M08.1’s dual_erf uses the same series
MilestoneMS-P2 (the Pass 2 gate)
Optional depthOpenStax, Calculus Volume 2 (free), sections 6.3 and 6.4 (Taylor and Maclaurin series, the remainder); Muller, Elementary Functions: Algorithms and Implementation, ch. 11 (range reduction, Cody and Waite); Abramowitz and Stegun 7.1.6 (the erf series used here)
  • The Taylor polynomial of degree nn matches ff and its first nn derivatives at a point; for exe^x at 0 its coefficients are 1/i!1/i! (test_coefficients_are_inverse_factorials).
  • The Lagrange remainder bounds the error exactly: ∣er−pn(r)∣≤emax⁡(r,0)∣r∣n+1/(n+1)!|e^r - p_n(r)| \le e^{\max(r, 0)} |r|^{n+1}/(n+1)!, tiny when ∣r∣|r| is small and useless when it is not (test_lagrange_bound_holds).
  • Range reduction makes ∣r∣|r| small for every input: ex=2kere^x = 2^k e^r with k=round⁡(x/ln⁡2)k = \operatorname{round}(x/\ln 2) and ∣r∣≤ln⁡22|r| \le \frac{\ln 2}{2}; degree 6 is then float32-accurate (test_hand_example, test_degree_six_is_float32_accurate).
  • Subtracting kln⁡2k \ln 2 in two parts (Cody and Waite) keeps rr exact to the last bit even for k=1000k = 1000 (test_full_precision_across_the_range).
  • A series can be correct and still useless in floating point: the textbook alternating series for erf cancels catastrophically at x=5x = 5; the series with all terms of one sign does not (test_erf_no_cancellation_at_x_5).
Terminal window
ol start M02.1 # stubs python/tinyllm/num/taylor.py into your repo
ol tests M02.1 # read the test catalog first: rung R0, you write no tests here
ol check M02.1 # exit code is the verdict
ol check M02.1 --ref-deps # only if your M00.4 is not passing yet
ol diff M02.1 # after passing: your code against the reference

A CPU can add and multiply; it cannot compute exe^x. Until now numpy has done it for you, and from Pass 6 on your C kernels must do it themselves: softmax (L9.2) exponentiates every logit, SiLU (L9.6) every activation. The way every math library computes exe^x is a polynomial on a small interval plus an exact rescaling, and the polynomial comes from the Taylor series, with an error bound that tells you which degree is enough. The same tools give erf, which your exact GELU (M01.3) needs next. This module builds both in Python, where you can watch the error bound hold point by point, so that M09.6 has a trusted reference to port.

SymbolMeaningType / shape
f(i)(a)f^{(i)}(a)the ii-th derivative of ff at aa (f(0)=ff^{(0)} = f)float
i!i!1⋅2⋯i1 \cdot 2 \cdots i, with 0!=10! = 1integer
pn(x)p_n(x)the degree-nn Taylor polynomial of ff at aafloat
Rn(x)R_n(x)the remainder f(x)−pn(x)f(x) - p_n(x)float
xxthe input of exp_range_reducedfloat64 array
kkround⁡(x/ln⁡2)\operatorname{round}(x / \ln 2), an integerint
rrthe reduced argument x−kln⁡2x - k\ln 2, ∣r∣≤ln⁡22\lvert r \rvert \le \frac{\ln 2}{2}float64 array
tnt_nthe nn-th term of the erf seriesfloat64 array
NNthe number of erf terms summed (terms)int

Near a point aa, which polynomial of degree nn is the best stand-in for ff? The one whose value and first nn derivatives at aa equal ff‘s. Write p(x)=∑i=0nci(x−a)ip(x) = \sum_{i=0}^{n} c_i (x - a)^i. Differentiating ii times and setting x=ax = a kills every term but one: p(i)(a)=i! cip^{(i)}(a) = i!\, c_i. Matching p(i)(a)=f(i)(a)p^{(i)}(a) = f^{(i)}(a) gives

pn(x)=∑i=0nf(i)(a)i!(x−a)i.p_n(x) = \sum_{i=0}^{n} \frac{f^{(i)}(a)}{i!} (x - a)^i .

For f=exf = e^x at a=0a = 0 every derivative is e0=1e^0 = 1, so pn(x)=1+x+x22+x36+⋯+xnn!p_n(x) = 1 + x + \frac{x^2}{2} + \frac{x^3}{6} + \cdots + \frac{x^n}{n!}, coefficients 1/i!1/i!. exp_taylor_coeffs(n) returns them in ascending order of power, [1/0!,…,1/n!][1/0!, \ldots, 1/n!], the order M00.4’s horner takes (numpy’s polyval wants the reverse). Computing ci=ci−1/ic_i = c_{i-1}/i avoids forming i!i!, which overflows float64 at i=171i = 171.

Taylor’s theorem (Lagrange form): if ff has n+1n + 1 continuous derivatives, then for some ξ\xi between aa and xx,

Rn(x)=f(x)−pn(x)=f(n+1)(ξ)(n+1)!(x−a)n+1.R_n(x) = f(x) - p_n(x) = \frac{f^{(n+1)}(\xi)}{(n+1)!}(x - a)^{n+1} .

You do not know ξ\xi, but you can bound ∣f(n+1)∣|f^{(n+1)}| between aa and xx, and that bounds the error. (M01.1 used the first terms of this formula to find the error of finite differences; this is the proven version.) For ere^r at 0, f(n+1)(ξ)=eξf^{(n+1)}(\xi) = e^\xi with ξ\xi between 0 and rr, so relative to ere^r:

∣er−pn(r)∣er=eξ−r ∣r∣n+1(n+1)!≤e∣r∣−r ∣r∣n+1(n+1)!.\frac{|e^r - p_n(r)|}{e^r} = \frac{e^{\xi - r}\,|r|^{n+1}}{(n+1)!} \le e^{|r| - r}\,\frac{|r|^{n+1}}{(n+1)!} .

(If r>0r > 0 then ξ≤r\xi \le r and eξ−r≤1e^{\xi - r} \le 1; if r<0r < 0 then ξ≤0\xi \le 0 and eξ−r≤e−r=e∣r∣e^{\xi - r} \le e^{-r} = e^{|r|}.) test_lagrange_bound_holds checks this inequality at 4001 points for every degree from 1 to 12. The bound shrinks factorially in nn but grows like ∣r∣n+1|r|^{n+1}: at r=0.35r = 0.35 degree 6 gives 1.2×10−71.2 \times 10^{-7}; at r=10r = 10 degree 6 gives 2000. A Taylor polynomial is only good near its center.

Every real xx can be written x=kln⁡2+rx = k \ln 2 + r with kk an integer and ∣r∣≤ln⁡22≈0.3466|r| \le \frac{\ln 2}{2} \approx 0.3466: take k=round⁡(x/ln⁡2)k = \operatorname{round}(x / \ln 2). Then

ex=ekln⁡2⋅er=2k er,e^x = e^{k\ln 2} \cdot e^r = 2^k\, e^r ,

and multiplying by 2k2^k is exact in binary floating point: it only changes the exponent field (np.ldexp(p, k)). So exe^x costs one reduction, one polynomial on ∣r∣≤0.3466|r| \le 0.3466, and one exponent adjustment, and the remainder bound of section 2.2 holds for every xx. With degree 6 the relative error is at most e2⋅0.3466 0.34667/7!=2.4×10−7e^{2 \cdot 0.3466}\,0.3466^7/7! = 2.4 \times 10^{-7}, about two float32 units in the last place: the target of tl_expf. Rounding kk down (floor) instead of to nearest leaves r∈[0,ln⁡2)r \in [0, \ln 2), doubles the largest ∣r∣|r|, and multiplies the bound by 27=1282^7 = 128.

Subtract kln⁡2k \ln 2 exactly. ln⁡2\ln 2 is irrational, so the float64 constant LN2 is off by up to ε/4\varepsilon/4 relative. Multiplied by k=1000k = 1000 that error lands in rr as 10−1310^{-13}, and ere^r inherits it: 400 times the float64 rounding level. Cody and Waite split the constant: LN2_HI holds the first 32 bits of ln⁡2\ln 2 with the low 21 bits zero, so k⋅k \cdot LN2_HI is exact for ∣k∣<221|k| < 2^{21}, and LN2_LO =ln⁡2−= \ln 2 - LN2_HI is tiny. Then r=(x−k LN2_HI)−k LN2_LOr = (x - k\,\mathrm{LN2\_HI}) - k\,\mathrm{LN2\_LO} is accurate to the last bit.

The edges. exe^x overflows float64 above x≈709.78x \approx 709.78 and is below the smallest subnormal under x≈−745.1x \approx -745.1. Clipping xx to [−750,710][-750, 710] first changes no result and keeps kk small; then +∞↦∞+\infty \mapsto \infty, −∞↦0-\infty \mapsto 0, and nan is passed through separately.

erf⁡(x)=2π∫0xe−t2dt\operatorname{erf}(x) = \frac{2}{\sqrt\pi}\int_0^x e^{-t^2}dt. Integrating the Taylor series of e−t2e^{-t^2} term by term gives the textbook series

erf⁡(x)=2π∑n≥0(−1)nx2n+1n! (2n+1),\operatorname{erf}(x) = \frac{2}{\sqrt\pi} \sum_{n \ge 0} \frac{(-1)^n x^{2n+1}}{n!\,(2n+1)} ,

which converges for every xx but alternates in sign. At x=5x = 5 its largest term is about 6×1086 \times 10^{8}, the sum is below 1, and float64 keeps 16 significant digits of each term: the cancellation leaves about 7 correct digits of the answer. The fix is a different series for the same function. The Taylor series of ex2erf⁡(x)e^{x^2}\operatorname{erf}(x) has only positive coefficients:

erf⁡(x)=2π e−x2∑n≥0tn,tn=2nx2n+11⋅3⋅5⋯(2n+1),tn+1tn=2x22n+3.\operatorname{erf}(x) = \frac{2}{\sqrt\pi}\, e^{-x^2} \sum_{n \ge 0} t_n, \qquad t_n = \frac{2^n x^{2n+1}}{1 \cdot 3 \cdot 5 \cdots (2n+1)}, \qquad \frac{t_{n+1}}{t_n} = \frac{2x^2}{2n+3} .

Every term has the sign of xx, so nothing cancels, and each term comes from the previous one by one multiplication. The tail bound: once the ratio ρ=2x22N+3\rho = \frac{2x^2}{2N+3} is below 1, the ratios keep falling, so the omitted terms are at most a geometric series, ∑n≥Ntn≤tN1−ρ\sum_{n \ge N} t_n \le \frac{t_N}{1 - \rho} (M02.2 sums geometric series). test_erf_tail_bound_holds checks it.

Cutoff. For ∣x∣≥6|x| \ge 6, 1−erf⁡(x)<2.2×10−171 - \operatorname{erf}(x) < 2.2 \times 10^{-17}, below half the gap between 1 and the float before it, so erf rounds to exactly ±1\pm 1. The series would need hundreds of terms there, and near ∣x∣=27|x| = 27 its partial sums overflow while e−x2e^{-x^2} underflows to 0, giving $0 \cdot \infty = $ nan. So erf_series returns sign⁡(x)\operatorname{sign}(x) for ∣x∣≥6|x| \ge 6. With 120 or more terms the result is within 3×10−153 \times 10^{-15} of erf everywhere.

e1e^1 with degree 3. k=round⁡(1/0.693147)=round⁡(1.4427)=1k = \operatorname{round}(1/0.693147) = \operatorname{round}(1.4427) = 1, so r=1−ln⁡2=0.3068528r = 1 - \ln 2 = 0.3068528.

p3(r)=1+r+r22+r36=1+0.3068528+0.0470793+0.0048155=1.3587476,p_3(r) = 1 + r + \frac{r^2}{2} + \frac{r^3}{6} = 1 + 0.3068528 + 0.0470793 + 0.0048155 = 1.3587476 ,

21⋅p3(r)=2.7174952vs.e=2.7182818.2^1 \cdot p_3(r) = 2.7174952 \quad \text{vs.} \quad e = 2.7182818 .

The relative error is 2.9×10−42.9 \times 10^{-4}, and the Lagrange bound (here r>0r > 0, so the factor is 1) is r4/4!=0.0088658/24=3.7×10−4r^4/4! = 0.0088658/24 = 3.7 \times 10^{-4}: the bound holds with little room to spare. Horner’s rule evaluates the same polynomial as 1+r(1+r(12+r⋅16))1 + r(1 + r(\frac12 + r \cdot \frac16)). This is test_hand_example.

erf⁡(0.5)\operatorname{erf}(0.5) with three terms. x2=0.25x^2 = 0.25, ratio 2x2=0.52x^2 = 0.5 over 2n+12n + 1: t0=0.5t_0 = 0.5, t1=0.5⋅0.5/3=0.0833333t_1 = 0.5 \cdot 0.5/3 = 0.0833333, t2=0.0833333⋅0.5/5=0.0083333t_2 = 0.0833333 \cdot 0.5/5 = 0.0083333, sum 0.59166670.5916667. Prefactor 2πe−0.25=1.1283792×0.7788008=0.8787826\frac{2}{\sqrt\pi} e^{-0.25} = 1.1283792 \times 0.7788008 = 0.8787826. Result 0.8787826×0.5916667=0.51994640.8787826 \times 0.5916667 = 0.5199464, against erf⁡(0.5)=0.5204999\operatorname{erf}(0.5) = 0.5204999. The next term t3=0.000595t_3 = 0.000595 times the prefactor is 0.0005230.000523, and the tail bound t3/(1−0.5/9)t_3/(1 - 0.5/9) times the prefactor, 0.0005540.000554, covers the actual gap of 0.0005530.000553. This is test_erf_hand_example.

ERF_CUTOFF: float # 6.0
def exp_taylor_coeffs(n: int) -> NDArray: ... # [1/0!, ..., 1/n!]
def exp_range_reduced(x: ArrayLike, deg: int = 6) -> NDArray: ...
def erf_series(x: ArrayLike, terms: int) -> NDArray: ...

All arithmetic is float64 and results have the shape of x. exp_range_reduced handles ±∞\pm\infty, nan, overflow, and underflow without warnings. Negative n, deg, or terms < 1 raise ValueError.

TestKINDChecksWhy it matters downstream
test_hand_exampleunit, smokesection 3: 2.71749522.7174952, error 2.9×10−42.9 \times 10^{-4} inside the boundyou and the tests agree on reduction and polynomial
test_coefficients_are_inverse_factorialsunit[1,1,12,16,124][1, 1, \frac12, \frac16, \frac1{24}], length n+1n + 1, 1/30!1/30!the coefficient order horner and M09.6 use
test_lagrange_bound_holdspropertythe bound of section 2.2 at 4001 points, degrees 1 to 12the remainder bound is the design’s property test
test_degree_six_is_float32_accurategoldenrelative error at most 2.4×10−72.4 \times 10^{-7} on [−87,88][-87, 88]tl_expf’s accuracy budget
test_full_precision_across_the_rangegoldendegree 20 within 10−1510^{-15} near ±700\pm 700the two-part reduction
test_special_values_and_extremesboundary±∞\pm\infty, nan, overflow, underflow, subnormal, no warningsmasked logits are −∞-\infty
test_shape_and_scalarsunitscalars, matrices, integer input, deg < 0 raisescallers pass every shape
test_erf_hand_exampleunit, smokesection 3: 0.5199464the series and its ratio
test_erf_matches_math_erfgolden160 terms within 3×10−153 \times 10^{-15} of math.erf on [−7,7][-7, 7]gelu_erf must match PyTorch
test_erf_no_cancellation_at_x_5boundaryx=3,4,5,−5.5x = 3, 4, 5, -5.5 to 3×10−153 \times 10^{-15}the alternating series fails here
test_erf_cutoff_and_specialsboundary±1\pm 1 at and beyond 6, ±∞\pm\infty, nan, 0GELU feeds x/2x/\sqrt2 up to 28
test_erf_tail_bound_holdspropertythe geometric tail bound at many NN and xxthe remainder for erf
PitfallSymptomCaught by
1. coefficients in the wrong order for the evaluator (numpy’s polyval is descending, horner ascending)p(r)p(r) evaluates the reversed polynomial; at degree 3, e1≈0.89e^1 \approx 0.89test_hand_example, test_lagrange_bound_holds (mutant s01)
2. rounding kk down instead of to nearestrr up to 0.69, error 8×10−68 \times 10^{-6} at degree 6test_degree_six_is_float32_accurate (mutant s04)
3. one rounded ln⁡2\ln 2 in the reductionrelative error 8×10−148 \times 10^{-14} near x=700x = 700test_full_precision_across_the_range (mutant s05)
4. the alternating erf series7 correct digits at x=5x = 5test_erf_no_cancellation_at_x_5 (mutant s09)
5. no cutoff for large ∣x∣\lvert x \rvertnan at x=28x = 28, wrong values beyond 6test_erf_cutoff_and_specials (mutant s10)
adding the low part of ln⁡2\ln 2 instead of subtracting itrelative error 4×10−104 \times 10^{-10} per unit of kktest_hand_example (mutant s02)
coefficients 1/(i+1)!1/(i+1)!every value wrongtest_coefficients_are_inverse_factorials (mutant s03)
nan passed through the clip as 0enan=1e^{\mathrm{nan}} = 1test_special_values_and_extremes (mutant s06)
an off-by-one in the erf ratio or term countthe hand example’s third term is wrongtest_erf_hand_example (mutants s07, s08)
DirectionModuleHow it uses this
BackM00.4horner(exp_taylor_coeffs(deg), r) evaluates the polynomial
BackM01.1derivatives and the first terms of Taylor’s formula (reading)
ForwardM01.3gelu_erf and dgelu_erf compute Φ(x)\Phi(x) with erf_series(x / sqrt(2), 160)
ForwardM08.1dual_erf differentiates erf in forward mode
ForwardM09.2stable softmax and logsumexp: why exe^x overflows at 709.78
ForwardM09.6tl_expf in C: the same reduction and a degree-6 polynomial in float32, checked against your Python
Your pieceProduction equivalentWhat it addsWhere to look
exp_range_reducedglibc exp and expfa 128-entry table (ej/128e^{j/128}) so rr is even smaller and the polynomial shorter; correctly rounded in most casesglibc sysdeps/ieee754/dbl-64/e_exp.c, sysdeps/ieee754/flt-32/e_expf.c
Taylor coefficientsminimax polynomials (Remez algorithm)the polynomial with the smallest maximum error on the interval, not the best at one point: one or two degrees fewer for the same accuracySollya; M09.6
two-part ln⁡2\ln 2Payne and Hanek reductionexact reduction of sin⁡\sin and cos⁡\cos for arguments like 1030010^{300}, where π\pi needs a thousand bitsMuller, Elementary Functions, ch. 11
erf_seriesSLEEF, CUDA erffbranch-free vectorized approximations for whole SIMD lanesSLEEF src/libm/sleefsimdsp.c