Skip to content

Commit c251f15

Browse files
authored
Refined the PDF layout for Appendix B (appendix_data.qmd) (#1831)
1 parent 8fcd0f3 commit c251f15

1 file changed

Lines changed: 54 additions & 31 deletions

File tree

book/quarto/contents/vol1/backmatter/appendix_data.qmd

Lines changed: 54 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -185,12 +185,12 @@ Even after data arrives at the machine, we face one final hurdle: the serializat
185185
[^fn-serialization]: **Serialization**: From Latin *serialis* (forming a series). The process of converting in-memory data structures into a byte stream for storage or transmission, and the reverse (*deserialization*). In ML pipelines, the choice of serialization format can dominate end-to-end training time; @sec-data-engineering covers pipeline design strategies that minimize this overhead.
186186

187187
| **Format** | **Decoding Speed (MB/s)** | **Relative CPU Decode Cost** | **Suitability** |
188-
|:------------------|----------------------------------------------------:|------------------------------------------------:|:--------------------|
188+
|:------------------|:---------------------------------------------------:|:-----------------------------------------------:|:--------------------|
189189
| **CSV/JSON** | `{python} SerializationCost.csv_speed_mb_s_str` | `{python} SerializationCost.csv_cycles_str` | Debugging only |
190190
| **Protobuf** | `{python} SerializationCost.proto_speed_mb_s_str` | `{python} SerializationCost.proto_cycles_str` | RPC/Messages |
191191
| **Parquet/Arrow** | `{python} SerializationCost.parquet_speed_mb_s_str` | `{python} SerializationCost.parquet_cycles_str` | High-Scale Training |
192192

193-
: **Serialization Overhead**: Zero-copy formats like Arrow are at least 10$\times$ faster than row-based text formats in this representative comparison because they align directly with internal memory structures. {#tbl-serialization-cost tbl-colwidths="[25,25,25,25]"}
193+
: **Serialization Overhead**: Zero-copy formats like Arrow are at least 10$\times$ faster than row-based text formats in this representative comparison because they align directly with internal memory structures. {#tbl-serialization-cost tbl-colwidths="[21,27,27,25]"}
194194

195195
### Row vs. columnar formats {#sec-appdx-data-foundations-row-vs-columnar-formats-bca2}
196196

@@ -546,40 +546,54 @@ class KLDrift:
546546
Pt_vec_str = MarkdownStr(f"[{Pt_fmt[0]}, {Pt_fmt[1]}, {Pt_fmt[2]}]")
547547
548548
# Renamed from kl_pq_formula → kl_pq_eq (canonical _eq suffix for fmt_math expression)
549-
kl_pq_eq = fmt_math(
549+
kl_pq_eq = (
550+
"\\begin{gather*}\n"
550551
f"\\mathcal{{D}}_{{\\text{{KL}}}}(P_0 \\lVert P_t) = {P0_fmt[0]} \\log\\frac{{{P0_fmt[0]}}}{{{Pt_fmt[0]}}} "
551552
f"+ {P0_fmt[1]} \\log\\frac{{{P0_fmt[1]}}}{{{Pt_fmt[1]}}} "
552553
f"+ {P0_fmt[2]} \\log\\frac{{{P0_fmt[2]}}}{{{Pt_fmt[2]}}}"
554+
"\\end{gather*}"
553555
)
554556
# Renamed from kl_pq_calc → kl_pq_calc_eq
555-
kl_pq_calc_eq = fmt_math(
557+
kl_pq_calc_eq = (
558+
"\\begin{gather*}\n"
556559
f"= {P0_fmt[0]} \\times {lr_fmt[0]} "
557560
f"+ {P0_fmt[1]} \\times ({lr_fmt[1]}) "
558561
f"+ {P0_fmt[2]} \\times ({lr_fmt[2]}) "
562+
"\\\\[-1pt]\n"
559563
f"= {kl_pq_t_fmt[0]} + ({kl_pq_t_fmt[1]}) + ({kl_pq_t_fmt[2]}) "
560-
f"= {kl_pq_str}"
564+
f"= {kl_pq_str}\\text{{ nats}}"
565+
"\\end{gather*}"
561566
)
562567
# Renamed from kl_qp_formula → kl_qp_eq
563-
kl_qp_eq = fmt_math(
568+
kl_qp_eq = (
569+
"\\begin{gather*}\n"
564570
f"\\mathcal{{D}}_{{\\text{{KL}}}}(P_t \\lVert P_0) = {Pt_fmt[0]} \\log\\frac{{{Pt_fmt[0]}}}{{{P0_fmt[0]}}} "
565571
f"+ {Pt_fmt[1]} \\log\\frac{{{Pt_fmt[1]}}}{{{P0_fmt[1]}}} "
566572
f"+ {Pt_fmt[2]} \\log\\frac{{{Pt_fmt[2]}}}{{{P0_fmt[2]}}}"
573+
"\\end{gather*}"
567574
)
568575
# Renamed from kl_qp_calc → kl_qp_calc_eq
569-
kl_qp_calc_eq = fmt_math(
576+
kl_qp_calc_eq = (
577+
"\\begin{gather*}\n"
570578
f"= {Pt_fmt[0]} \\times ({-lr[0]:.4f}) "
571579
f"+ {Pt_fmt[1]} \\times ({-lr[1]:.4f}) "
572580
f"+ {Pt_fmt[2]} \\times ({-lr[2]:.4f}) "
581+
"\\\\[-1pt]\n"
573582
f"= {kl_qp_t_fmt[0]} + {kl_qp_t_fmt[1]} + {kl_qp_t_fmt[2]} "
574-
f"= {kl_qp_str}"
583+
f"= {kl_qp_str}\\text{{ nats}}"
584+
"\\end{gather*}"
575585
)
576-
psi_eq = fmt_math(
577-
f"\\text{{PSI}} = \\sum (P_{{0,i}} - P_{{t,i}}) \\log\\frac{{P_{{0,i}}}}{{P_{{t,i}}}} "
586+
psi_eq = (
587+
"\\begin{gather*}\n"
588+
f"\\text{{PSI}} = \\sum (P_{{0,i}} - P_{{t,i}}) "
589+
f"\\log\\frac{{P_{{0,i}}}}{{P_{{t,i}}}} "
578590
f"= ({diff_fmt[0]})({lr_fmt[0]}) "
579591
f"+ ({diff_fmt[1]})({lr_fmt[1]}) "
580592
f"+ ({diff_fmt[2]})({lr_fmt[2]}) "
593+
"\\\\[-1pt]\n"
581594
f"= {psi_t_fmt[0]} + {psi_t_fmt[1]} + {psi_t_fmt[2]} "
582-
f"= {psi_str}"
595+
f"= {psi_str}\n"
596+
"\\end{gather*}"
583597
)
584598
```
585599

@@ -588,19 +602,27 @@ class KLDrift:
588602
**Scenario**: A sentiment classifier was trained on data where `{python} KLDrift.scenario_str`.
589603

590604
Training distribution $P_0$: `{python} KLDrift.P0_vec_str`. Serving distribution $P_t$: `{python} KLDrift.Pt_vec_str`.
591-
592-
`{python} KLDrift.kl_qp_eq`
593-
594-
`{python} KLDrift.kl_qp_calc_eq` nats
595-
596-
`{python} KLDrift.kl_pq_eq`
597-
598-
`{python} KLDrift.kl_pq_calc_eq` nats
605+
```{python}
606+
#| echo: false
607+
#| output: asis
608+
print(r"\vspace{-4mm}")
609+
print(KLDrift.kl_qp_eq)
610+
print(r"\vspace{-6mm}")
611+
print(KLDrift.kl_qp_calc_eq)
612+
print(r"\vspace{-4mm}")
613+
print(KLDrift.kl_pq_eq)
614+
print(r"\vspace{-6mm}")
615+
print(KLDrift.kl_pq_calc_eq)
616+
```
599617

600618
Notice the asymmetry: $\mathcal{D}_{\text{KL}}(P_0 \lVert P_t) \neq \mathcal{D}_{\text{KL}}(P_t \lVert P_0)$. The Population Stability Index (PSI) symmetrizes this:
601-
602-
`{python} KLDrift.psi_eq`
603-
619+
```{python}
620+
#| echo: false
621+
#| output: asis
622+
print(r"\vspace{-4mm}")
623+
print(KLDrift.psi_eq)
624+
```
625+
604626
```{python}
605627
#| echo: false
606628
#| label: psi-threshold
@@ -662,12 +684,11 @@ class LogitOverflowExample:
662684
Neural networks output *logits*[^fn-logit] (unnormalized scores), not probabilities. We convert them using Softmax:
663685

664686
[^fn-logit]: **Logit**: From *log* + *unit*, coined by Joseph Berkson in 1944. The logit function is the inverse of the logistic (sigmoid) function: $\text{logit}(p) = \log(p/(1-p))$. In deep learning, "logits" refers more loosely to the raw, unnormalized output of the final linear layer before any activation function is applied.
665-
666687
$$ \text{Softmax}(z_i) = \frac{e^{z_i}}{\sum e^{z_j}} $$
667688

668-
The problem is that if $z_i$ is large (for example, `{python} LogitOverflowExample.logit_val_str`), the exponential $e^{z_i}$ overflows common training and inference formats such as FP32, BF16, and FP16. FP64 can represent this specific value, but production ML kernels rarely use FP64 for softmax. The solution is to compute in log-space: the "Log-Sum-Exp" trick allows us to compute $\log(\sum e^{z_j})$ without ever calculating the massive exponentials directly, preserving numerical precision.[^fn-logsumexp]
689+
The problem is that if $z_i$ is large (for example, `{python} LogitOverflowExample.logit_val_str`), the exponential $e^{z_i}$ overflows common training and inference formats such as FP32, BF16, and FP16. FP64 can represent this specific value, but production ML kernels rarely use FP64 for softmax. The solution is to compute in log-space: the "Log-Sum-Exp" trick allows us to compute $\log\left(\sum e^{z_j}\right)$ without ever calculating the massive exponentials directly, preserving numerical precision.[^fn-logsumexp]
669690

670-
[^fn-logsumexp]: **Log-Sum-Exp**: Implemented as `torch.logsumexp` in PyTorch and `scipy.special.logsumexp` in SciPy. It relies on the identity $\log(\sum e^{x_i}) = a + \log(\sum e^{x_i - a})$, where $a = \max(x_i)$. Shifted values $x_i - a$ are $\le 0$, ensuring exponentials never overflow.
691+
[^fn-logsumexp]: **Log-Sum-Exp**: Implemented as `torch.logsumexp` in PyTorch and `scipy.special.logsumexp` in SciPy. It relies on the identity $\log\left(\sum e^{x_i}\right) = a + \log\left(\sum e^{x_i - a}\right)$, where $a = \max(x_i)$. Shifted values $x_i - a$ are $\le 0$, ensuring exponentials never overflow.
671692

672693
A small softmax calculation shows why this trick matters in practice, even for large but realistic logit values:
673694

@@ -778,16 +799,18 @@ class LogSumExp:
778799
**The setup**: A three-class classifier outputs logits $z =$ `{python} LogSumExp.z_math`.
779800

780801
**Without the trick (naive softmax)**:
781-
782-
`{python} LogSumExp.naive_exp0_eq`, `{python} LogSumExp.naive_exp1_eq`, `{python} LogSumExp.naive_exp2_eq`
802+
$$
803+
\text{`{python} LogSumExp.naive_exp0_eq`},\quad \text{`{python} LogSumExp.naive_exp1_eq`}, \quad \text{`{python} LogSumExp.naive_exp2_eq`}
804+
$$
783805

784806
These numbers are representable in FP64 but overflow FP32 (max $\approx 3.4 \times 10^{38}$). With FP16 (max $\approx 65{,}504$), even $e^{12}$ overflows. In practice, logits of magnitude 100 are not unusual in deep networks, and FP16/BF16 is the standard training precision—so naive softmax fails routinely.
785807

786808
**With the trick**: Subtract $a = \max(z) =$ `{python} LogSumExp.a_str`:
787-
788-
$z - a =$ `{python} LogSumExp.shifted_math`
789-
790-
`{python} LogSumExp.stable_exp0_eq`, `{python} LogSumExp.stable_exp1_eq`, `{python} LogSumExp.stable_exp2_eq`
809+
\begin{gather*}
810+
\text{$z - a =$ `{python} LogSumExp.shifted_math`}
811+
\\
812+
\text{`{python} LogSumExp.stable_exp0_eq`,}\quad \text{`{python} LogSumExp.stable_exp1_eq`,}\quad \text{`{python} LogSumExp.stable_exp2_eq`}
813+
\end{gather*}
791814

792815
Sum = `{python} LogSumExp.sum_str`. LogSumExp = `{python} LogSumExp.a_str` `{python} LogSumExp.log_sum_eq`.
793816

0 commit comments

Comments
 (0)