Taylor series, remainder bounds, range reduction
Overview
Section titled “Overview”| Module | M02.1 · build · Python · Pass 2 · 3 to 4 h |
| You build | python/tinyllm/num/taylor.py: exp_taylor_coeffs, exp_range_reduced (exp as times a polynomial), erf_series (erf from a series with no cancellation) |
| Contract | course/contracts/py/tinyllm/num/taylor.pyi |
| Tests | course/tests/M02.1/test_taylor.py (what they check: section 4) |
| Needs | M00.4 horner, which evaluates the polynomial (or --ref-deps). Reading: M01.1 (derivatives and their error terms) |
| Used by | M01.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 |
| Milestone | MS-P2 (the Pass 2 gate) |
| Optional depth | OpenStax, 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) |
Key Takeaways
Section titled “Key Takeaways”- The Taylor polynomial of degree matches and its first derivatives at a point; for at 0 its coefficients are (
test_coefficients_are_inverse_factorials). - The Lagrange remainder bounds the error exactly: , tiny when is small and useless when it is not (
test_lagrange_bound_holds). - Range reduction makes small for every input: with and ; degree 6 is then float32-accurate (
test_hand_example,test_degree_six_is_float32_accurate). - Subtracting in two parts (Cody and Waite) keeps exact to the last bit even for (
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 ; the series with all terms of one sign does not (
test_erf_no_cancellation_at_x_5).
How to work this chapter
Section titled “How to work this chapter”ol start M02.1 # stubs python/tinyllm/num/taylor.py into your repool tests M02.1 # read the test catalog first: rung R0, you write no tests hereol check M02.1 # exit code is the verdictol check M02.1 --ref-deps # only if your M00.4 is not passing yetol diff M02.1 # after passing: your code against the reference1. Why now
Section titled “1. Why now”A CPU can add and multiply; it cannot compute . 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 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.
2. Principles
Section titled “2. Principles”| Symbol | Meaning | Type / shape |
|---|---|---|
| the -th derivative of at () | float | |
| , with | integer | |
| the degree- Taylor polynomial of at | float | |
| the remainder | float | |
the input of exp_range_reduced | float64 array | |
| , an integer | int | |
| the reduced argument , | float64 array | |
| the -th term of the erf series | float64 array | |
the number of erf terms summed (terms) | int |
2.1 Polynomials that match derivatives
Section titled “2.1 Polynomials that match derivatives”Near a point , which polynomial of degree is the best stand-in for ? The one whose value and first derivatives at equal ‘s. Write . Differentiating times and setting kills every term but one: . Matching gives
For at every derivative is , so , coefficients . exp_taylor_coeffs(n) returns them in ascending order of power, , the order M00.4’s horner takes (numpy’s polyval wants the reverse). Computing avoids forming , which overflows float64 at .
2.2 The remainder
Section titled “2.2 The remainder”Taylor’s theorem (Lagrange form): if has continuous derivatives, then for some between and ,
You do not know , but you can bound between and , 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 at 0, with between 0 and , so relative to :
(If then and ; if then and .) test_lagrange_bound_holds checks this inequality at 4001 points for every degree from 1 to 12. The bound shrinks factorially in but grows like : at degree 6 gives ; at degree 6 gives 2000. A Taylor polynomial is only good near its center.
2.3 Range reduction
Section titled “2.3 Range reduction”Every real can be written with an integer and : take . Then
and multiplying by is exact in binary floating point: it only changes the exponent field (np.ldexp(p, k)). So costs one reduction, one polynomial on , and one exponent adjustment, and the remainder bound of section 2.2 holds for every . With degree 6 the relative error is at most , about two float32 units in the last place: the target of tl_expf. Rounding down (floor) instead of to nearest leaves , doubles the largest , and multiplies the bound by .
Subtract exactly. is irrational, so the float64 constant LN2 is off by up to relative. Multiplied by that error lands in as , and inherits it: 400 times the float64 rounding level. Cody and Waite split the constant: LN2_HI holds the first 32 bits of with the low 21 bits zero, so LN2_HI is exact for , and LN2_LO LN2_HI is tiny. Then is accurate to the last bit.
The edges. overflows float64 above and is below the smallest subnormal under . Clipping to first changes no result and keeps small; then , , and nan is passed through separately.
2.4 A series for erf without cancellation
Section titled “2.4 A series for erf without cancellation”. Integrating the Taylor series of term by term gives the textbook series
which converges for every but alternates in sign. At its largest term is about , 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 has only positive coefficients:
Every term has the sign of , so nothing cancels, and each term comes from the previous one by one multiplication. The tail bound: once the ratio is below 1, the ratios keep falling, so the omitted terms are at most a geometric series, (M02.2 sums geometric series). test_erf_tail_bound_holds checks it.
Cutoff. For , , below half the gap between 1 and the float before it, so erf rounds to exactly . The series would need hundreds of terms there, and near its partial sums overflow while underflows to 0, giving $0 \cdot \infty = $ nan. So erf_series returns for . With 120 or more terms the result is within of erf everywhere.
3. Worked example by hand
Section titled “3. Worked example by hand”with degree 3. , so .
The relative error is , and the Lagrange bound (here , so the factor is 1) is : the bound holds with little room to spare. Horner’s rule evaluates the same polynomial as . This is test_hand_example.
with three terms. , ratio over : , , , sum . Prefactor . Result , against . The next term times the prefactor is , and the tail bound times the prefactor, , covers the actual gap of . This is test_erf_hand_example.
4. The interface
Section titled “4. The interface”ERF_CUTOFF: float # 6.0def 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 , nan, overflow, and underflow without warnings. Negative n, deg, or terms < 1 raise ValueError.
What the tests check
Section titled “What the tests check”| Test | KIND | Checks | Why it matters downstream |
|---|---|---|---|
test_hand_example | unit, smoke | section 3: , error inside the bound | you and the tests agree on reduction and polynomial |
test_coefficients_are_inverse_factorials | unit | , length , | the coefficient order horner and M09.6 use |
test_lagrange_bound_holds | property | the bound of section 2.2 at 4001 points, degrees 1 to 12 | the remainder bound is the design’s property test |
test_degree_six_is_float32_accurate | golden | relative error at most on | tl_expf’s accuracy budget |
test_full_precision_across_the_range | golden | degree 20 within near | the two-part reduction |
test_special_values_and_extremes | boundary | , nan, overflow, underflow, subnormal, no warnings | masked logits are |
test_shape_and_scalars | unit | scalars, matrices, integer input, deg < 0 raises | callers pass every shape |
test_erf_hand_example | unit, smoke | section 3: 0.5199464 | the series and its ratio |
test_erf_matches_math_erf | golden | 160 terms within of math.erf on | gelu_erf must match PyTorch |
test_erf_no_cancellation_at_x_5 | boundary | to | the alternating series fails here |
test_erf_cutoff_and_specials | boundary | at and beyond 6, , nan, 0 | GELU feeds up to 28 |
test_erf_tail_bound_holds | property | the geometric tail bound at many and | the remainder for erf |
5. Pitfalls
Section titled “5. Pitfalls”| Pitfall | Symptom | Caught by |
|---|---|---|
1. coefficients in the wrong order for the evaluator (numpy’s polyval is descending, horner ascending) | evaluates the reversed polynomial; at degree 3, | test_hand_example, test_lagrange_bound_holds (mutant s01) |
| 2. rounding down instead of to nearest | up to 0.69, error at degree 6 | test_degree_six_is_float32_accurate (mutant s04) |
| 3. one rounded in the reduction | relative error near | test_full_precision_across_the_range (mutant s05) |
| 4. the alternating erf series | 7 correct digits at | test_erf_no_cancellation_at_x_5 (mutant s09) |
| 5. no cutoff for large | nan at , wrong values beyond 6 | test_erf_cutoff_and_specials (mutant s10) |
| adding the low part of instead of subtracting it | relative error per unit of | test_hand_example (mutant s02) |
| coefficients | every value wrong | test_coefficients_are_inverse_factorials (mutant s03) |
| nan passed through the clip as 0 | test_special_values_and_extremes (mutant s06) | |
| an off-by-one in the erf ratio or term count | the hand example’s third term is wrong | test_erf_hand_example (mutants s07, s08) |
6. Where it’s used next
Section titled “6. Where it’s used next”| Direction | Module | How it uses this |
|---|---|---|
| Back | M00.4 | horner(exp_taylor_coeffs(deg), r) evaluates the polynomial |
| Back | M01.1 | derivatives and the first terms of Taylor’s formula (reading) |
| Forward | M01.3 | gelu_erf and dgelu_erf compute with erf_series(x / sqrt(2), 160) |
| Forward | M08.1 | dual_erf differentiates erf in forward mode |
| Forward | M09.2 | stable softmax and logsumexp: why overflows at 709.78 |
| Forward | M09.6 | tl_expf in C: the same reduction and a degree-6 polynomial in float32, checked against your Python |
Going further
Section titled “Going further”| Your piece | Production equivalent | What it adds | Where to look |
|---|---|---|---|
exp_range_reduced | glibc exp and expf | a 128-entry table () so is even smaller and the polynomial shorter; correctly rounded in most cases | glibc sysdeps/ieee754/dbl-64/e_exp.c, sysdeps/ieee754/flt-32/e_expf.c |
| Taylor coefficients | minimax 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 accuracy | Sollya; M09.6 |
| two-part | Payne and Hanek reduction | exact reduction of and for arguments like , where needs a thousand bits | Muller, Elementary Functions, ch. 11 |
erf_series | SLEEF, CUDA erff | branch-free vectorized approximations for whole SIMD lanes | SLEEF src/libm/sleefsimdsp.c |